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