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