Copyright updates:
[exim.git] / src / src / auths / cram_md5.c
... / ...
CommitLineData
1/*************************************************
2* Exim - an Internet mail transport agent *
3*************************************************/
4
5/* Copyright (c) University of Cambridge 1995 - 2018 */
6/* Copyright (c) The Exim Maintainers 2020 */
7/* See the file NOTICE for conditions of use and distribution. */
8
9
10/* The stand-alone version just tests the algorithm. We have to drag
11in the MD5 computation functions, without their own stand-alone main
12program. */
13
14#ifdef STAND_ALONE
15#define CRAM_STAND_ALONE
16#include "md5.c"
17
18
19/* This is the normal, non-stand-alone case */
20
21#else
22#include "../exim.h"
23#include "cram_md5.h"
24
25/* Options specific to the cram_md5 authentication mechanism. */
26
27optionlist auth_cram_md5_options[] = {
28 { "client_name", opt_stringptr,
29 OPT_OFF(auth_cram_md5_options_block, client_name) },
30 { "client_secret", opt_stringptr,
31 OPT_OFF(auth_cram_md5_options_block, client_secret) },
32 { "server_secret", opt_stringptr,
33 OPT_OFF(auth_cram_md5_options_block, server_secret) }
34};
35
36/* Size of the options list. An extern variable has to be used so that its
37address can appear in the tables drtables.c. */
38
39int auth_cram_md5_options_count =
40 sizeof(auth_cram_md5_options)/sizeof(optionlist);
41
42/* Default private options block for the condition authentication method. */
43
44auth_cram_md5_options_block auth_cram_md5_option_defaults = {
45 NULL, /* server_secret */
46 NULL, /* client_secret */
47 NULL /* client_name */
48};
49
50
51#ifdef MACRO_PREDEF
52
53/* Dummy values */
54void auth_cram_md5_init(auth_instance *ablock) {}
55int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;}
56int auth_cram_md5_client(auth_instance *ablock, void *sx, int timeout,
57 uschar *buffer, int buffsize) {return 0;}
58
59#else /*!MACRO_PREDEF*/
60
61
62/*************************************************
63* Initialization entry point *
64*************************************************/
65
66/* Called for each instance, after its options have been read, to
67enable consistency checks to be done, or anything else that needs
68to be set up. */
69
70void
71auth_cram_md5_init(auth_instance *ablock)
72{
73auth_cram_md5_options_block *ob =
74 (auth_cram_md5_options_block *)(ablock->options_block);
75if (ob->server_secret != NULL) ablock->server = TRUE;
76if (ob->client_secret != NULL)
77 {
78 ablock->client = TRUE;
79 if (ob->client_name == NULL) ob->client_name = primary_hostname;
80 }
81}
82
83#endif /*!MACRO_PREDEF*/
84#endif /* STAND_ALONE */
85
86
87
88#ifndef MACRO_PREDEF
89/*************************************************
90* Perform the CRAM-MD5 algorithm *
91*************************************************/
92
93/* The CRAM-MD5 algorithm is described in RFC 2195. It computes
94
95 MD5((secret XOR opad), MD5((secret XOR ipad), challenge))
96
97where secret is padded out to 64 characters (after being reduced to an MD5
98digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and
990x5c respectively, and comma means concatenation.
100
101Arguments:
102 secret the shared secret
103 challenge the challenge text
104 digest 16-byte slot to put the answer in
105
106Returns: nothing
107*/
108
109static void
110compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr)
111{
112md5 base;
113int len = Ustrlen(secret);
114uschar isecret[64];
115uschar osecret[64];
116uschar md5secret[16];
117
118/* If the secret is longer than 64 characters, we compute its MD5 digest
119and use that. */
120
121if (len > 64)
122 {
123 md5_start(&base);
124 md5_end(&base, US secret, len, md5secret);
125 secret = US md5secret;
126 len = 16;
127 }
128
129/* The key length is now known to be <= 64. Set up the padded and xor'ed
130versions. */
131
132memcpy(isecret, secret, len);
133memset(isecret+len, 0, 64-len);
134memcpy(osecret, isecret, 64);
135
136for (int i = 0; i < 64; i++)
137 {
138 isecret[i] ^= 0x36;
139 osecret[i] ^= 0x5c;
140 }
141
142/* Compute the inner MD5 digest */
143
144md5_start(&base);
145md5_mid(&base, isecret);
146md5_end(&base, US challenge, Ustrlen(challenge), md5secret);
147
148/* Compute the outer MD5 digest */
149
150md5_start(&base);
151md5_mid(&base, osecret);
152md5_end(&base, md5secret, 16, digestptr);
153}
154
155
156#ifndef STAND_ALONE
157
158/*************************************************
159* Server entry point *
160*************************************************/
161
162/* For interface, see auths/README */
163
164int
165auth_cram_md5_server(auth_instance *ablock, uschar *data)
166{
167auth_cram_md5_options_block *ob =
168 (auth_cram_md5_options_block *)(ablock->options_block);
169uschar *challenge = string_sprintf("<%d.%ld@%s>", getpid(),
170 (long int) time(NULL), primary_hostname);
171uschar *clear, *secret;
172uschar digest[16];
173int i, rc, len;
174
175/* If we are running in the test harness, always send the same challenge,
176an example string taken from the RFC. */
177
178if (f.running_in_test_harness)
179 challenge = US"<1896.697170952@postoffice.reston.mci.net>";
180
181/* No data should have been sent with the AUTH command */
182
183if (*data) return UNEXPECTED;
184
185/* Send the challenge, read the return */
186
187if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc;
188if ((len = b64decode(data, &clear)) < 0) return BAD64;
189
190/* The return consists of a user name, space-separated from the CRAM-MD5
191digest, expressed in hex. Extract the user name and put it in $auth1 and $1.
192The former is now the preferred variable; the latter is the original one. Then
193check that the remaining length is 32. */
194
195auth_vars[0] = expand_nstring[1] = clear;
196while (*clear && !isspace(*clear)) clear++;
197if (!isspace(*clear)) return FAIL;
198*clear++ = 0;
199
200expand_nlength[1] = clear - expand_nstring[1] - 1;
201if (len - expand_nlength[1] - 1 != 32) return FAIL;
202expand_nmax = 1;
203
204/* Expand the server_secret string so that it can compute a value dependent on
205the user name if necessary. */
206
207debug_print_string(ablock->server_debug_string); /* customized debugging */
208secret = expand_string(ob->server_secret);
209
210/* A forced fail implies failure of authentication - i.e. we have no secret for
211the given name. */
212
213if (secret == NULL)
214 {
215 if (f.expand_string_forcedfail) return FAIL;
216 auth_defer_msg = expand_string_message;
217 return DEFER;
218 }
219
220/* Compute the CRAM-MD5 digest that we should have received from the client. */
221
222compute_cram_md5(secret, challenge, digest);
223
224HDEBUG(D_auth)
225 {
226 uschar buff[64];
227 debug_printf("CRAM-MD5: user name = %s\n", auth_vars[0]);
228 debug_printf(" challenge = %s\n", challenge);
229 debug_printf(" received = %s\n", clear);
230 Ustrcpy(buff, US" digest = ");
231 for (i = 0; i < 16; i++) sprintf(CS buff+22+2*i, "%02x", digest[i]);
232 debug_printf("%.54s\n", buff);
233 }
234
235/* We now have to compare the digest, which is 16 bytes in binary, with the
236data received, which is expressed in lower case hex. We checked above that
237there were 32 characters of data left. */
238
239for (i = 0; i < 16; i++)
240 {
241 int a = *clear++;
242 int b = *clear++;
243 if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) +
244 ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL;
245 }
246
247/* Expand server_condition as an authorization check */
248return auth_check_serv_cond(ablock);
249}
250
251
252
253/*************************************************
254* Client entry point *
255*************************************************/
256
257/* For interface, see auths/README */
258
259int
260auth_cram_md5_client(
261 auth_instance *ablock, /* authenticator block */
262 void * sx, /* smtp connextion */
263 int timeout, /* command timeout */
264 uschar *buffer, /* for reading response */
265 int buffsize) /* size of buffer */
266{
267auth_cram_md5_options_block *ob =
268 (auth_cram_md5_options_block *)(ablock->options_block);
269uschar *secret = expand_string(ob->client_secret);
270uschar *name = expand_string(ob->client_name);
271uschar *challenge, *p;
272int i;
273uschar digest[16];
274
275/* If expansion of either the secret or the user name failed, return CANCELLED
276or ERROR, as appropriate. */
277
278if (!secret || !name)
279 {
280 if (f.expand_string_forcedfail)
281 {
282 *buffer = 0; /* No message */
283 return CANCELLED;
284 }
285 string_format(buffer, buffsize, "expansion of \"%s\" failed in "
286 "%s authenticator: %s",
287 !secret ? ob->client_secret : ob->client_name,
288 ablock->name, expand_string_message);
289 return ERROR;
290 }
291
292/* Initiate the authentication exchange and read the challenge, which arrives
293in base 64. */
294
295if (smtp_write_command(sx, SCMD_FLUSH, "AUTH %s\r\n", ablock->public_name) < 0)
296 return FAIL_SEND;
297if (!smtp_read_response(sx, buffer, buffsize, '3', timeout))
298 return FAIL;
299
300if (b64decode(buffer + 4, &challenge) < 0)
301 {
302 string_format(buffer, buffsize, "bad base 64 string in challenge: %s",
303 big_buffer + 4);
304 return ERROR;
305 }
306
307/* Run the CRAM-MD5 algorithm on the secret and the challenge */
308
309compute_cram_md5(secret, challenge, digest);
310
311/* Create the response from the user name plus the CRAM-MD5 digest */
312
313string_format(big_buffer, big_buffer_size - 36, "%s", name);
314for (p = big_buffer; *p; ) p++;
315*p++ = ' ';
316
317for (i = 0; i < 16; i++)
318 p += sprintf(CS p, "%02x", digest[i]);
319
320/* Send the response, in base 64, and check the result. The response is
321in big_buffer, but b64encode() returns its result in working store,
322so calling smtp_write_command(), which uses big_buffer, is OK. */
323
324buffer[0] = 0;
325if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", b64encode(CUS big_buffer,
326 p - big_buffer)) < 0) return FAIL_SEND;
327
328return smtp_read_response(sx, US buffer, buffsize, '2', timeout)
329 ? OK : FAIL;
330}
331#endif /* STAND_ALONE */
332
333
334/*************************************************
335**************************************************
336* Stand-alone test program *
337**************************************************
338*************************************************/
339
340#ifdef STAND_ALONE
341
342int main(int argc, char **argv)
343{
344int i;
345uschar *secret = US argv[1];
346uschar *challenge = US argv[2];
347uschar digest[16];
348
349compute_cram_md5(secret, challenge, digest);
350
351for (i = 0; i < 16; i++) printf("%02x", digest[i]);
352printf("\n");
353
354return 0;
355}
356
357#endif
358
359#endif /*!MACRO_PREDEF*/
360/* End of cram_md5.c */