diff options
author | Marius Halden <marius.h@lden.org> | 2016-06-13 01:35:48 +0200 |
---|---|---|
committer | Marius Halden <marius.h@lden.org> | 2016-06-26 14:28:58 +0200 |
commit | a110ae7c40ddd8ad099f94d7197848cf26f4511a (patch) | |
tree | 1a4ba4cb0c13be1f9fa3e56428bf3622bbd9db66 | |
parent | 7949d5a9e21f48f4a41522d3c0377d0ea29b3eac (diff) |
Add initial support for SCRAM-SHA-1
-rw-r--r-- | protocols/jabber/jabber.c | 2 | ||||
-rw-r--r-- | protocols/jabber/jabber.h | 12 | ||||
-rw-r--r-- | protocols/jabber/sasl.c | 230 |
3 files changed, 231 insertions, 13 deletions
diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c index 11a90ff4..2fa19c0f 100644 --- a/protocols/jabber/jabber.c +++ b/protocols/jabber/jabber.c @@ -377,6 +377,8 @@ static void jabber_logout(struct im_connection *ic) g_free(jd->muc_host); g_free(jd->username); g_free(jd->me); + g_free(jd->challenge.cnonce); + g_free(jd->challenge.server_signature); g_free(jd); jabber_connections = g_slist_remove(jabber_connections, ic); diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h index d76ee08f..5412a08f 100644 --- a/protocols/jabber/jabber.h +++ b/protocols/jabber/jabber.h @@ -77,6 +77,11 @@ typedef enum { JCFLAG_ALWAYS_USE_NICKS = 2, } jabber_chat_flags_t; +typedef enum { + JCHALLENGE_DIGEST_MD5, + JCHALLENGE_SCRAM +} jabber_challenge_t; + struct jabber_data { struct im_connection *ic; @@ -94,6 +99,13 @@ struct jabber_data { char *me; /* bare jid */ char *internal_jid; + struct { + jabber_challenge_t type; + int scram_algo; + char *cnonce; + char *server_signature; + } challenge; + const struct oauth2_service *oauth2_service; char *oauth2_access_token; diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index 7778af1f..de91034b 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -22,11 +22,13 @@ \***************************************************************************/ #include <ctype.h> +#include <gcrypt.h> #include "jabber.h" #include "base64.h" #include "oauth2.h" #include "oauth.h" +#include "sha1.h" const struct oauth2_service oauth2_service_google = { @@ -44,7 +46,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; + int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0; int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE; GString *mechs; @@ -71,24 +73,26 @@ 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; - } - 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, "SCRAM-SHA-1") == 0) { + sup_scram = 1; + } + 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) { imcb_error(ic, "This server requires OAuth " "(supported schemes:%s)", mechs->str); @@ -139,11 +143,38 @@ 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) { + GString *gs; + int len; + unsigned char cnonce_bin[30]; + char *cnonce; + + jd->challenge.type = JCHALLENGE_SCRAM; + jd->challenge.scram_algo = GCRY_MD_SHA1; + xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); + + gs = g_string_sized_new(128); + + random_bytes(cnonce_bin, sizeof(cnonce_bin)); + cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin)); + + jd->challenge.cnonce = cnonce; + + /* XXX: Saslprep username */ + g_string_append_printf(gs, "n,,n=%s,r=%s", jd->username, cnonce); + + len = gs->len; + s = g_string_free(gs, FALSE); + + reply->text = base64_encode((unsigned char *)s , len); + reply->text_len = strlen(reply->text); + 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/>. */ @@ -266,7 +297,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; @@ -385,6 +416,175 @@ 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, + *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL; + unsigned char *salt = NULL; + size_t salt_len, iter_count; + + int algo = jd->challenge.scram_algo; + 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; + } + + // XXX: Normalize password with saslprep + + salt_len = base64_decode(salt64, &salt); + + if (gcry_kdf_derive(ic->acc->pass, strlen(ic->acc->pass), + GCRY_KDF_PBKDF2, algo, + salt, salt_len, iter_count, + sizeof(salted_password), salted_password) != 0) { + imcb_error(ic, "gcrypt went booboo :("); + 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)); + + client_first_bare = g_strdup_printf("n=%s,r=%s", jd->username, jd->challenge.cnonce); + client_final_noproof = g_strdup_printf("c=biws,r=%s", 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 (size_t 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(random); + g_free(salt64); + g_free(salt); + g_free(iter_str); + g_free(encoded_proof); + 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; + 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; @@ -402,6 +602,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); } |