Do not use arc4random_stir() directly (Bug 2304)
[exim.git] / src / src / auths / cram_md5.c
CommitLineData
0756eb3c
PH
1/*************************************************
2* Exim - an Internet mail transport agent *
3*************************************************/
4
f9ba5e22 5/* Copyright (c) University of Cambridge 1995 - 2018 */
0756eb3c
PH
6/* See the file NOTICE for conditions of use and distribution. */
7
8
9/* The stand-alone version just tests the algorithm. We have to drag
10in the MD5 computation functions, without their own stand-alone main
11program. */
12
13#ifdef STAND_ALONE
14#define CRAM_STAND_ALONE
15#include "md5.c"
16
17
18/* This is the normal, non-stand-alone case */
19
20#else
21#include "../exim.h"
22#include "cram_md5.h"
23
24/* Options specific to the cram_md5 authentication mechanism. */
25
26optionlist auth_cram_md5_options[] = {
27 { "client_name", opt_stringptr,
28 (void *)(offsetof(auth_cram_md5_options_block, client_name)) },
29 { "client_secret", opt_stringptr,
30 (void *)(offsetof(auth_cram_md5_options_block, client_secret)) },
31 { "server_secret", opt_stringptr,
32 (void *)(offsetof(auth_cram_md5_options_block, server_secret)) }
33};
34
35/* Size of the options list. An extern variable has to be used so that its
36address can appear in the tables drtables.c. */
37
38int auth_cram_md5_options_count =
39 sizeof(auth_cram_md5_options)/sizeof(optionlist);
40
4c04137d 41/* Default private options block for the condition authentication method. */
0756eb3c
PH
42
43auth_cram_md5_options_block auth_cram_md5_option_defaults = {
44 NULL, /* server_secret */
45 NULL, /* client_secret */
46 NULL /* client_name */
47};
48
49
d185889f
JH
50#ifdef MACRO_PREDEF
51
52/* Dummy values */
53void auth_cram_md5_init(auth_instance *ablock) {}
54int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;}
55int auth_cram_md5_client(auth_instance *ablock, smtp_inblock *inblock,
56 smtp_outblock *outblock, int timeout, uschar *buffer, int buffsize) {return 0;}
57
58#else /*!MACRO_PREDEF*/
59
60
0756eb3c
PH
61/*************************************************
62* Initialization entry point *
63*************************************************/
64
65/* Called for each instance, after its options have been read, to
66enable consistency checks to be done, or anything else that needs
67to be set up. */
68
69void
70auth_cram_md5_init(auth_instance *ablock)
71{
72auth_cram_md5_options_block *ob =
73 (auth_cram_md5_options_block *)(ablock->options_block);
74if (ob->server_secret != NULL) ablock->server = TRUE;
75if (ob->client_secret != NULL)
76 {
77 ablock->client = TRUE;
78 if (ob->client_name == NULL) ob->client_name = primary_hostname;
79 }
80}
81
d185889f 82#endif /*!MACRO_PREDEF*/
0756eb3c
PH
83#endif /* STAND_ALONE */
84
85
86
d185889f 87#ifndef MACRO_PREDEF
0756eb3c 88/*************************************************
4c04137d 89* Perform the CRAM-MD5 algorithm *
0756eb3c
PH
90*************************************************/
91
92/* The CRAM-MD5 algorithm is described in RFC 2195. It computes
93
94 MD5((secret XOR opad), MD5((secret XOR ipad), challenge))
95
96where secret is padded out to 64 characters (after being reduced to an MD5
97digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and
980x5c respectively, and comma means concatenation.
99
100Arguments:
101 secret the shared secret
102 challenge the challenge text
103 digest 16-byte slot to put the answer in
104
105Returns: nothing
106*/
107
108static void
109compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr)
110{
111md5 base;
112int i;
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);
5903c6ff
JH
124 md5_end(&base, US secret, len, md5secret);
125 secret = US md5secret;
0756eb3c
PH
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 (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);
5903c6ff 146md5_end(&base, US challenge, Ustrlen(challenge), md5secret);
0756eb3c
PH
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);
438257ba
PP
169uschar *challenge = string_sprintf("<%d.%ld@%s>", getpid(),
170 (long int) time(NULL), primary_hostname);
0756eb3c
PH
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
8768d548 178if (f.running_in_test_harness)
0756eb3c
PH
179 challenge = US"<1896.697170952@postoffice.reston.mci.net>";
180
181/* No data should have been sent with the AUTH command */
182
183if (*data != 0) return UNEXPECTED;
184
185/* Send the challenge, read the return */
186
187if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc;
f4d091fb 188if ((len = b64decode(data, &clear)) < 0) return BAD64;
0756eb3c
PH
189
190/* The return consists of a user name, space-separated from the CRAM-MD5
f78eb7c6
PH
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. */
0756eb3c 194
f78eb7c6 195auth_vars[0] = expand_nstring[1] = clear;
0756eb3c
PH
196while (*clear != 0 && !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 {
8768d548 215 if (f.expand_string_forcedfail) return FAIL;
0756eb3c
PH
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];
f78eb7c6 227 debug_printf("CRAM-MD5: user name = %s\n", auth_vars[0]);
0756eb3c
PH
228 debug_printf(" challenge = %s\n", challenge);
229 debug_printf(" received = %s\n", clear);
230 Ustrcpy(buff," 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
16ff981e
PH
247/* Expand server_condition as an authorization check */
248return auth_check_serv_cond(ablock);
0756eb3c
PH
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 smtp_inblock *inblock, /* input connection */
263 smtp_outblock *outblock, /* output connection */
264 int timeout, /* command timeout */
4730f942 265 uschar *buffer, /* for reading response */
0756eb3c
PH
266 int buffsize) /* size of buffer */
267{
268auth_cram_md5_options_block *ob =
269 (auth_cram_md5_options_block *)(ablock->options_block);
270uschar *secret = expand_string(ob->client_secret);
271uschar *name = expand_string(ob->client_name);
272uschar *challenge, *p;
273int i;
274uschar digest[16];
275
276/* If expansion of either the secret or the user name failed, return CANCELLED
4c04137d 277or ERROR, as appropriate. */
0756eb3c 278
d129bdf7 279if (!secret || !name)
0756eb3c 280 {
8768d548 281 if (f.expand_string_forcedfail)
4730f942
PH
282 {
283 *buffer = 0; /* No message */
284 return CANCELLED;
285 }
0756eb3c
PH
286 string_format(buffer, buffsize, "expansion of \"%s\" failed in "
287 "%s authenticator: %s",
d129bdf7 288 !secret ? ob->client_secret : ob->client_name,
0756eb3c
PH
289 ablock->name, expand_string_message);
290 return ERROR;
291 }
292
293/* Initiate the authentication exchange and read the challenge, which arrives
294in base 64. */
295
4e910c01
JH
296if (smtp_write_command(outblock, SCMD_FLUSH, "AUTH %s\r\n",
297 ablock->public_name) < 0)
0756eb3c 298 return FAIL_SEND;
d129bdf7 299if (!smtp_read_response(inblock, buffer, buffsize, '3', timeout))
0756eb3c
PH
300 return FAIL;
301
f4d091fb 302if (b64decode(buffer + 4, &challenge) < 0)
0756eb3c
PH
303 {
304 string_format(buffer, buffsize, "bad base 64 string in challenge: %s",
305 big_buffer + 4);
306 return ERROR;
307 }
308
309/* Run the CRAM-MD5 algorithm on the secret and the challenge */
310
311compute_cram_md5(secret, challenge, digest);
312
313/* Create the response from the user name plus the CRAM-MD5 digest */
314
315string_format(big_buffer, big_buffer_size - 36, "%s", name);
d129bdf7 316for (p = big_buffer; *p; ) p++;
0756eb3c
PH
317*p++ = ' ';
318
319for (i = 0; i < 16; i++)
5976eb99 320 p += sprintf(CS p, "%02x", digest[i]);
0756eb3c
PH
321
322/* Send the response, in base 64, and check the result. The response is
f4d091fb 323in big_buffer, but b64encode() returns its result in working store,
0756eb3c
PH
324so calling smtp_write_command(), which uses big_buffer, is OK. */
325
326buffer[0] = 0;
4e910c01 327if (smtp_write_command(outblock, SCMD_FLUSH, "%s\r\n", b64encode(big_buffer,
0756eb3c
PH
328 p - big_buffer)) < 0) return FAIL_SEND;
329
5903c6ff 330return smtp_read_response(inblock, US buffer, buffsize, '2', timeout)
d129bdf7 331 ? OK : FAIL;
0756eb3c
PH
332}
333#endif /* STAND_ALONE */
334
335
336/*************************************************
337**************************************************
338* Stand-alone test program *
339**************************************************
340*************************************************/
341
342#ifdef STAND_ALONE
343
344int main(int argc, char **argv)
345{
346int i;
347uschar *secret = US argv[1];
348uschar *challenge = US argv[2];
349uschar digest[16];
350
351compute_cram_md5(secret, challenge, digest);
352
353for (i = 0; i < 16; i++) printf("%02x", digest[i]);
354printf("\n");
355
356return 0;
357}
358
359#endif
360
d185889f 361#endif /*!MACRO_PREDEF*/
0756eb3c 362/* End of cram_md5.c */