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