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