diff options
Diffstat (limited to 'protocols/jabber/sasl.c')
-rw-r--r-- | protocols/jabber/sasl.c | 263 |
1 files changed, 248 insertions, 15 deletions
diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index 77a2cee0..d4556811 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -22,11 +22,15 @@ \***************************************************************************/ #include <ctype.h> +#include <gcrypt.h> +#include <gnutls/gnutls.h> +#include <stringprep.h> #include "jabber.h" #include "base64.h" #include "oauth2.h" #include "oauth.h" +#include "sha1.h" const struct oauth2_service oauth2_service_google = { @@ -47,7 +51,7 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) struct jabber_data *jd = ic->proto_data; struct xt_node *c, *reply; char *s; - int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_hipchat_oauth = 0; + int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0, sup_hipchat_oauth = 0; int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE; GString *mechs; @@ -74,26 +78,30 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) mechs = g_string_new(""); c = node->children; while ((c = xt_find_node(c, "mechanism"))) { - if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) { - sup_plain = 1; - } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) { - sup_digest = 1; - } else if (c->text && g_strcasecmp(c->text, "ANONYMOUS") == 0) { - sup_anonymous = 1; - } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) { - sup_gtalk = 1; - } else if (c->text && g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) { - sup_hipchat_oauth = 1; - } - if (c->text) { + if (g_strcasecmp(c->text, "PLAIN") == 0) { + sup_plain = 1; + } else if (g_strcasecmp(c->text, "DIGEST-MD5") == 0) { + sup_digest = 1; + } else if (g_strcasecmp(c->text, "ANONYMOUS") == 0) { + sup_anonymous = 1; + } else if (g_strcasecmp(c->text, "X-OAUTH2") == 0) { + sup_gtalk = 1; + } else if (g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) { + sup_hipchat_oauth = 1; + } else if (g_strcasecmp(c->text, "SCRAM-SHA-1") == 0) { + sup_scram |= JSCRAM_SHA1; + } else if (g_strcasecmp(c->text, "SCRAM-SHA-256") == 0) { + sup_scram |= JSCRAM_SHA256; + } + g_string_append_printf(mechs, " %s", c->text); } c = c->next; } - if (!want_oauth && !sup_plain && !sup_digest) { + if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) { if (sup_gtalk || sup_hipchat_oauth) { imcb_error(ic, "This server requires OAuth " "(supported schemes:%s)", mechs->str); @@ -158,11 +166,47 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) imc_logout(ic, FALSE); xt_free_node(reply); return XT_ABORT; + } else if (sup_scram && !set_getbool(&ic->acc->set, "disable_scram")) { + int rc; + unsigned char cnonce_bin[30]; + char *puser = NULL; + + if (sup_scram & JSCRAM_SHA256) { + jd->challenge.type = JCHALLENGE_SCRAM; + jd->challenge.scram_algo = GCRY_MD_SHA256; + xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); + } else if (sup_scram & JSCRAM_SHA1) { + jd->challenge.type = JCHALLENGE_SCRAM; + jd->challenge.scram_algo = GCRY_MD_SHA1; + xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); + } else { + imcb_error(ic, "Unknown scram method"); /* Just in case, but we should not get here */ + return XT_ABORT; + } + + rc = stringprep_profile(jd->username, &puser, "SASLprep", 0); + if (rc != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); + return XT_ABORT; + } + + random_bytes(cnonce_bin, sizeof(cnonce_bin)); + jd->challenge.cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin)); + + jd->challenge.cb_header = g_strdup("n,,"); + + s = g_strdup_printf("%sn=%s,r=%s", jd->challenge.cb_header, puser, jd->challenge.cnonce); + + reply->text = base64_encode((unsigned char *)s , strlen(s)); + reply->text_len = strlen(reply->text); + g_free(puser); + g_free(s); } else if (sup_digest && !(jd->ssl && sup_plain)) { /* Only try DIGEST-MD5 if there's no SSL/TLS or if PLAIN isn't supported. * Which in practice means "don't bother with DIGEST-MD5 most of the time". * It's weak, pointless over TLS, and often breaks with some servers (hi openfire) */ + jd->challenge.type = JCHALLENGE_DIGEST_MD5; xt_add_attr(reply, "mechanism", "DIGEST-MD5"); /* The rest will be done later, when we receive a <challenge/>. */ @@ -281,7 +325,7 @@ char *sasl_get_part(char *data, char *field) } } -xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data) +static xt_status sasl_pkt_challenge_digest_md5(struct xt_node *node, gpointer data) { struct im_connection *ic = data; struct jabber_data *jd = ic->proto_data; @@ -400,6 +444,191 @@ silent_error: return ret; } +static void hmac(int algo, unsigned char *key, size_t key_len, unsigned char *buf, size_t buf_len, unsigned char *out) +{ + gcry_md_hd_t digest; + unsigned char *tmp; + + gcry_md_open(&digest, algo, GCRY_MD_FLAG_HMAC); + gcry_md_setkey(digest, key, key_len); + gcry_md_write(digest, buf, buf_len); + + tmp = gcry_md_read(digest, 0); + memcpy(out, tmp, gcry_md_get_algo_dlen(algo)); + + gcry_md_close(digest); +} + +static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) +{ + struct im_connection *ic = data; + struct jabber_data *jd = ic->proto_data; + struct xt_node *reply_pkt = NULL; + xt_status ret = XT_ABORT; + char *random = NULL, *salt64 = NULL, *iter_str = NULL, *encoded_proof = NULL, + *reply = NULL, *client_first_bare = NULL, *server_first = NULL, *cb_header64 = NULL, + *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL, *puser = NULL, *ppass = NULL; + + unsigned char *salt = NULL; + size_t salt_len, iter_count, i; + + int algo = jd->challenge.scram_algo, rc; + size_t md_len = gcry_md_get_algo_dlen(algo); + + unsigned char client_key[md_len]; + unsigned char client_proof[md_len]; + unsigned char client_signature[md_len]; + unsigned char salted_password[md_len]; + unsigned char server_key[md_len]; + unsigned char server_signature[md_len]; + unsigned char stored_key[md_len]; + + if (node->text_len == 0) { + goto error; + } + + server_first = frombase64(node->text); + + random = sasl_get_part(server_first, "r"); + salt64 = sasl_get_part(server_first, "s"); + iter_str = sasl_get_part(server_first, "i"); + iter_count = atoi(iter_str); + + if (strncmp(random, jd->challenge.cnonce, strlen(jd->challenge.cnonce)) != 0) { + imcb_error(ic, "Server nonce doesn't start with client nonce"); + goto error; + } + + rc = stringprep_profile(jd->username, &puser, "SASLprep", 0); + if (rc != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); + goto error; + } + rc = stringprep_profile(ic->acc->pass, &ppass, "SASLprep", 0); + if (rc != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); + goto error; + } + + salt_len = base64_decode(salt64, &salt); + + if (gcry_kdf_derive(ppass, strlen(ppass), + GCRY_KDF_PBKDF2, algo, + salt, salt_len, iter_count, + sizeof(salted_password), salted_password) != 0) { + imcb_error(ic, "PBKDF failed"); + goto error; + } + + hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Client Key", 10, client_key); + + gcry_md_hash_buffer(algo, stored_key, client_key, sizeof(client_key)); + + cb_header64 = tobase64(jd->challenge.cb_header); + client_first_bare = g_strdup_printf("n=%s,r=%s", puser, jd->challenge.cnonce); + client_final_noproof = g_strdup_printf("c=%s,r=%s", cb_header64, random); + + auth_message = g_strdup_printf("%s,%s,%s", client_first_bare, server_first, client_final_noproof); + + hmac(algo, stored_key, sizeof(stored_key), (unsigned char *)auth_message, strlen(auth_message), client_signature); + + for (i = 0; i < sizeof(client_key); i++) { + client_proof[i] = client_key[i] ^ client_signature[i]; + } + + encoded_proof = base64_encode(client_proof, sizeof(client_proof)); + + hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Server Key", 10, server_key); + hmac(algo, server_key, sizeof(server_key), (unsigned char *)auth_message, strlen(auth_message), server_signature); + + jd->challenge.server_signature = base64_encode(server_signature, sizeof(server_signature)); + + client_final = g_strdup_printf("%s,p=%s", client_final_noproof, encoded_proof); + reply = tobase64(client_final); + reply_pkt = xt_new_node("response", reply, NULL); + xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL); + + if (!jabber_write_packet(ic, reply_pkt)) { + goto silent_error; + } + + ret = XT_HANDLED; + goto silent_error; + +error: + imcb_error(ic, "Incorrect SASL challenge received"); + imc_logout(ic, FALSE); + +silent_error: + g_free(puser); + g_free(ppass); + g_free(random); + g_free(salt64); + g_free(salt); + g_free(iter_str); + g_free(encoded_proof); + g_free(cb_header64); + g_free(client_first_bare); + g_free(client_final_noproof); + g_free(client_final); + g_free(auth_message); + g_free(reply); + g_free(jd->challenge.cnonce); + jd->challenge.cnonce = NULL; + g_free(jd->challenge.cb_header); + jd->challenge.cb_header = NULL; + xt_free_node(reply_pkt); + return ret; +} + +xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data) +{ + struct im_connection *ic = data; + struct jabber_data *jd = ic->proto_data; + + switch (jd->challenge.type) { + case JCHALLENGE_DIGEST_MD5: + return sasl_pkt_challenge_digest_md5(node, data); + + case JCHALLENGE_SCRAM: + return sasl_pkt_challenge_scram(node, data); + + default: + imcb_error(ic, "Invalid challenge type"); + return XT_ABORT; + } +} + +static xt_status sasl_pkt_validate_scram_success(struct xt_node *node, gpointer data) +{ + struct im_connection *ic = data; + struct jabber_data *jd = ic->proto_data; + char *v, *dec; + xt_status ret = XT_HANDLED; + + if (node->text_len == 0) { + imcb_error(ic, "Server didn't send signature"); + imc_logout(ic, FALSE); + ret = XT_ABORT; + goto error; + } + + dec = frombase64(node->text); + v = sasl_get_part(dec, "v"); + + if (g_strcmp0(v, jd->challenge.server_signature) != 0) { + imcb_error(ic, "Invalid server signature"); + imc_logout(ic, FALSE); + ret = XT_ABORT; + goto error; + } + +error: + g_free(jd->challenge.server_signature); + jd->challenge.server_signature = NULL; + return ret; +} + xt_status sasl_pkt_result(struct xt_node *node, gpointer data) { struct im_connection *ic = data; @@ -417,6 +646,10 @@ xt_status sasl_pkt_result(struct xt_node *node, gpointer data) imcb_log(ic, "Authentication finished"); jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART; + if (jd->challenge.type == JCHALLENGE_SCRAM) { + return sasl_pkt_validate_scram_success(node, data); + } + if (jd->flags & JFLAG_HIPCHAT) { return hipchat_handle_success(ic, node); } |