/* SPDX-License-Identifier: MIT OR GPL-3.0-only */ /* sasl.c ** strophe XMPP client library -- SASL authentication helpers ** ** Copyright (C) 2005-2009 Collecta, Inc. ** ** This software is provided AS-IS with no warranty, either express ** or implied. ** ** This program is dual licensed under the MIT or GPLv3 licenses. */ /** @file * SASL authentication. */ #include #include #include "strophe.h" #include "common.h" #include "ostypes.h" #include "sasl.h" #include "md5.h" #include "scram.h" #include "util.h" /* strtok_s() has appeared in visual studio 2005. Use own implementation for older versions. */ #ifdef _MSC_VER #if (_MSC_VER >= 1400) #define strtok_r strtok_s #else #define strtok_r xmpp_strtok_r #endif #endif /* _MSC_VER */ /** generate authentication string for the SASL PLAIN mechanism */ char *sasl_plain(xmpp_ctx_t *ctx, const char *authid, const char *password) { size_t idlen, passlen; size_t msglen; char *result = NULL; char *msg; /* our message is Base64(authzid,\0,authid,\0,password) if there is no authzid, that field is left empty */ idlen = strlen(authid); passlen = strlen(password); msglen = 2 + idlen + passlen; msg = strophe_alloc(ctx, msglen); if (msg != NULL) { msg[0] = '\0'; memcpy(msg + 1, authid, idlen); msg[1 + idlen] = '\0'; memcpy(msg + 1 + idlen + 1, password, passlen); result = xmpp_base64_encode(ctx, (unsigned char *)msg, msglen); strophe_free(ctx, msg); } return result; } /** helpers for digest auth */ /* create a new, null-terminated string from a substring */ static char *_make_string(xmpp_ctx_t *ctx, const char *s, unsigned len) { char *result; result = strophe_alloc(ctx, len + 1); if (result != NULL) { memcpy(result, s, len); result[len] = '\0'; } return result; } /* create a new, null-terminated string quoting another string */ static char *_make_quoted(xmpp_ctx_t *ctx, const char *s) { char *result; size_t len = strlen(s); result = strophe_alloc(ctx, len + 3); if (result != NULL) { result[0] = '"'; memcpy(result + 1, s, len); result[len + 1] = '"'; result[len + 2] = '\0'; } return result; } /* split key, value pairs into a hash */ static hash_t *_parse_digest_challenge(xmpp_ctx_t *ctx, const char *msg) { hash_t *result; unsigned char *text; char *key, *value; unsigned char *s, *t; text = (unsigned char *)xmpp_base64_decode_str(ctx, msg, strlen(msg)); if (text == NULL) { strophe_error(ctx, "SASL", "couldn't Base64 decode challenge!"); return NULL; } result = hash_new(ctx, 10, strophe_free); if (result != NULL) { s = text; while (*s != '\0') { /* skip any leading commas and spaces */ while ((*s == ',') || (*s == ' ')) s++; /* accumulate a key ending at '=' */ t = s; while ((*t != '=') && (*t != '\0')) t++; if (*t == '\0') break; /* bad string */ key = _make_string(ctx, (char *)s, (t - s)); if (key == NULL) break; /* advance our start pointer past the key */ s = t + 1; t = s; /* if we see quotes, grab the string in between */ if ((*s == '\'') || (*s == '"')) { t++; while ((*t != *s) && (*t != '\0')) t++; value = _make_string(ctx, (char *)s + 1, (t - s - 1)); if (*t == *s) { s = t + 1; } else { s = t; } /* otherwise, accumulate a value ending in ',' or '\0' */ } else { while ((*t != ',') && (*t != '\0')) t++; value = _make_string(ctx, (char *)s, (t - s)); s = t; } if (value == NULL) { strophe_free(ctx, key); break; } /* TODO: check for collisions per spec */ hash_add(result, key, value); /* hash table now owns the value, free the key */ strophe_free(ctx, key); } } strophe_free(ctx, text); return result; } /** expand a 16 byte MD5 digest to a 32 byte hex representation */ static void _digest_to_hex(const char *digest, char *hex) { int i; const char hexdigit[] = "0123456789abcdef"; for (i = 0; i < 16; i++) { *hex++ = hexdigit[(digest[i] >> 4) & 0x0F]; *hex++ = hexdigit[digest[i] & 0x0F]; } } /** append 'key="value"' to a buffer, growing as necessary */ static char * _add_key(xmpp_ctx_t *ctx, hash_t *table, const char *key, char *buf, int quote) { int olen, nlen; int keylen, valuelen; const char *value, *qvalue; char *c; /* allocate a zero-length string if necessary */ if (buf == NULL) { buf = strophe_alloc(ctx, 1); buf[0] = '\0'; } if (buf == NULL) return NULL; /* get current string length */ olen = strlen(buf); value = hash_get(table, key); if (value == NULL) { strophe_error(ctx, "SASL", "couldn't retrieve value for '%s'", key); value = ""; } if (quote) { qvalue = _make_quoted(ctx, value); } else { qvalue = value; } /* added length is key + '=' + value */ /* (+ ',' if we're not the first entry */ keylen = strlen(key); valuelen = strlen(qvalue); nlen = (olen ? 1 : 0) + keylen + 1 + valuelen + 1; buf = strophe_realloc(ctx, buf, olen + nlen); if (buf != NULL) { c = buf + olen; if (olen) *c++ = ','; memcpy(c, key, keylen); c += keylen; *c++ = '='; memcpy(c, qvalue, valuelen); c += valuelen; *c++ = '\0'; } if (quote) strophe_free(ctx, (char *)qvalue); return buf; } /** generate auth response string for the SASL DIGEST-MD5 mechanism */ char *sasl_digest_md5(xmpp_ctx_t *ctx, const char *challenge, const char *jid, const char *password) { hash_t *table; char *result = NULL; char *node, *domain, *realm; char *value; char *response; struct MD5Context MD5; unsigned char digest[16], HA1[16], HA2[16]; char hex[32]; char cnonce[13]; /* our digest response is Hex( KD( HEX(MD5(A1)), nonce ':' nc ':' cnonce ':' qop ':' HEX(MD5(A2)) )) where KD(k, s) = MD5(k ':' s), A1 = MD5( node ':' realm ':' password ) ':' nonce ':' cnonce A2 = "AUTHENTICATE" ':' "xmpp/" domain If there is an authzid it is ':'-appended to A1 */ /* parse the challenge */ table = _parse_digest_challenge(ctx, challenge); if (table == NULL) { strophe_error(ctx, "SASL", "couldn't parse digest challenge"); return NULL; } node = xmpp_jid_node(ctx, jid); domain = xmpp_jid_domain(ctx, jid); /* generate default realm of domain if one didn't come from the server */ realm = hash_get(table, "realm"); if (realm == NULL || strlen(realm) == 0) { hash_add(table, "realm", strophe_strdup(ctx, domain)); realm = hash_get(table, "realm"); } /* add our response fields */ hash_add(table, "username", strophe_strdup(ctx, node)); xmpp_rand_nonce(ctx->rand, cnonce, sizeof(cnonce)); hash_add(table, "cnonce", strophe_strdup(ctx, cnonce)); hash_add(table, "nc", strophe_strdup(ctx, "00000001")); if (hash_get(table, "qop") == NULL) hash_add(table, "qop", strophe_strdup(ctx, "auth")); value = strophe_alloc(ctx, 5 + strlen(domain) + 1); memcpy(value, "xmpp/", 5); memcpy(value + 5, domain, strlen(domain)); value[5 + strlen(domain)] = '\0'; hash_add(table, "digest-uri", value); /* generate response */ /* construct MD5(node : realm : password) */ MD5Init(&MD5); MD5Update(&MD5, (unsigned char *)node, strlen(node)); MD5Update(&MD5, (unsigned char *)":", 1); MD5Update(&MD5, (unsigned char *)realm, strlen(realm)); MD5Update(&MD5, (unsigned char *)":", 1); MD5Update(&MD5, (unsigned char *)password, strlen(password)); MD5Final(digest, &MD5); /* digest now contains the first field of A1 */ MD5Init(&MD5); MD5Update(&MD5, digest, 16); MD5Update(&MD5, (unsigned char *)":", 1); value = hash_get(table, "nonce"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); MD5Update(&MD5, (unsigned char *)":", 1); value = hash_get(table, "cnonce"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); MD5Final(digest, &MD5); /* now digest is MD5(A1) */ memcpy(HA1, digest, 16); /* construct MD5(A2) */ MD5Init(&MD5); MD5Update(&MD5, (unsigned char *)"AUTHENTICATE:", 13); value = hash_get(table, "digest-uri"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); if (strcmp(hash_get(table, "qop"), "auth") != 0) { MD5Update(&MD5, (unsigned char *)":00000000000000000000000000000000", 33); } MD5Final(digest, &MD5); memcpy(HA2, digest, 16); /* construct response */ MD5Init(&MD5); _digest_to_hex((char *)HA1, hex); MD5Update(&MD5, (unsigned char *)hex, 32); MD5Update(&MD5, (unsigned char *)":", 1); value = hash_get(table, "nonce"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); MD5Update(&MD5, (unsigned char *)":", 1); value = hash_get(table, "nc"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); MD5Update(&MD5, (unsigned char *)":", 1); value = hash_get(table, "cnonce"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); MD5Update(&MD5, (unsigned char *)":", 1); value = hash_get(table, "qop"); MD5Update(&MD5, (unsigned char *)value, strlen(value)); MD5Update(&MD5, (unsigned char *)":", 1); _digest_to_hex((char *)HA2, hex); MD5Update(&MD5, (unsigned char *)hex, 32); MD5Final(digest, &MD5); response = strophe_alloc(ctx, 32 + 1); _digest_to_hex((char *)digest, hex); memcpy(response, hex, 32); response[32] = '\0'; hash_add(table, "response", response); /* construct reply */ result = NULL; result = _add_key(ctx, table, "username", result, 1); result = _add_key(ctx, table, "realm", result, 1); result = _add_key(ctx, table, "nonce", result, 1); result = _add_key(ctx, table, "cnonce", result, 1); result = _add_key(ctx, table, "nc", result, 0); result = _add_key(ctx, table, "qop", result, 0); result = _add_key(ctx, table, "digest-uri", result, 1); result = _add_key(ctx, table, "response", result, 0); result = _add_key(ctx, table, "charset", result, 0); strophe_free(ctx, node); strophe_free(ctx, domain); hash_release(table); /* also frees value strings */ /* reuse response for the base64 encode of our result */ response = xmpp_base64_encode(ctx, (unsigned char *)result, strlen(result)); strophe_free(ctx, result); return response; } /** generate auth response string for the SASL SCRAM mechanism */ char *sasl_scram(xmpp_ctx_t *ctx, const struct hash_alg *alg, const char *channel_binding, const char *challenge, const char *first_bare, const char *jid, const char *password) { uint8_t key[SCRAM_DIGEST_SIZE]; uint8_t sign[SCRAM_DIGEST_SIZE]; char *r = NULL; char *s = NULL; char *i = NULL; unsigned char *sval; size_t sval_len; long ival; char *tmp; char *ptr; char *saveptr = NULL; char *response; char *auth; char *response_b64; char *sign_b64; char *result = NULL; size_t response_len; size_t auth_len; int l; UNUSED(jid); tmp = strophe_strdup(ctx, challenge); if (!tmp) { return NULL; } ptr = strtok_r(tmp, ",", &saveptr); while (ptr) { if (strncmp(ptr, "r=", 2) == 0) { r = ptr; } else if (strncmp(ptr, "s=", 2) == 0) { s = ptr + 2; } else if (strncmp(ptr, "i=", 2) == 0) { i = ptr + 2; } ptr = strtok_r(NULL, ",", &saveptr); } if (!r || !s || !i) { goto out; } xmpp_base64_decode_bin(ctx, s, strlen(s), &sval, &sval_len); if (!sval) { goto out; } ival = strtol(i, &saveptr, 10); /* "c=," + r + ",p=" + sign_b64 + '\0' */ response_len = 3 + strlen(channel_binding) + strlen(r) + 3 + ((alg->digest_size + 2) / 3 * 4) + 1; response = strophe_alloc(ctx, response_len); if (!response) { goto out_sval; } auth_len = 3 + response_len + strlen(first_bare) + strlen(challenge); auth = strophe_alloc(ctx, auth_len); if (!auth) { goto out_response; } l = strophe_snprintf(response, response_len, "c=%s,%s", channel_binding, r); if (l < 0 || (size_t)l >= response_len) { goto out_auth; } l = strophe_snprintf(auth, auth_len, "%s,%s,%s", first_bare, challenge, response); if (l < 0 || (size_t)l >= auth_len) { goto out_auth; } SCRAM_ClientKey(alg, (uint8_t *)password, strlen(password), (uint8_t *)sval, sval_len, (uint32_t)ival, key); SCRAM_ClientSignature(alg, key, (uint8_t *)auth, strlen(auth), sign); SCRAM_ClientProof(alg, key, sign, sign); sign_b64 = xmpp_base64_encode(ctx, sign, alg->digest_size); if (!sign_b64) { goto out_auth; } /* Check for buffer overflow */ if (strlen(response) + strlen(sign_b64) + 3 + 1 > response_len) { strophe_free(ctx, sign_b64); goto out_auth; } strcat(response, ",p="); strcat(response, sign_b64); strophe_free(ctx, sign_b64); response_b64 = xmpp_base64_encode(ctx, (unsigned char *)response, strlen(response)); if (!response_b64) { goto out_auth; } result = response_b64; out_auth: strophe_free(ctx, auth); out_response: strophe_free(ctx, response); out_sval: strophe_free(ctx, sval); out: strophe_free(ctx, tmp); return result; }