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