Line data Source code
1 : /* sasl.c
2 : ** strophe XMPP client library -- SASL authentication helpers
3 : **
4 : ** Copyright (C) 2005-2009 Collecta, Inc.
5 : **
6 : ** This software is provided AS-IS with no warranty, either express
7 : ** or implied.
8 : **
9 : ** This program is dual licensed under the MIT and GPLv3 licenses.
10 : */
11 :
12 : /** @file
13 : * SASL authentication.
14 : */
15 :
16 : #include <stdlib.h>
17 : #include <string.h>
18 :
19 : #include "strophe.h"
20 : #include "common.h"
21 : #include "ostypes.h"
22 : #include "sasl.h"
23 : #include "md5.h"
24 : #include "scram.h"
25 : #include "util.h"
26 :
27 : /* strtok_s() has appeared in visual studio 2005.
28 : Use own implementation for older versions. */
29 : #ifdef _MSC_VER
30 : #if (_MSC_VER >= 1400)
31 : #define strtok_r strtok_s
32 : #else
33 : #define strtok_r xmpp_strtok_r
34 : #endif
35 : #endif /* _MSC_VER */
36 :
37 : /** generate authentication string for the SASL PLAIN mechanism */
38 0 : char *sasl_plain(xmpp_ctx_t *ctx, const char *authid, const char *password)
39 : {
40 0 : size_t idlen, passlen;
41 0 : size_t msglen;
42 0 : char *result = NULL;
43 0 : char *msg;
44 :
45 : /* our message is Base64(authzid,\0,authid,\0,password)
46 : if there is no authzid, that field is left empty */
47 :
48 0 : idlen = strlen(authid);
49 0 : passlen = strlen(password);
50 0 : msglen = 2 + idlen + passlen;
51 0 : msg = strophe_alloc(ctx, msglen);
52 0 : if (msg != NULL) {
53 0 : msg[0] = '\0';
54 0 : memcpy(msg + 1, authid, idlen);
55 0 : msg[1 + idlen] = '\0';
56 0 : memcpy(msg + 1 + idlen + 1, password, passlen);
57 0 : result = xmpp_base64_encode(ctx, (unsigned char *)msg, msglen);
58 0 : strophe_free(ctx, msg);
59 : }
60 :
61 0 : return result;
62 : }
63 :
64 : /** helpers for digest auth */
65 :
66 : /* create a new, null-terminated string from a substring */
67 0 : static char *_make_string(xmpp_ctx_t *ctx, const char *s, unsigned len)
68 : {
69 0 : char *result;
70 :
71 0 : result = strophe_alloc(ctx, len + 1);
72 0 : if (result != NULL) {
73 0 : memcpy(result, s, len);
74 0 : result[len] = '\0';
75 : }
76 0 : return result;
77 : }
78 :
79 : /* create a new, null-terminated string quoting another string */
80 0 : static char *_make_quoted(xmpp_ctx_t *ctx, const char *s)
81 : {
82 0 : char *result;
83 0 : size_t len = strlen(s);
84 :
85 0 : result = strophe_alloc(ctx, len + 3);
86 0 : if (result != NULL) {
87 0 : result[0] = '"';
88 0 : memcpy(result + 1, s, len);
89 0 : result[len + 1] = '"';
90 0 : result[len + 2] = '\0';
91 : }
92 0 : return result;
93 : }
94 :
95 : /* split key, value pairs into a hash */
96 0 : static hash_t *_parse_digest_challenge(xmpp_ctx_t *ctx, const char *msg)
97 : {
98 0 : hash_t *result;
99 0 : unsigned char *text;
100 0 : char *key, *value;
101 0 : unsigned char *s, *t;
102 :
103 0 : text = (unsigned char *)xmpp_base64_decode_str(ctx, msg, strlen(msg));
104 0 : if (text == NULL) {
105 0 : strophe_error(ctx, "SASL", "couldn't Base64 decode challenge!");
106 0 : return NULL;
107 : }
108 :
109 0 : result = hash_new(ctx, 10, strophe_free);
110 0 : if (result != NULL) {
111 : s = text;
112 0 : while (*s != '\0') {
113 : /* skip any leading commas and spaces */
114 0 : while ((*s == ',') || (*s == ' '))
115 0 : s++;
116 : /* accumulate a key ending at '=' */
117 : t = s;
118 0 : while ((*t != '=') && (*t != '\0'))
119 0 : t++;
120 0 : if (*t == '\0')
121 : break; /* bad string */
122 0 : key = _make_string(ctx, (char *)s, (t - s));
123 0 : if (key == NULL)
124 : break;
125 : /* advance our start pointer past the key */
126 0 : s = t + 1;
127 0 : t = s;
128 : /* if we see quotes, grab the string in between */
129 0 : if ((*s == '\'') || (*s == '"')) {
130 0 : t++;
131 0 : while ((*t != *s) && (*t != '\0'))
132 0 : t++;
133 0 : value = _make_string(ctx, (char *)s + 1, (t - s - 1));
134 0 : if (*t == *s) {
135 0 : s = t + 1;
136 : } else {
137 : s = t;
138 : }
139 : /* otherwise, accumulate a value ending in ',' or '\0' */
140 : } else {
141 0 : while ((*t != ',') && (*t != '\0'))
142 0 : t++;
143 0 : value = _make_string(ctx, (char *)s, (t - s));
144 0 : s = t;
145 : }
146 0 : if (value == NULL) {
147 0 : strophe_free(ctx, key);
148 0 : break;
149 : }
150 : /* TODO: check for collisions per spec */
151 0 : hash_add(result, key, value);
152 : /* hash table now owns the value, free the key */
153 0 : strophe_free(ctx, key);
154 : }
155 : }
156 0 : strophe_free(ctx, text);
157 :
158 0 : return result;
159 : }
160 :
161 : /** expand a 16 byte MD5 digest to a 32 byte hex representation */
162 0 : static void _digest_to_hex(const char *digest, char *hex)
163 : {
164 0 : int i;
165 0 : const char hexdigit[] = "0123456789abcdef";
166 :
167 0 : for (i = 0; i < 16; i++) {
168 0 : *hex++ = hexdigit[(digest[i] >> 4) & 0x0F];
169 0 : *hex++ = hexdigit[digest[i] & 0x0F];
170 : }
171 0 : }
172 :
173 : /** append 'key="value"' to a buffer, growing as necessary */
174 : static char *
175 0 : _add_key(xmpp_ctx_t *ctx, hash_t *table, const char *key, char *buf, int quote)
176 : {
177 0 : int olen, nlen;
178 0 : int keylen, valuelen;
179 0 : const char *value, *qvalue;
180 0 : char *c;
181 :
182 : /* allocate a zero-length string if necessary */
183 0 : if (buf == NULL) {
184 0 : buf = strophe_alloc(ctx, 1);
185 0 : buf[0] = '\0';
186 : }
187 0 : if (buf == NULL)
188 : return NULL;
189 :
190 : /* get current string length */
191 0 : olen = strlen(buf);
192 0 : value = hash_get(table, key);
193 0 : if (value == NULL) {
194 0 : strophe_error(ctx, "SASL", "couldn't retrieve value for '%s'", key);
195 0 : value = "";
196 : }
197 0 : if (quote) {
198 0 : qvalue = _make_quoted(ctx, value);
199 : } else {
200 : qvalue = value;
201 : }
202 : /* added length is key + '=' + value */
203 : /* (+ ',' if we're not the first entry */
204 0 : keylen = strlen(key);
205 0 : valuelen = strlen(qvalue);
206 0 : nlen = (olen ? 1 : 0) + keylen + 1 + valuelen + 1;
207 0 : buf = strophe_realloc(ctx, buf, olen + nlen);
208 :
209 0 : if (buf != NULL) {
210 0 : c = buf + olen;
211 0 : if (olen)
212 0 : *c++ = ',';
213 0 : memcpy(c, key, keylen);
214 0 : c += keylen;
215 0 : *c++ = '=';
216 0 : memcpy(c, qvalue, valuelen);
217 0 : c += valuelen;
218 0 : *c++ = '\0';
219 : }
220 :
221 0 : if (quote)
222 0 : strophe_free(ctx, (char *)qvalue);
223 :
224 0 : return buf;
225 : }
226 :
227 : /** generate auth response string for the SASL DIGEST-MD5 mechanism */
228 0 : char *sasl_digest_md5(xmpp_ctx_t *ctx,
229 : const char *challenge,
230 : const char *jid,
231 : const char *password)
232 : {
233 0 : hash_t *table;
234 0 : char *result = NULL;
235 0 : char *node, *domain, *realm;
236 0 : char *value;
237 0 : char *response;
238 0 : struct MD5Context MD5;
239 0 : unsigned char digest[16], HA1[16], HA2[16];
240 0 : char hex[32];
241 0 : char cnonce[13];
242 :
243 : /* our digest response is
244 : Hex( KD( HEX(MD5(A1)),
245 : nonce ':' nc ':' cnonce ':' qop ':' HEX(MD5(A2))
246 : ))
247 :
248 : where KD(k, s) = MD5(k ':' s),
249 : A1 = MD5( node ':' realm ':' password ) ':' nonce ':' cnonce
250 : A2 = "AUTHENTICATE" ':' "xmpp/" domain
251 :
252 : If there is an authzid it is ':'-appended to A1 */
253 :
254 : /* parse the challenge */
255 0 : table = _parse_digest_challenge(ctx, challenge);
256 0 : if (table == NULL) {
257 0 : strophe_error(ctx, "SASL", "couldn't parse digest challenge");
258 0 : return NULL;
259 : }
260 :
261 0 : node = xmpp_jid_node(ctx, jid);
262 0 : domain = xmpp_jid_domain(ctx, jid);
263 :
264 : /* generate default realm of domain if one didn't come from the
265 : server */
266 0 : realm = hash_get(table, "realm");
267 0 : if (realm == NULL || strlen(realm) == 0) {
268 0 : hash_add(table, "realm", strophe_strdup(ctx, domain));
269 0 : realm = hash_get(table, "realm");
270 : }
271 :
272 : /* add our response fields */
273 0 : hash_add(table, "username", strophe_strdup(ctx, node));
274 0 : xmpp_rand_nonce(ctx->rand, cnonce, sizeof(cnonce));
275 0 : hash_add(table, "cnonce", strophe_strdup(ctx, cnonce));
276 0 : hash_add(table, "nc", strophe_strdup(ctx, "00000001"));
277 0 : if (hash_get(table, "qop") == NULL)
278 0 : hash_add(table, "qop", strophe_strdup(ctx, "auth"));
279 0 : value = strophe_alloc(ctx, 5 + strlen(domain) + 1);
280 0 : memcpy(value, "xmpp/", 5);
281 0 : memcpy(value + 5, domain, strlen(domain));
282 0 : value[5 + strlen(domain)] = '\0';
283 0 : hash_add(table, "digest-uri", value);
284 :
285 : /* generate response */
286 :
287 : /* construct MD5(node : realm : password) */
288 0 : MD5Init(&MD5);
289 0 : MD5Update(&MD5, (unsigned char *)node, strlen(node));
290 0 : MD5Update(&MD5, (unsigned char *)":", 1);
291 0 : MD5Update(&MD5, (unsigned char *)realm, strlen(realm));
292 0 : MD5Update(&MD5, (unsigned char *)":", 1);
293 0 : MD5Update(&MD5, (unsigned char *)password, strlen(password));
294 0 : MD5Final(digest, &MD5);
295 :
296 : /* digest now contains the first field of A1 */
297 :
298 0 : MD5Init(&MD5);
299 0 : MD5Update(&MD5, digest, 16);
300 0 : MD5Update(&MD5, (unsigned char *)":", 1);
301 0 : value = hash_get(table, "nonce");
302 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
303 0 : MD5Update(&MD5, (unsigned char *)":", 1);
304 0 : value = hash_get(table, "cnonce");
305 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
306 0 : MD5Final(digest, &MD5);
307 :
308 : /* now digest is MD5(A1) */
309 0 : memcpy(HA1, digest, 16);
310 :
311 : /* construct MD5(A2) */
312 0 : MD5Init(&MD5);
313 0 : MD5Update(&MD5, (unsigned char *)"AUTHENTICATE:", 13);
314 0 : value = hash_get(table, "digest-uri");
315 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
316 0 : if (strcmp(hash_get(table, "qop"), "auth") != 0) {
317 0 : MD5Update(&MD5, (unsigned char *)":00000000000000000000000000000000",
318 : 33);
319 : }
320 0 : MD5Final(digest, &MD5);
321 :
322 0 : memcpy(HA2, digest, 16);
323 :
324 : /* construct response */
325 0 : MD5Init(&MD5);
326 0 : _digest_to_hex((char *)HA1, hex);
327 0 : MD5Update(&MD5, (unsigned char *)hex, 32);
328 0 : MD5Update(&MD5, (unsigned char *)":", 1);
329 0 : value = hash_get(table, "nonce");
330 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
331 0 : MD5Update(&MD5, (unsigned char *)":", 1);
332 0 : value = hash_get(table, "nc");
333 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
334 0 : MD5Update(&MD5, (unsigned char *)":", 1);
335 0 : value = hash_get(table, "cnonce");
336 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
337 0 : MD5Update(&MD5, (unsigned char *)":", 1);
338 0 : value = hash_get(table, "qop");
339 0 : MD5Update(&MD5, (unsigned char *)value, strlen(value));
340 0 : MD5Update(&MD5, (unsigned char *)":", 1);
341 0 : _digest_to_hex((char *)HA2, hex);
342 0 : MD5Update(&MD5, (unsigned char *)hex, 32);
343 0 : MD5Final(digest, &MD5);
344 :
345 0 : response = strophe_alloc(ctx, 32 + 1);
346 0 : _digest_to_hex((char *)digest, hex);
347 0 : memcpy(response, hex, 32);
348 0 : response[32] = '\0';
349 0 : hash_add(table, "response", response);
350 :
351 : /* construct reply */
352 0 : result = NULL;
353 0 : result = _add_key(ctx, table, "username", result, 1);
354 0 : result = _add_key(ctx, table, "realm", result, 1);
355 0 : result = _add_key(ctx, table, "nonce", result, 1);
356 0 : result = _add_key(ctx, table, "cnonce", result, 1);
357 0 : result = _add_key(ctx, table, "nc", result, 0);
358 0 : result = _add_key(ctx, table, "qop", result, 0);
359 0 : result = _add_key(ctx, table, "digest-uri", result, 1);
360 0 : result = _add_key(ctx, table, "response", result, 0);
361 0 : result = _add_key(ctx, table, "charset", result, 0);
362 :
363 0 : strophe_free(ctx, node);
364 0 : strophe_free(ctx, domain);
365 0 : hash_release(table); /* also frees value strings */
366 :
367 : /* reuse response for the base64 encode of our result */
368 0 : response = xmpp_base64_encode(ctx, (unsigned char *)result, strlen(result));
369 0 : strophe_free(ctx, result);
370 :
371 : return response;
372 : }
373 :
374 : /** generate auth response string for the SASL SCRAM mechanism */
375 0 : char *sasl_scram(xmpp_ctx_t *ctx,
376 : const struct hash_alg *alg,
377 : const char *challenge,
378 : const char *first_bare,
379 : const char *jid,
380 : const char *password)
381 : {
382 0 : uint8_t key[SCRAM_DIGEST_SIZE];
383 0 : uint8_t sign[SCRAM_DIGEST_SIZE];
384 0 : char *r = NULL;
385 0 : char *s = NULL;
386 0 : char *i = NULL;
387 0 : unsigned char *sval;
388 0 : size_t sval_len;
389 0 : long ival;
390 0 : char *tmp;
391 0 : char *ptr;
392 0 : char *saveptr = NULL;
393 0 : char *response;
394 0 : char *auth;
395 0 : char *response_b64;
396 0 : char *sign_b64;
397 0 : char *result = NULL;
398 0 : size_t response_len;
399 0 : size_t auth_len;
400 :
401 0 : UNUSED(jid);
402 :
403 0 : tmp = strophe_strdup(ctx, challenge);
404 0 : if (!tmp) {
405 0 : return NULL;
406 : }
407 :
408 0 : ptr = strtok_r(tmp, ",", &saveptr);
409 0 : while (ptr) {
410 0 : if (strncmp(ptr, "r=", 2) == 0) {
411 : r = ptr;
412 0 : } else if (strncmp(ptr, "s=", 2) == 0) {
413 0 : s = ptr + 2;
414 0 : } else if (strncmp(ptr, "i=", 2) == 0) {
415 0 : i = ptr + 2;
416 : }
417 0 : ptr = strtok_r(NULL, ",", &saveptr);
418 : }
419 :
420 0 : if (!r || !s || !i) {
421 0 : goto out;
422 : }
423 :
424 0 : xmpp_base64_decode_bin(ctx, s, strlen(s), &sval, &sval_len);
425 0 : if (!sval) {
426 0 : goto out;
427 : }
428 0 : ival = strtol(i, &saveptr, 10);
429 :
430 0 : auth_len = 10 + strlen(r) + strlen(first_bare) + strlen(challenge);
431 0 : auth = strophe_alloc(ctx, auth_len);
432 0 : if (!auth) {
433 0 : goto out_sval;
434 : }
435 :
436 : /* "c=biws," + r + ",p=" + sign_b64 + '\0' */
437 0 : response_len = 7 + strlen(r) + 3 + ((alg->digest_size + 2) / 3 * 4) + 1;
438 0 : response = strophe_alloc(ctx, response_len);
439 0 : if (!response) {
440 0 : goto out_auth;
441 : }
442 :
443 0 : strophe_snprintf(response, response_len, "c=biws,%s", r);
444 0 : strophe_snprintf(auth, auth_len, "%s,%s,%s", first_bare + 3, challenge,
445 : response);
446 :
447 0 : SCRAM_ClientKey(alg, (uint8_t *)password, strlen(password), (uint8_t *)sval,
448 : sval_len, (uint32_t)ival, key);
449 0 : SCRAM_ClientSignature(alg, key, (uint8_t *)auth, strlen(auth), sign);
450 0 : SCRAM_ClientProof(alg, sign, key, sign);
451 :
452 0 : sign_b64 = xmpp_base64_encode(ctx, sign, alg->digest_size);
453 0 : if (!sign_b64) {
454 0 : goto out_response;
455 : }
456 :
457 : /* Check for buffer overflow */
458 0 : if (strlen(response) + strlen(sign_b64) + 3 + 1 > response_len) {
459 0 : strophe_free(ctx, sign_b64);
460 0 : goto out_response;
461 : }
462 0 : strcat(response, ",p=");
463 0 : strcat(response, sign_b64);
464 0 : strophe_free(ctx, sign_b64);
465 :
466 0 : response_b64 =
467 0 : xmpp_base64_encode(ctx, (unsigned char *)response, strlen(response));
468 0 : if (!response_b64) {
469 0 : goto out_response;
470 : }
471 : result = response_b64;
472 :
473 0 : out_response:
474 0 : strophe_free(ctx, response);
475 0 : out_auth:
476 0 : strophe_free(ctx, auth);
477 0 : out_sval:
478 0 : strophe_free(ctx, sval);
479 0 : out:
480 0 : strophe_free(ctx, tmp);
481 : return result;
482 : }
|