Mercurial > pidgin
diff libpurple/protocols/jabber/auth_scram.c @ 28707:c1d41b7484ff
jabber: Complete (though untested) SCRAM implementation.
Client proof calculations function properly, but parsing is untested.
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Mon, 09 Nov 2009 03:42:26 +0000 |
parents | 2b4465db73f1 |
children | b0fb53868142 |
line wrap: on
line diff
--- a/libpurple/protocols/jabber/auth_scram.c Sun Nov 08 18:39:30 2009 +0000 +++ b/libpurple/protocols/jabber/auth_scram.c Mon Nov 09 03:42:26 2009 +0000 @@ -29,16 +29,33 @@ #include "debug.h" static const struct { + const char *mech_substr; + const char *hash; +} mech_hashes[] = { + { "-SHA-1-", "sha1" }, +}; + +static const struct { const char *hash; guint size; } hash_sizes[] = { { "sha1", 20 }, - { "sha224", 28 }, - { "sha256", 32 }, - { "sha384", 48 }, - { "sha512", 64 } }; +static const gchar *mech_to_hash(const char *mech) +{ + int i; + + g_return_val_if_fail(mech != NULL && *mech != '\0', NULL); + + for (i = 0; i < G_N_ELEMENTS(mech_hashes); ++i) { + if (strstr(mech, mech_hashes[i].mech_substr)) + return mech_hashes[i].hash; + } + + return NULL; +} + static guint hash_to_output_len(const gchar *hash) { int i; @@ -55,11 +72,11 @@ return 0; } -GString *jabber_auth_scram_hi(const gchar *hash, const GString *str, - GString *salt, guint iterations) +guchar *jabber_scram_hi(const gchar *hash, const GString *str, + GString *salt, guint iterations) { PurpleCipherContext *context; - GString *result; + guchar *result; guint i; guint hash_len; guchar *prev, *tmp; @@ -74,6 +91,7 @@ prev = g_new0(guint8, hash_len); tmp = g_new0(guint8, hash_len); + result = g_new0(guint8, hash_len); context = purple_cipher_context_new_by_name("hmac", NULL); @@ -81,15 +99,13 @@ * octet first. */ g_string_append_len(salt, "\0\0\0\1", 4); - result = g_string_sized_new(hash_len); - /* Compute U0 */ purple_cipher_context_set_option(context, "hash", (gpointer)hash); purple_cipher_context_set_key_with_len(context, (guchar *)str->str, str->len); purple_cipher_context_append(context, (guchar *)salt->str, salt->len); - purple_cipher_context_digest(context, hash_len, (guchar *)result->str, &(result->len)); + purple_cipher_context_digest(context, hash_len, result, NULL); - memcpy(prev, result->str, hash_len); + memcpy(prev, result, hash_len); /* Compute U1...Ui */ for (i = 1; i < iterations; ++i) { @@ -100,7 +116,7 @@ purple_cipher_context_digest(context, hash_len, tmp, NULL); for (j = 0; j < hash_len; ++j) - result->str[j] ^= tmp[j]; + result[j] ^= tmp[j]; memcpy(prev, tmp, hash_len); } @@ -110,3 +126,431 @@ g_free(prev); return result; } + +/* + * Helper functions for doing the SCRAM calculations. The first argument + * is the hash algorithm and the second (len) is the length of the output + * buffer and key/data (the fourth argument). + * "str" is a NULL-terminated string for hmac(). + * + * Needless to say, these are fragile. + */ +static void +hmac(const gchar *hash_alg, gsize len, guchar *out, const guchar *key, const gchar *str) +{ + PurpleCipherContext *context; + + context = purple_cipher_context_new_by_name("hmac", NULL); + purple_cipher_context_set_option(context, "hash", (gpointer)hash_alg); + purple_cipher_context_set_key_with_len(context, key, len); + purple_cipher_context_append(context, (guchar *)str, strlen(str)); + purple_cipher_context_digest(context, len, out, NULL); + purple_cipher_context_destroy(context); +} + +static void +hash(const gchar *hash_alg, gsize len, guchar *out, const guchar *data) +{ + PurpleCipherContext *context; + + context = purple_cipher_context_new_by_name(hash_alg, NULL); + purple_cipher_context_append(context, data, len); + purple_cipher_context_digest(context, len, out, NULL); + purple_cipher_context_destroy(context); +} + +gboolean +jabber_scram_calc_proofs(JabberScramData *data, const char *password, + GString *salt, guint iterations) +{ + guint hash_len = hash_to_output_len(data->hash); + guint i; + + GString *pass = g_string_new(password); + + guchar *salted_password; + guchar client_key[hash_len]; + guchar stored_key[hash_len]; + guchar client_signature[hash_len]; + guchar server_key[hash_len]; + + data->client_proof = g_string_sized_new(hash_len); + data->client_proof->len = hash_len; + data->server_signature = g_string_sized_new(hash_len); + data->server_signature->len = hash_len; + + salted_password = jabber_scram_hi(data->hash, pass, salt, iterations); + g_string_free(pass, TRUE); + if (!salted_password) + return FALSE; + + /* client_key = HMAC(salted_password, "Client Key") */ + hmac(data->hash, hash_len, client_key, salted_password, "Client Key"); + /* server_key = HMAC(salted_password, "Server Key") */ + hmac(data->hash, hash_len, server_key, salted_password, "Server Key"); + g_free(salted_password); + + /* stored_key = HASH(client_key) */ + hash(data->hash, hash_len, stored_key, client_key); + + /* client_signature = HMAC(stored_key, auth_message) */ + hmac(data->hash, hash_len, client_signature, stored_key, data->auth_message->str); + /* server_signature = HMAC(server_key, auth_message) */ + hmac(data->hash, hash_len, (guchar *)data->server_signature->str, server_key, data->auth_message->str); + + /* client_proof = client_key XOR client_signature */ + for (i = 0; i < hash_len; ++i) + data->client_proof->str[i] = client_key[i] ^ client_signature[i]; + + return TRUE; +} + +static gboolean +parse_challenge(JabberScramData *data, const char *challenge, + gchar **out_nonce, GString **out_salt, guint *out_iterations) +{ + gsize cnonce_len; + const char *cur; + const char *end; + const char *val_start, *val_end; + char *tmp, *decoded; + gsize decoded_len; + char *nonce; + GString *salt; + guint iterations; + + cur = challenge; + end = challenge + strlen(challenge); + + if (cur[0] != 'r' || cur[1] != '=') + return FALSE; + + val_start = cur + 2; + val_end = strchr(val_start, ','); + if (val_end == NULL) + return FALSE; + + /* Ensure that the first cnonce_len bytes of the nonce are the original + * cnonce we sent to the server. + */ + cnonce_len = strlen(data->cnonce); + if ((val_end - val_start + 1) <= cnonce_len || + strncmp(data->cnonce, val_start, cnonce_len) != 0) + return FALSE; + + nonce = g_strndup(val_start, val_end - val_start + 1); + + /* The Salt, base64-encoded */ + cur = val_end + 1; + if (cur[0] != 's' || cur[1] != '=') { + g_free(nonce); + return FALSE; + } + + val_start = cur + 2; + val_end = strchr(val_start, ','); + if (val_end == NULL) { + g_free(nonce); + return FALSE; + } + + tmp = g_strndup(val_start, val_end - val_start + 1); + decoded = (gchar *)purple_base64_decode(tmp, &decoded_len); + g_free(tmp); + salt = g_string_new_len(decoded, decoded_len); + g_free(decoded); + + /* The iteration count */ + cur = val_end + 1; + if (cur[0] != 'i' || cur[1] != '=') { + g_free(nonce); + g_string_free(salt, TRUE); + return FALSE; + } + + val_start = cur + 2; + val_end = strchr(val_start, ','); + if (val_end == NULL) + /* There could be extensions. This should possibly be a hard fail. */ + val_end = end - 1; + + /* Validate the string */ + for (tmp = (gchar *)val_start; tmp != val_end; ++tmp) { + if (!g_ascii_isdigit(*tmp)) { + g_free(nonce); + g_string_free(salt, TRUE); + return FALSE; + } + } + + tmp = g_strndup(val_start, val_end - val_start + 1); + iterations = strtoul(tmp, NULL, 10); + g_free(tmp); + + *out_nonce = nonce; + *out_salt = salt; + *out_iterations = iterations; + return TRUE; +} + +static gboolean +parse_success(JabberScramData *data, const char *success, + gchar **out_verifier) +{ + const char *cur; + const char *val_start, *val_end; + const char *end; + + char *verifier; + + g_return_val_if_fail(data != NULL, FALSE); + g_return_val_if_fail(success != NULL, FALSE); + g_return_val_if_fail(out_verifier != NULL, FALSE); + + cur = success; + end = cur + strlen(cur); + + if (cur[0] != 'v' || cur[1] != '=') { + /* TODO: Error handling */ + return FALSE; + } + + val_start = cur + 2; + val_end = strchr(val_start, ','); + if (val_end == NULL) + /* TODO: Maybe make this a strict check on not having any extensions? */ + val_end = end - 1; + + verifier = g_strndup(val_start, val_end - val_start + 1); + + *out_verifier = verifier; + return TRUE; +} + +static xmlnode *scram_start(JabberStream *js, xmlnode *mechanisms) +{ + xmlnode *reply; + JabberScramData *data; + guint64 cnonce; +#ifdef CHANNEL_BINDING + gboolean binding_supported = TRUE; +#endif + gchar *dec_out, *enc_out; + + data = js->auth_mech_data = g_new0(JabberScramData, 1); + data->hash = mech_to_hash(js->auth_mech->name); + +#ifdef CHANNEL_BINDING + if (strstr(js->auth_mech_name, "-PLUS")) + data->channel_binding = TRUE; +#endif + cnonce = ((guint64)g_random_int() << 32) | g_random_int(); + data->cnonce = purple_base64_encode((guchar *)cnonce, sizeof(cnonce)); + + data->auth_message = g_string_new(NULL); + g_string_printf(data->auth_message, "n=%s,r=%s", + js->user->node /* TODO: SaslPrep */, + data->cnonce); + + reply = xmlnode_new("auth"); + xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl"); + xmlnode_set_attrib(reply, "mechanism", js->auth_mech->name); + + /* TODO: Channel binding */ + dec_out = g_strdup_printf("%c,,%s", 'n', data->auth_message->str); + enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out)); + purple_debug_misc("jabber", "initial SCRAM message '%s'\n", dec_out); + + xmlnode_insert_data(reply, enc_out, -1); + + g_free(enc_out); + g_free(dec_out); + + return reply; +} + +static xmlnode *scram_handle_challenge(JabberStream *js, xmlnode *challenge) +{ + JabberScramData *data = js->auth_mech_data; + xmlnode *reply; + + gchar *enc_in, *dec_in; + gchar *enc_out, *dec_out; + gsize decoded_size; + + gchar *enc_proof; + + gchar *nonce; + GString *salt; + guint iterations; + + g_return_val_if_fail(data != NULL, NULL); + + enc_in = xmlnode_get_data(challenge); + /* TODO: Error handling */ + g_return_val_if_fail(enc_in != NULL && *enc_in != '\0', NULL); + + dec_in = (gchar *)purple_base64_decode(enc_in, &decoded_size); + g_free(enc_in); + if (!dec_in || decoded_size != strlen(dec_in)) { + /* Danger afoot; SCRAM shouldn't contain NUL bytes */ + /* TODO: Error handling */ + g_free(dec_in); + return NULL; + } + + purple_debug_misc("jabber", "decoded challenge (%" G_GSIZE_FORMAT "): %s\n", + decoded_size, dec_in); + + g_string_append_c(data->auth_message, ','); + g_string_append(data->auth_message, dec_in); + + if (!parse_challenge(data, dec_in, &nonce, &salt, &iterations)) { + /* TODO: Error handling */ + return NULL; + } + + g_string_append_c(data->auth_message, ','); + /* "biwsCg==" is the base64 encoding of "n,,". I promise. */ + g_string_append_printf(data->auth_message, "c=%s,r=%s", "biwsCg==", nonce); +#ifdef CHANNEL_BINDING + #error fix this +#endif + + if (!jabber_scram_calc_proofs(data, purple_connection_get_password(js->gc), salt, iterations)) { + /* TODO: Error handling */ + return NULL; + } + + reply = xmlnode_new("response"); + xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl"); + + enc_proof = purple_base64_encode((guchar *)data->client_proof->str, data->client_proof->len); + dec_out = g_strdup_printf("c=%s,r=%s,p=%s", "biwsCg==", nonce, enc_proof); + enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out)); + + purple_debug_misc("jabber", "decoded response (%" G_GSIZE_FORMAT "): %s\n", + strlen(dec_out), dec_out); + + xmlnode_insert_data(reply, enc_out, -1); + + g_free(enc_out); + g_free(dec_out); + g_free(enc_proof); + + return reply; +} + +static gboolean scram_handle_success(JabberStream *js, xmlnode *packet) +{ + JabberScramData *data = js->auth_mech_data; + char *enc_in, *dec_in; + char *enc_server_signature; + guchar *server_signature; + gsize decoded_size; + + enc_in = xmlnode_get_data(packet); + /* TODO: Error handling */ + g_return_val_if_fail(enc_in != NULL && *enc_in != '\0', FALSE); + + dec_in = (gchar *)purple_base64_decode(enc_in, &decoded_size); + g_free(enc_in); + if (!dec_in || decoded_size != strlen(dec_in)) { + /* Danger afoot; SCRAM shouldn't contain NUL bytes */ + /* TODO: Error handling */ + g_free(dec_in); + return FALSE; + } + + purple_debug_misc("jabber", "decoded success (%" G_GSIZE_FORMAT "): %s\n", + decoded_size, dec_in); + + if (!parse_success(data, dec_in, &enc_server_signature)) { + /* TODO: Error handling */ + return FALSE; + } + + server_signature = purple_base64_decode(enc_server_signature, &decoded_size); + if (server_signature == NULL) { + /* TODO: Error handling */ + return FALSE; + } + + if (decoded_size != data->server_signature->len) { + /* TODO: Error handling */ + purple_debug_error("jabber", "SCRAM server signature wrong length (was " + "(was %" G_GSIZE_FORMAT ", expected %" G_GSIZE_FORMAT ")\n", + decoded_size, data->server_signature->len); + return FALSE; + } + + if (0 != memcmp(server_signature, data->server_signature->str, decoded_size)) { + /* TODO: Error handling */ + purple_debug_error("jabber", "SCRAM server signature did not match!\n"); + return FALSE; + } + + /* Hooray */ + return TRUE; +} + +static void scram_dispose(JabberStream *js) +{ + if (js->auth_mech_data) { + JabberScramData *data = js->auth_mech_data; + + g_free(data->cnonce); + if (data->auth_message) + g_string_free(data->auth_message, TRUE); + if (data->client_proof) + g_string_free(data->client_proof, TRUE); + if (data->server_signature) + g_string_free(data->server_signature, TRUE); + g_free(data); + js->auth_mech_data = NULL; + } +} + +static JabberSaslMech scram_sha1_mech = { + 50, /* priority */ + "SCRAM-SHA-1", /* name */ + scram_start, + scram_handle_challenge, + scram_handle_success, + NULL, /* handle_failure */ + scram_dispose +}; + +#ifdef CHANNEL_BINDING +/* With channel binding */ +static JabberSaslMech scram_sha1_plus_mech = { + scram_sha1_mech.priority + 1, /* priority */ + "SCRAM-SHA-1-PLUS", /* name */ + scram_start, + scram_handle_challenge, + scram_handle_success, + NULL, /* handle_failure */ + scram_dispose +}; +#endif + +/* For tests */ +JabberSaslMech *jabber_scram_get_sha1(void) +{ + return &scram_sha1_mech; +} + +JabberSaslMech **jabber_auth_get_scram_mechs(gint *count) +{ + static JabberSaslMech *mechs[] = { + &scram_sha1_mech, +#ifdef CHANNEL_BINDING + &scram_sha1_plus_mech, +#endif + }; + + g_return_val_if_fail(count != NULL, NULL); + + *count = G_N_ELEMENTS(mechs); + return mechs; +}