* 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 * 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 static const JabberScramHash hashes[] = { { "-SHA-1", G_CHECKSUM_SHA1 }, static const JabberScramHash *mech_to_hash(const char *mech) g_return_val_if_fail(mech != NULL && *mech != '\0', NULL); for (i = 0; i < G_N_ELEMENTS(hashes); ++i) { if (strstr(mech, hashes[i].mech_substr)) purple_debug_error("jabber", "Unknown SCRAM mechanism %s\n", mech); g_return_val_if_reached(NULL); guchar *jabber_scram_hi(const JabberScramHash *hash, const GString *str, GString *salt, guint iterations) 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); digest_len = g_checksum_type_get_length(hash->type); prev = g_new0(guchar, digest_len); tmp = g_new0(guchar, digest_len); result = g_new0(guchar, digest_len); hmac = g_hmac_new(hash->type, (guchar *)str->str, str->len); /* Append INT(1), a four-octet encoding of the integer 1, most significant g_string_append_len(salt, "\0\0\0\1", 4); g_hmac_update(hmac, (guchar *)salt->str, salt->len); g_hmac_get_digest(hmac, result, &digest_len); memcpy(prev, result, digest_len); for (i = 1; i < iterations; ++i) { hmac = g_hmac_new(hash->type, (guchar *)str->str, str->len); g_hmac_update(hmac, prev, digest_len); g_hmac_get_digest(hmac, tmp, &digest_len); for (j = 0; j < digest_len; ++j) memcpy(prev, tmp, digest_len); * Helper functions for doing the SCRAM calculations. The first argument * is the hash algorithm. All buffers must be of the appropriate size * according to the JabberScramHash. * "str" is a NULL-terminated string for jabber_scram_hmac(). * Needless to say, these are fragile. jabber_scram_hmac(const JabberScramHash *hash, guchar *out, const guchar *key, const gchar *str) gsize digest_len = g_checksum_type_get_length(hash->type); hmac = g_hmac_new(hash->type, key, digest_len); g_hmac_update(hmac, (guchar *)str, -1); g_hmac_get_digest(hmac, out, &digest_len); jabber_scram_hash(const JabberScramHash *hash, guchar *out, const guchar *data) gsize digest_len = g_checksum_type_get_length(hash->type); checksum = g_checksum_new(hash->type); g_checksum_update(checksum, data, digest_len); g_checksum_get_digest(checksum, out, &digest_len); g_checksum_free(checksum); jabber_scram_calc_proofs(JabberScramData *data, GString *salt, guint iterations) guint hash_len = g_checksum_type_get_length(data->hash->type); GString *pass = g_string_new(data->password); guchar *client_key, *stored_key, *client_signature, *server_key; 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); memset(pass->str, 0, pass->allocated_len); g_string_free(pass, TRUE); client_key = g_new0(guchar, hash_len); stored_key = g_new0(guchar, hash_len); client_signature = g_new0(guchar, hash_len); server_key = g_new0(guchar, hash_len); /* client_key = HMAC(salted_password, "Client Key") */ jabber_scram_hmac(data->hash, client_key, salted_password, "Client Key"); /* server_key = HMAC(salted_password, "Server Key") */ jabber_scram_hmac(data->hash, server_key, salted_password, "Server Key"); /* stored_key = HASH(client_key) */ jabber_scram_hash(data->hash, stored_key, client_key); /* client_signature = HMAC(stored_key, auth_message) */ jabber_scram_hmac(data->hash, client_signature, stored_key, data->auth_message->str); /* server_signature = HMAC(server_key, auth_message) */ jabber_scram_hmac(data->hash, (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]; g_free(client_signature); parse_server_step1(JabberScramData *data, const char *challenge, gchar **out_nonce, GString **out_salt, guint *out_iterations) char *token, *decoded, *tmp; tokens = g_strsplit(challenge, ",", -1); if (token[0] != 'r' || token[1] != '=') /* Ensure that the first cnonce_len bytes of the nonce are the original * cnonce we sent to the server. if (0 != strncmp(data->cnonce, token + 2, strlen(data->cnonce))) nonce = g_strdup(token + 2); /* The Salt, base64-encoded */ if (token[0] != 's' || token[1] != '=') decoded = (gchar *)g_base64_decode(token + 2, &len); if (!decoded || *decoded == '\0') { salt = g_string_new_len(decoded, len); /* The iteration count */ if (token[0] != 'i' || token[1] != '=' || token[2] == '\0') /* Validate the string */ for (tmp = token + 2; *tmp; ++tmp) if (!g_ascii_isdigit(*tmp)) iterations = strtoul(token + 2, NULL, 10); *out_iterations = iterations; g_string_free(salt, TRUE); parse_server_step2(JabberScramData *data, const char *challenge, gchar **out_verifier) tokens = g_strsplit(challenge, ",", -1); if (token[0] != 'v' || token[1] != '=' || token[2] == '\0') { *out_verifier = g_strdup(token + 2); jabber_scram_feed_parser(JabberScramData *data, gchar *in, gchar **out) g_return_val_if_fail(data != NULL, FALSE); g_string_append_c(data->auth_message, ','); g_string_append(data->auth_message, in); ret = parse_server_step1(data, in, &nonce, &salt, &iterations); g_string_append_c(data->auth_message, ','); /* "biws" is the base64 encoding of "n,,". I promise. */ g_string_append_printf(data->auth_message, "c=%s,r=%s", "biws", nonce); ret = jabber_scram_calc_proofs(data, salt, iterations); g_string_free(salt, TRUE); proof = g_base64_encode((guchar *)data->client_proof->str, data->client_proof->len); *out = g_strdup_printf("c=%s,r=%s,p=%s", "biws", nonce, proof); } else if (data->step == 2) { gchar *server_sig, *enc_server_sig; ret = parse_server_step2(data, in, &enc_server_sig); server_sig = (gchar *)g_base64_decode(enc_server_sig, &len); if (server_sig == NULL || len != data->server_signature->len) { if (0 != memcmp(server_sig, data->server_signature->str, len)) { purple_debug_error("jabber", "SCRAM: There is no step %d\n", data->step); static gchar *escape_username(const gchar *in) tmp = purple_strreplace(in, "=", "=3D"); tmp2 = purple_strreplace(tmp, ",", "=2C"); scram_start(JabberStream *js, PurpleXmlNode *mechanisms, PurpleXmlNode **out, char **error) gboolean binding_supported = TRUE; gchar *dec_out, *enc_out; gchar *prepped_node, *tmp; prepped_node = jabber_saslprep(js->user->node); *error = g_strdup(_("Unable to canonicalize username")); return JABBER_SASL_STATE_FAIL; tmp = escape_username(prepped_node); prepped_pass = jabber_saslprep(purple_connection_get_password(js->gc)); *error = g_strdup(_("Unable to canonicalize password")); return JABBER_SASL_STATE_FAIL; data = js->auth_mech_data = g_new0(JabberScramData, 1); data->hash = mech_to_hash(js->auth_mech->name); data->password = prepped_pass; if (strstr(js->auth_mech_name, "-PLUS")) data->channel_binding = TRUE; cnonce = ((guint64)g_random_int() << 32) | g_random_int(); data->cnonce = g_base64_encode((guchar *)&cnonce, sizeof(cnonce)); data->auth_message = g_string_new(NULL); g_string_printf(data->auth_message, "n=%s,r=%s", prepped_node, data->cnonce); reply = purple_xmlnode_new("auth"); purple_xmlnode_set_namespace(reply, NS_XMPP_SASL); purple_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 = g_base64_encode((guchar *)dec_out, strlen(dec_out)); purple_debug_misc("jabber", "initial SCRAM message '%s'\n", dec_out); purple_xmlnode_insert_data(reply, enc_out, -1); return JABBER_SASL_STATE_CONTINUE; scram_handle_challenge(JabberStream *js, PurpleXmlNode *challenge, PurpleXmlNode **out, char **error) JabberScramData *data = js->auth_mech_data; gchar *enc_in, *dec_in = NULL; gchar *enc_out = NULL, *dec_out = NULL; JabberSaslState state = JABBER_SASL_STATE_FAIL; enc_in = purple_xmlnode_get_data(challenge); if (!enc_in || *enc_in == '\0') { reply = purple_xmlnode_new("abort"); purple_xmlnode_set_namespace(reply, NS_XMPP_SASL); *error = g_strdup(_("Invalid challenge from server")); dec_in = (gchar *)g_base64_decode(enc_in, &len); if (!dec_in || len != strlen(dec_in)) { /* Danger afoot; SCRAM shouldn't contain NUL bytes */ reply = purple_xmlnode_new("abort"); purple_xmlnode_set_namespace(reply, NS_XMPP_SASL); *error = g_strdup(_("Malicious challenge from server")); purple_debug_misc("jabber", "decoded challenge: %s\n", dec_in); if (!jabber_scram_feed_parser(data, dec_in, &dec_out)) { reply = purple_xmlnode_new("abort"); purple_xmlnode_set_namespace(reply, NS_XMPP_SASL); *error = g_strdup(_("Invalid challenge from server")); reply = purple_xmlnode_new("response"); purple_xmlnode_set_namespace(reply, NS_XMPP_SASL); purple_debug_misc("jabber", "decoded response: %s\n", dec_out ? dec_out : "(null)"); enc_out = g_base64_encode((guchar *)dec_out, strlen(dec_out)); purple_xmlnode_insert_data(reply, enc_out, -1); state = JABBER_SASL_STATE_CONTINUE; scram_handle_success(JabberStream *js, PurpleXmlNode *packet, char **error) JabberScramData *data = js->auth_mech_data; enc_in = purple_xmlnode_get_data(packet); if (data->step != 3 && (!enc_in || *enc_in == '\0')) { *error = g_strdup(_("Invalid challenge from server")); return JABBER_SASL_STATE_FAIL; * If the server took the slow approach (sending the verifier * as a challenge/response pair), we get here. return JABBER_SASL_STATE_OK; *error = g_strdup(_("Unexpected response from server")); return JABBER_SASL_STATE_FAIL; dec_in = (gchar *)g_base64_decode(enc_in, &len); if (!dec_in || len != strlen(dec_in)) { /* Danger afoot; SCRAM shouldn't contain NUL bytes */ *error = g_strdup(_("Malicious challenge from server")); return JABBER_SASL_STATE_FAIL; purple_debug_misc("jabber", "decoded success: %s\n", dec_in); if (!jabber_scram_feed_parser(data, dec_in, &dec_out) || dec_out != NULL) { *error = g_strdup(_("Invalid challenge from server")); return JABBER_SASL_STATE_FAIL; return JABBER_SASL_STATE_OK; void jabber_scram_data_destroy(JabberScramData *data) g_string_free(data->auth_message, TRUE); g_string_free(data->client_proof, TRUE); if (data->server_signature) g_string_free(data->server_signature, TRUE); memset(data->password, 0, strlen(data->password)); static void scram_dispose(JabberStream *js) if (js->auth_mech_data) { jabber_scram_data_destroy(js->auth_mech_data); js->auth_mech_data = NULL; static JabberSaslMech scram_sha1_mech = { "SCRAM-SHA-1", /* name */ NULL, /* handle_failure */ /* With channel binding */ static JabberSaslMech scram_sha1_plus_mech = { scram_sha1_mech.priority + 1, /* priority */ "SCRAM-SHA-1-PLUS", /* name */ NULL, /* handle_failure */ JabberSaslMech **jabber_auth_get_scram_mechs(gint *count) static JabberSaslMech *mechs[] = { *count = G_N_ELEMENTS(mechs);