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