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