Mercurial > pidgin
view libpurple/protocols/jabber/auth_scram.c @ 28709:b0fb53868142
jabber: Handle the case where the server success-with-data is sent as a challenge/response pair.
This should also make it easier to feed C/R pairs via the tester.
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Wed, 11 Nov 2009 20:32:09 +0000 |
parents | c1d41b7484ff |
children | 65a34cce02e3 |
line wrap: on
line source
/* * purple - Jabber Protocol Plugin * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA * */ #include "internal.h" #include "auth.h" #include "auth_scram.h" #include "cipher.h" #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 }, }; 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; g_return_val_if_fail(hash != NULL && *hash != '\0', 0); for (i = 0; i < G_N_ELEMENTS(hash_sizes); ++i) { if (g_str_equal(hash, hash_sizes[i].hash)) return hash_sizes[i].size; } purple_debug_error("jabber", "Unknown SCRAM hash function %s\n", hash); return 0; } guchar *jabber_scram_hi(const gchar *hash, const GString *str, GString *salt, guint iterations) { PurpleCipherContext *context; guchar *result; guint i; guint hash_len; guchar *prev, *tmp; g_return_val_if_fail(hash != NULL, NULL); g_return_val_if_fail(str != NULL && str->len > 0, NULL); g_return_val_if_fail(salt != NULL && salt->len > 0, NULL); g_return_val_if_fail(iterations > 0, NULL); hash_len = hash_to_output_len(hash); g_return_val_if_fail(hash_len > 0, NULL); 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); /* Append INT(1), a four-octet encoding of the integer 1, most significant * octet first. */ g_string_append_len(salt, "\0\0\0\1", 4); /* 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, result, NULL); memcpy(prev, result, hash_len); /* Compute U1...Ui */ for (i = 1; i < iterations; ++i) { guint j; 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, prev, hash_len); purple_cipher_context_digest(context, hash_len, tmp, NULL); for (j = 0; j < hash_len; ++j) result[j] ^= tmp[j]; memcpy(prev, tmp, hash_len); } purple_cipher_context_destroy(context); g_free(tmp); 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, GString *salt, guint iterations) { guint hash_len = hash_to_output_len(data->hash); guint i; GString *pass = g_string_new(data->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_server_step1(JabberScramData *data, const char *challenge, gchar **out_nonce, GString **out_salt, guint *out_iterations) { char **tokens; char *token, *decoded, *tmp; gsize len; char *nonce = NULL; GString *salt = NULL; guint iterations; tokens = g_strsplit(challenge, ",", -1); if (tokens == NULL) return FALSE; token = tokens[0]; if (token[0] != 'r' || token[1] != '=') goto err; /* Ensure that the first cnonce_len bytes of the nonce are the original * cnonce we sent to the server. */ if (!g_str_equal(data->cnonce, token + 2)) goto err; nonce = g_strdup(token + 2); /* The Salt, base64-encoded */ token = tokens[1]; if (token[0] != 's' || token[1] != '=') goto err; decoded = (gchar *)purple_base64_decode(token + 2, &len); if (!decoded || *decoded == '\0') { g_free(decoded); goto err; } salt = g_string_new_len(decoded, len); g_free(decoded); /* The iteration count */ token = tokens[2]; if (token[0] != 'i' || token[1] != '=' || token[2] == '\0') goto err; /* Validate the string */ for (tmp = token + 2; *tmp; ++tmp) if (!g_ascii_isdigit(*tmp)) goto err; iterations = strtoul(token + 2, NULL, 10); g_strfreev(tokens); *out_nonce = nonce; *out_salt = salt; *out_iterations = iterations; return TRUE; err: g_free(nonce); g_string_free(salt, TRUE); g_strfreev(tokens); return FALSE; } static gboolean parse_server_step2(JabberScramData *data, const char *challenge, gchar **out_verifier) { char **tokens; char *token; tokens = g_strsplit(challenge, ",", -1); if (tokens == NULL) return FALSE; token = tokens[0]; if (token[0] != 'v' || token[1] != '=' || token[2] == '\0') { g_strfreev(tokens); return FALSE; } *out_verifier = g_strdup(token + 2); g_strfreev(tokens); return TRUE; } static gboolean feed_parser(JabberScramData *data, gchar *in, gchar **out) { gboolean ret; g_return_val_if_fail(data != NULL, FALSE); g_string_append_c(data->auth_message, ','); g_string_append(data->auth_message, in); if (data->step == 1) { gchar *nonce, *proof; GString *salt; guint iterations; ret = parse_server_step1(data, in, &nonce, &salt, &iterations); if (!ret) return FALSE; 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 ret = jabber_scram_calc_proofs(data, salt, iterations); if (!ret) return FALSE; proof = purple_base64_encode((guchar *)data->client_proof->str, data->client_proof->len); *out = g_strdup_printf("c=%s,r=%s,p=%s", "biwsCg==", nonce, proof); g_free(proof); } else if (data->step == 2) { gchar *server_sig, *enc_server_sig; gsize len; ret = parse_server_step2(data, in, &enc_server_sig); if (!ret) return FALSE; server_sig = (gchar *)purple_base64_decode(enc_server_sig, &len); g_free(enc_server_sig); if (server_sig == NULL || len != data->server_signature->len) { g_free(server_sig); return FALSE; } if (0 != memcmp(server_sig, data->server_signature->str, len)) { g_free(server_sig); return FALSE; } g_free(server_sig); *out = NULL; } else { purple_debug_error("jabber", "SCRAM: There is no step %d\n", data->step); return FALSE; } 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); data->password = purple_connection_get_password(js->gc); #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); data->step = 1; 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 = NULL, *dec_out = NULL; gsize len; enc_in = xmlnode_get_data(challenge); if (!enc_in || *enc_in == '\0') { reply = xmlnode_new("abort"); xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl"); data->step = -1; goto out; } dec_in = (gchar *)purple_base64_decode(enc_in, &len); g_free(enc_in); if (!dec_in || len != strlen(dec_in)) { /* Danger afoot; SCRAM shouldn't contain NUL bytes */ reply = xmlnode_new("abort"); xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl"); data->step = -1; goto out; } purple_debug_misc("jabber", "decoded challenge: %s\n", dec_in); if (!feed_parser(data, dec_in, &dec_out)) { reply = xmlnode_new("abort"); xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl"); data->step = -1; goto out; } data->step += 1; reply = xmlnode_new("response"); xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl"); purple_debug_misc("jabber", "decoded response: %s\n", dec_out ? dec_out : "(null)"); if (dec_out) { enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out)); xmlnode_insert_data(reply, enc_out, -1); } out: g_free(enc_out); g_free(dec_out); return reply; } static gboolean scram_handle_success(JabberStream *js, xmlnode *packet) { JabberScramData *data = js->auth_mech_data; char *enc_in, *dec_in; char *dec_out = NULL; gsize len; enc_in = xmlnode_get_data(packet); g_return_val_if_fail(enc_in != NULL && *enc_in != '\0', FALSE); if (data->step == 3) return TRUE; if (data->step != 2) return FALSE; dec_in = (gchar *)purple_base64_decode(enc_in, &len); g_free(enc_in); if (!dec_in || len != strlen(dec_in)) { /* Danger afoot; SCRAM shouldn't contain NUL bytes */ g_free(dec_in); return FALSE; } purple_debug_misc("jabber", "decoded success: %s\n", dec_in); if (!feed_parser(data, dec_in, &dec_out) || dec_out != NULL) { g_free(dec_out); 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; }