3f80bd6745f035a800461dd00c222a8ce677de8f
[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 - 2017 */
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
10 in the MD5 computation functions, without their own stand-alone main
11 program. */
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
26 optionlist 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
36 address can appear in the tables drtables.c. */
37
38 int auth_cram_md5_options_count =
39   sizeof(auth_cram_md5_options)/sizeof(optionlist);
40
41 /* Default private options block for the condition authentication method. */
42
43 auth_cram_md5_options_block auth_cram_md5_option_defaults = {
44   NULL,             /* server_secret */
45   NULL,             /* client_secret */
46   NULL              /* client_name */
47 };
48
49
50 #ifdef MACRO_PREDEF
51
52 /* Dummy values */
53 void auth_cram_md5_init(auth_instance *ablock) {}
54 int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;}
55 int 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
61 /*************************************************
62 *          Initialization entry point            *
63 *************************************************/
64
65 /* Called for each instance, after its options have been read, to
66 enable consistency checks to be done, or anything else that needs
67 to be set up. */
68
69 void
70 auth_cram_md5_init(auth_instance *ablock)
71 {
72 auth_cram_md5_options_block *ob =
73   (auth_cram_md5_options_block *)(ablock->options_block);
74 if (ob->server_secret != NULL) ablock->server = TRUE;
75 if (ob->client_secret != NULL)
76   {
77   ablock->client = TRUE;
78   if (ob->client_name == NULL) ob->client_name = primary_hostname;
79   }
80 }
81
82 #endif  /*!MACRO_PREDEF*/
83 #endif  /* STAND_ALONE */
84
85
86
87 #ifndef MACRO_PREDEF
88 /*************************************************
89 *      Perform the CRAM-MD5 algorithm            *
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
96 where secret is padded out to 64 characters (after being reduced to an MD5
97 digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and
98 0x5c respectively, and comma means concatenation.
99
100 Arguments:
101   secret         the shared secret
102   challenge      the challenge text
103   digest         16-byte slot to put the answer in
104
105 Returns:         nothing
106 */
107
108 static void
109 compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr)
110 {
111 md5 base;
112 int i;
113 int len = Ustrlen(secret);
114 uschar isecret[64];
115 uschar osecret[64];
116 uschar md5secret[16];
117
118 /* If the secret is longer than 64 characters, we compute its MD5 digest
119 and use that. */
120
121 if (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
130 versions. */
131
132 memcpy(isecret, secret, len);
133 memset(isecret+len, 0, 64-len);
134 memcpy(osecret, isecret, 64);
135
136 for (i = 0; i < 64; i++)
137   {
138   isecret[i] ^= 0x36;
139   osecret[i] ^= 0x5c;
140   }
141
142 /* Compute the inner MD5 digest */
143
144 md5_start(&base);
145 md5_mid(&base, isecret);
146 md5_end(&base, US challenge, Ustrlen(challenge), md5secret);
147
148 /* Compute the outer MD5 digest */
149
150 md5_start(&base);
151 md5_mid(&base, osecret);
152 md5_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
164 int
165 auth_cram_md5_server(auth_instance *ablock, uschar *data)
166 {
167 auth_cram_md5_options_block *ob =
168   (auth_cram_md5_options_block *)(ablock->options_block);
169 uschar *challenge = string_sprintf("<%d.%ld@%s>", getpid(),
170     (long int) time(NULL), primary_hostname);
171 uschar *clear, *secret;
172 uschar digest[16];
173 int i, rc, len;
174
175 /* If we are running in the test harness, always send the same challenge,
176 an example string taken from the RFC. */
177
178 if (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
183 if (*data != 0) return UNEXPECTED;
184
185 /* Send the challenge, read the return */
186
187 if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc;
188 if ((len = b64decode(data, &clear)) < 0) return BAD64;
189
190 /* The return consists of a user name, space-separated from the CRAM-MD5
191 digest, expressed in hex. Extract the user name and put it in $auth1 and $1.
192 The former is now the preferred variable; the latter is the original one. Then
193 check that the remaining length is 32. */
194
195 auth_vars[0] = expand_nstring[1] = clear;
196 while (*clear != 0 && !isspace(*clear)) clear++;
197 if (!isspace(*clear)) return FAIL;
198 *clear++ = 0;
199
200 expand_nlength[1] = clear - expand_nstring[1] - 1;
201 if (len - expand_nlength[1] - 1 != 32) return FAIL;
202 expand_nmax = 1;
203
204 /* Expand the server_secret string so that it can compute a value dependent on
205 the user name if necessary. */
206
207 debug_print_string(ablock->server_debug_string);    /* customized debugging */
208 secret = expand_string(ob->server_secret);
209
210 /* A forced fail implies failure of authentication - i.e. we have no secret for
211 the given name. */
212
213 if (secret == NULL)
214   {
215   if (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
222 compute_cram_md5(secret, challenge, digest);
223
224 HDEBUG(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,"          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
236 data received, which is expressed in lower case hex. We checked above that
237 there were 32 characters of data left. */
238
239 for (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 */
248 return auth_check_serv_cond(ablock);
249 }
250
251
252
253 /*************************************************
254 *              Client entry point                *
255 *************************************************/
256
257 /* For interface, see auths/README */
258
259 int
260 auth_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 */
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 (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(outblock, SCMD_FLUSH, "AUTH %s\r\n",
297                         ablock->public_name) < 0)
298   return FAIL_SEND;
299 if (!smtp_read_response(inblock, buffer, buffsize, '3', timeout))
300   return FAIL;
301
302 if (b64decode(buffer + 4, &challenge) < 0)
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
311 compute_cram_md5(secret, challenge, digest);
312
313 /* Create the response from the user name plus the CRAM-MD5 digest */
314
315 string_format(big_buffer, big_buffer_size - 36, "%s", name);
316 for (p = big_buffer; *p; ) p++;
317 *p++ = ' ';
318
319 for (i = 0; i < 16; i++)
320   p += sprintf(CS p, "%02x", digest[i]);
321
322 /* Send the response, in base 64, and check the result. The response is
323 in big_buffer, but b64encode() returns its result in working store,
324 so calling smtp_write_command(), which uses big_buffer, is OK. */
325
326 buffer[0] = 0;
327 if (smtp_write_command(outblock, SCMD_FLUSH, "%s\r\n", b64encode(big_buffer,
328   p - big_buffer)) < 0) return FAIL_SEND;
329
330 return smtp_read_response(inblock, US buffer, buffsize, '2', timeout)
331   ? OK : FAIL;
332 }
333 #endif  /* STAND_ALONE */
334
335
336 /*************************************************
337 **************************************************
338 *             Stand-alone test program           *
339 **************************************************
340 *************************************************/
341
342 #ifdef STAND_ALONE
343
344 int main(int argc, char **argv)
345 {
346 int i;
347 uschar *secret = US argv[1];
348 uschar *challenge = US argv[2];
349 uschar digest[16];
350
351 compute_cram_md5(secret, challenge, digest);
352
353 for (i = 0; i < 16; i++) printf("%02x", digest[i]);
354 printf("\n");
355
356 return 0;
357 }
358
359 #endif
360
361 #endif  /*!MACRO_PREDEF*/
362 /* End of cram_md5.c */