Mercurial > pidgin
changeset 28751:5af6304fde5d
propagate from branch 'im.pidgin.pidgin' (head 2b7a2f20d950d118157c7629dfbfa17b7afb04f0)
to branch 'im.pidgin.cpw.darkrain42.xmpp.scram' (head a7195390270427d08290c571865a54515cfb33fd)
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Thu, 03 Dec 2009 17:00:54 +0000 |
parents | ebb424b7e0a1 (current diff) 0437b62ffaa5 (diff) |
children | a6cd1db77aee |
files | |
diffstat | 19 files changed, 2031 insertions(+), 793 deletions(-) [+] |
line wrap: on
line diff
--- a/configure.ac Thu Dec 03 08:02:54 2009 +0000 +++ b/configure.ac Thu Dec 03 17:00:54 2009 +0000 @@ -2267,11 +2267,15 @@ AC_ARG_ENABLE(cyrus-sasl, AC_HELP_STRING([--enable-cyrus-sasl], [enable Cyrus SASL support for jabberd]), enable_cyrus_sasl=$enableval, enable_cyrus_sasl=no) if test "x$enable_cyrus_sasl" = "xyes" ; then AC_CHECK_LIB(sasl2, sasl_client_init, [ + AM_CONDITIONAL(USE_CYRUS_SASL, true) AC_DEFINE(HAVE_CYRUS_SASL, [1], [Define to 1 if Cyrus SASL is present]) SASL_LIBS=-"lsasl2" ], [ + AM_CONDITIONAL(USE_CYRUS_SASL, false) AC_ERROR(Cyrus SASL library not found) ]) +else + AM_CONDITIONAL(USE_CYRUS_SASL, false) fi dnl #######################################################################
--- a/libpurple/protocols/jabber/Makefile.am Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/Makefile.am Thu Dec 03 17:00:54 2009 +0000 @@ -10,6 +10,10 @@ adhoccommands.h \ auth.c \ auth.h \ + auth_digest_md5.c \ + auth_plain.c \ + auth_scram.c \ + auth_scram.h \ buddy.c \ buddy.h \ bosh.c \ @@ -78,6 +82,10 @@ libxmpp_la_LDFLAGS = -module -avoid-version +if USE_CYRUS_SASL +JABBERSOURCES += auth_cyrus.c +endif + if STATIC_JABBER st = -DPURPLE_STATIC_PRPL
--- a/libpurple/protocols/jabber/auth.c Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/auth.c Thu Dec 03 17:00:54 2009 +0000 @@ -39,6 +39,8 @@ #include "iq.h" #include "notify.h" +static GSList *auth_mechs = NULL; + static void auth_old_result_cb(JabberStream *js, const char *from, JabberIqType type, const char *id, xmlnode *packet, gpointer data); @@ -46,8 +48,11 @@ gboolean jabber_process_starttls(JabberStream *js, xmlnode *packet) { + PurpleAccount *account; xmlnode *starttls; + account = purple_connection_get_account(js->gc); + if((starttls = xmlnode_get_child(packet, "starttls"))) { if(purple_ssl_is_supported()) { jabber_send_raw(js, @@ -58,7 +63,7 @@ PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, _("Server requires TLS/SSL, but no TLS/SSL support was found.")); return TRUE; - } else if(purple_account_get_bool(js->gc->account, "require_tls", JABBER_DEFAULT_REQUIRE_TLS)) { + } else if(purple_account_get_bool(account, "require_tls", JABBER_DEFAULT_REQUIRE_TLS)) { purple_connection_error_reason(js->gc, PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT, _("You require encryption, but no TLS/SSL support was found.")); @@ -71,418 +76,96 @@ static void finish_plaintext_authentication(JabberStream *js) { - if(js->auth_type == JABBER_AUTH_PLAIN) { - xmlnode *auth; - GString *response; - gchar *enc_out; - - auth = xmlnode_new("auth"); - xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl"); - - xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth"); - xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true"); - - response = g_string_new(""); - response = g_string_append_len(response, "\0", 1); - response = g_string_append(response, js->user->node); - response = g_string_append_len(response, "\0", 1); - response = g_string_append(response, - purple_connection_get_password(js->gc)); - - enc_out = purple_base64_encode((guchar *)response->str, response->len); + JabberIq *iq; + xmlnode *query, *x; - xmlnode_set_attrib(auth, "mechanism", "PLAIN"); - xmlnode_insert_data(auth, enc_out, -1); - g_free(enc_out); - g_string_free(response, TRUE); - - jabber_send(js, auth); - xmlnode_free(auth); - } else if(js->auth_type == JABBER_AUTH_IQ_AUTH) { - JabberIq *iq; - xmlnode *query, *x; - - iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:auth"); - query = xmlnode_get_child(iq->node, "query"); - x = xmlnode_new_child(query, "username"); - xmlnode_insert_data(x, js->user->node, -1); - x = xmlnode_new_child(query, "resource"); - xmlnode_insert_data(x, js->user->resource, -1); - x = xmlnode_new_child(query, "password"); - xmlnode_insert_data(x, purple_connection_get_password(js->gc), -1); - jabber_iq_set_callback(iq, auth_old_result_cb, NULL); - jabber_iq_send(iq); - } + iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:auth"); + query = xmlnode_get_child(iq->node, "query"); + x = xmlnode_new_child(query, "username"); + xmlnode_insert_data(x, js->user->node, -1); + x = xmlnode_new_child(query, "resource"); + xmlnode_insert_data(x, js->user->resource, -1); + x = xmlnode_new_child(query, "password"); + xmlnode_insert_data(x, purple_connection_get_password(js->gc), -1); + jabber_iq_set_callback(iq, auth_old_result_cb, NULL); + jabber_iq_send(iq); } static void allow_plaintext_auth(PurpleAccount *account) { + PurpleConnection *gc; + JabberStream *js; + purple_account_set_bool(account, "auth_plain_in_clear", TRUE); - finish_plaintext_authentication(account->gc->proto_data); + gc = purple_account_get_connection(account); + js = purple_connection_get_protocol_data(gc); + + finish_plaintext_authentication(js); } static void disallow_plaintext_auth(PurpleAccount *account) { - purple_connection_error_reason(account->gc, + purple_connection_error_reason(purple_account_get_connection(account), PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, _("Server requires plaintext authentication over an unencrypted stream")); } #ifdef HAVE_CYRUS_SASL - -static void jabber_auth_start_cyrus(JabberStream *); -static void jabber_sasl_build_callbacks(JabberStream *); - -/* Callbacks for Cyrus SASL */ - -static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result) -{ - JabberStream *js = (JabberStream *)ctx; - - if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM; - - *result = js->user->domain; - - return SASL_OK; -} - -static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len) +static void +auth_old_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) { - JabberStream *js = (JabberStream *)ctx; - - switch(id) { - case SASL_CB_AUTHNAME: - *res = js->user->node; - break; - case SASL_CB_USER: - *res = ""; - break; - default: - return SASL_BADPARAM; - } - if (len) *len = strlen((char *)*res); - return SASL_OK; -} - -static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret) -{ - JabberStream *js = (JabberStream *)ctx; - const char *pw = purple_account_get_password(js->gc->account); - size_t len; - static sasl_secret_t *x = NULL; - - if (!conn || !secret || id != SASL_CB_PASS) - return SASL_BADPARAM; - - len = strlen(pw); - x = (sasl_secret_t *) realloc(x, sizeof(sasl_secret_t) + len); - - if (!x) - return SASL_NOMEM; - - x->len = len; - strcpy((char*)x->data, pw); - - *secret = x; - return SASL_OK; -} - -static void allow_cyrus_plaintext_auth(PurpleAccount *account) -{ - purple_account_set_bool(account, "auth_plain_in_clear", TRUE); - - jabber_auth_start_cyrus(account->gc->proto_data); -} - -static gboolean auth_pass_generic(JabberStream *js, PurpleRequestFields *fields) -{ + PurpleAccount *account; + JabberStream *js; const char *entry; gboolean remember; + /* The password prompt dialog doesn't get disposed if the account disconnects */ + if (!PURPLE_CONNECTION_IS_VALID(gc)) + return; + + account = purple_connection_get_account(gc); + js = purple_connection_get_protocol_data(gc); + entry = purple_request_fields_get_string(fields, "password"); remember = purple_request_fields_get_bool(fields, "remember"); if (!entry || !*entry) { - purple_notify_error(js->gc->account, NULL, _("Password is required to sign on."), NULL); - return FALSE; + purple_notify_error(account, NULL, _("Password is required to sign on."), NULL); + return; } if (remember) - purple_account_set_remember_password(js->gc->account, TRUE); - - purple_account_set_password(js->gc->account, entry); - - return TRUE; -} - -static void auth_pass_cb(PurpleConnection *conn, PurpleRequestFields *fields) -{ - JabberStream *js; - - /* The password prompt dialog doesn't get disposed if the account disconnects */ - if (!PURPLE_CONNECTION_IS_VALID(conn)) - return; - - js = conn->proto_data; - - if (!auth_pass_generic(js, fields)) - return; + purple_account_set_remember_password(account, TRUE); - /* Rebuild our callbacks as we now have a password to offer */ - jabber_sasl_build_callbacks(js); - - /* Restart our connection */ - jabber_auth_start_cyrus(js); -} - -static void -auth_old_pass_cb(PurpleConnection *conn, PurpleRequestFields *fields) -{ - JabberStream *js; - - /* The password prompt dialog doesn't get disposed if the account disconnects */ - if (!PURPLE_CONNECTION_IS_VALID(conn)) - return; - - js = conn->proto_data; - - if (!auth_pass_generic(js, fields)) - return; + purple_account_set_password(account, entry); /* Restart our connection */ jabber_auth_start_old(js); } - static void -auth_no_pass_cb(PurpleConnection *conn, PurpleRequestFields *fields) +auth_no_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) { - JabberStream *js; - /* The password prompt dialog doesn't get disposed if the account disconnects */ - if (!PURPLE_CONNECTION_IS_VALID(conn)) + if (!PURPLE_CONNECTION_IS_VALID(gc)) return; - js = conn->proto_data; - /* Disable the account as the user has canceled connecting */ - purple_account_set_enabled(conn->account, purple_core_get_ui(), FALSE); + purple_account_set_enabled(purple_connection_get_account(gc), purple_core_get_ui(), FALSE); } - -static void jabber_auth_start_cyrus(JabberStream *js) -{ - const char *clientout = NULL; - char *enc_out; - unsigned coutlen = 0; - xmlnode *auth; - sasl_security_properties_t secprops; - gboolean again; - gboolean plaintext = TRUE; - - /* Set up security properties and options */ - secprops.min_ssf = 0; - secprops.security_flags = SASL_SEC_NOANONYMOUS; - - if (!jabber_stream_is_ssl(js)) { - secprops.max_ssf = -1; - secprops.maxbufsize = 4096; - plaintext = purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE); - if (!plaintext) - secprops.security_flags |= SASL_SEC_NOPLAINTEXT; - } else { - secprops.max_ssf = 0; - secprops.maxbufsize = 0; - plaintext = TRUE; - } - secprops.property_names = 0; - secprops.property_values = 0; - - do { - again = FALSE; - - js->sasl_state = sasl_client_new("xmpp", js->serverFQDN, NULL, NULL, js->sasl_cb, 0, &js->sasl); - if (js->sasl_state==SASL_OK) { - sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops); - purple_debug_info("sasl", "Mechs found: %s\n", js->sasl_mechs->str); - js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &js->current_mech); - } - switch (js->sasl_state) { - /* Success */ - case SASL_OK: - case SASL_CONTINUE: - break; - case SASL_NOMECH: - /* No mechanisms have offered to help */ - - /* Firstly, if we don't have a password try - * to get one - */ - - if (!purple_account_get_password(js->gc->account)) { - purple_account_request_password(js->gc->account, G_CALLBACK(auth_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc); - return; - - /* If we've got a password, but aren't sending - * it in plaintext, see if we can turn on - * plaintext auth - */ - } else if (!plaintext) { - char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), - js->gc->account->username); - purple_request_yes_no(js->gc, _("Plaintext Authentication"), - _("Plaintext Authentication"), - msg, - 1, js->gc->account, NULL, NULL, js->gc->account, - allow_cyrus_plaintext_auth, - disallow_plaintext_auth); - g_free(msg); - return; - - } else { - /* We have no mechs which can work. - * Try falling back on the old jabber:iq:auth method. We get here if the server supports - * one or more sasl mechs, we are compiled with cyrus-sasl support, but we support or can connect with none of - * the offerred mechs. jabberd 2.0 w/ SASL and Apple's iChat Server 10.5 both handle and expect - * jabber:iq:auth in this situation. iChat Server in particular offers SASL GSSAPI by default, which is often - * not configured on the client side, and expects a fallback to jabber:iq:auth when it (predictably) fails. - * - * Note: xep-0078 points out that using jabber:iq:auth after a sasl failure is wrong. However, - * I believe this refers to actual authentication failure, not a simple lack of concordant mechanisms. - * Doing otherwise means that simply compiling with SASL support renders the client unable to connect to servers - * which would connect without issue otherwise. -evands - */ - js->auth_type = JABBER_AUTH_IQ_AUTH; - jabber_auth_start_old(js); - return; - } - /* not reached */ - break; - - /* Fatal errors. Give up and go home */ - case SASL_BADPARAM: - case SASL_NOMEM: - break; - - /* For everything else, fail the mechanism and try again */ - default: - purple_debug_info("sasl", "sasl_state is %d, failing the mech and trying again\n", js->sasl_state); - - /* - * DAA: is this right? - * The manpage says that "mech" will contain the chosen mechanism on success. - * Presumably, if we get here that isn't the case and we shouldn't try again? - * I suspect that this never happens. - */ - /* - * SXW: Yes, this is right. What this handles is the situation where a - * mechanism, say GSSAPI, is tried. If that mechanism fails, it may be - * due to mechanism specific issues, so we want to try one of the other - * supported mechanisms. This code handles that case - */ - if (js->current_mech && *js->current_mech) { - char *pos; - if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) { - g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(js->current_mech)); - } - /* Remove space which separated this mech from the next */ - if ((js->sasl_mechs->str)[0] == ' ') { - g_string_erase(js->sasl_mechs, 0, 1); - } - again = TRUE; - } - - sasl_dispose(&js->sasl); - } - } while (again); - - if (js->sasl_state == SASL_CONTINUE || js->sasl_state == SASL_OK) { - auth = xmlnode_new("auth"); - xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl"); - xmlnode_set_attrib(auth, "mechanism", js->current_mech); - - xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth"); - xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true"); - - if (clientout) { - if (coutlen == 0) { - xmlnode_insert_data(auth, "=", -1); - } else { - enc_out = purple_base64_encode((unsigned char*)clientout, coutlen); - xmlnode_insert_data(auth, enc_out, -1); - g_free(enc_out); - } - } - jabber_send(js, auth); - xmlnode_free(auth); - } else { - purple_connection_error_reason(js->gc, - PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, - _("SASL authentication failed")); - } -} - -static int -jabber_sasl_cb_log(void *context, int level, const char *message) -{ - if(level <= SASL_LOG_TRACE) - purple_debug_info("sasl", "%s\n", message); - - return SASL_OK; -} - -void -jabber_sasl_build_callbacks(JabberStream *js) -{ - int id; - - /* Set up our callbacks structure */ - if (js->sasl_cb == NULL) - js->sasl_cb = g_new0(sasl_callback_t,6); - - id = 0; - js->sasl_cb[id].id = SASL_CB_GETREALM; - js->sasl_cb[id].proc = jabber_sasl_cb_realm; - js->sasl_cb[id].context = (void *)js; - id++; - - js->sasl_cb[id].id = SASL_CB_AUTHNAME; - js->sasl_cb[id].proc = jabber_sasl_cb_simple; - js->sasl_cb[id].context = (void *)js; - id++; - - js->sasl_cb[id].id = SASL_CB_USER; - js->sasl_cb[id].proc = jabber_sasl_cb_simple; - js->sasl_cb[id].context = (void *)js; - id++; - - if (purple_account_get_password(js->gc->account) != NULL ) { - js->sasl_cb[id].id = SASL_CB_PASS; - js->sasl_cb[id].proc = jabber_sasl_cb_secret; - js->sasl_cb[id].context = (void *)js; - id++; - } - - js->sasl_cb[id].id = SASL_CB_LOG; - js->sasl_cb[id].proc = jabber_sasl_cb_log; - js->sasl_cb[id].context = (void*)js; - id++; - - js->sasl_cb[id].id = SASL_CB_LIST_END; -} - #endif void jabber_auth_start(JabberStream *js, xmlnode *packet) { -#ifndef HAVE_CYRUS_SASL - gboolean digest_md5 = FALSE, plain=FALSE; -#endif - + GSList *mechanisms = NULL; + GSList *l; + xmlnode *response = NULL; xmlnode *mechs, *mechnode; - + JabberSaslState state; + const char *msg = NULL; if(js->registration) { jabber_register_start(js); @@ -490,7 +173,6 @@ } mechs = xmlnode_get_child(packet, "mechanisms"); - if(!mechs) { purple_connection_error_reason(js->gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, @@ -498,76 +180,51 @@ return; } -#ifdef HAVE_CYRUS_SASL - js->sasl_mechs = g_string_new(""); -#endif - for(mechnode = xmlnode_get_child(mechs, "mechanism"); mechnode; mechnode = xmlnode_get_next_twin(mechnode)) { char *mech_name = xmlnode_get_data(mechnode); -#ifdef HAVE_CYRUS_SASL - /* Don't include Google Talk's X-GOOGLE-TOKEN mechanism, as we will not - * support it and including it gives a false fall-back to other mechs offerred, - * leading to incorrect error handling. - */ - if (purple_strequal(mech_name, "X-GOOGLE-TOKEN")) { - g_free(mech_name); - continue; - } - g_string_append(js->sasl_mechs, mech_name); - g_string_append_c(js->sasl_mechs, ' '); -#else - if (purple_strequal(mech_name, "DIGEST-MD5")) - digest_md5 = TRUE; - else if (purple_strequal(mech_name, "PLAIN")) - plain = TRUE; -#endif - g_free(mech_name); + if (mech_name && *mech_name) + mechanisms = g_slist_prepend(mechanisms, mech_name); + else if (mech_name) + g_free(mech_name); + } -#ifdef HAVE_CYRUS_SASL - js->auth_type = JABBER_AUTH_CYRUS; - - jabber_sasl_build_callbacks(js); - - jabber_auth_start_cyrus(js); -#else + for (l = auth_mechs; l; l = l->next) { + JabberSaslMech *possible = l->data; - if(digest_md5) { - xmlnode *auth; - - js->auth_type = JABBER_AUTH_DIGEST_MD5; - auth = xmlnode_new("auth"); - xmlnode_set_namespace(auth, "urn:ietf:params:xml:ns:xmpp-sasl"); - xmlnode_set_attrib(auth, "mechanism", "DIGEST-MD5"); + /* Is this the Cyrus SASL mechanism? */ + if (g_str_equal(possible->name, "*")) { + js->auth_mech = possible; + break; + } - jabber_send(js, auth); - xmlnode_free(auth); - } else if(plain) { - js->auth_type = JABBER_AUTH_PLAIN; + /* Can we find this mechanism in the server's list? */ + if (g_slist_find_custom(mechanisms, possible->name, (GCompareFunc)strcmp)) { + js->auth_mech = possible; + break; + } + } - if(!jabber_stream_is_ssl(js) && !purple_account_get_bool(js->gc->account, "auth_plain_in_clear", FALSE)) { - char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), - js->gc->account->username); - purple_request_yes_no(js->gc, _("Plaintext Authentication"), - _("Plaintext Authentication"), - msg, - 1, - purple_connection_get_account(js->gc), NULL, NULL, - purple_connection_get_account(js->gc), allow_plaintext_auth, - disallow_plaintext_auth); - g_free(msg); - return; - } - finish_plaintext_authentication(js); - } else { + if (js->auth_mech == NULL) { + /* Found no good mechanisms... */ purple_connection_error_reason(js->gc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, _("Server does not use any supported authentication method")); + return; } -#endif + + state = js->auth_mech->start(js, mechs, &response, &msg); + if (state == JABBER_SASL_STATE_FAIL) { + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, + msg ? msg : _("Unknown Error")); + } else if (response) { + jabber_send(js, response); + xmlnode_free(response); + } } static void auth_old_result_cb(JabberStream *js, const char *from, @@ -578,19 +235,22 @@ jabber_stream_set_state(js, JABBER_STREAM_POST_AUTH); jabber_disco_items_server(js); } else { + PurpleAccount *account; PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; char *msg = jabber_parse_error(js, packet, &reason); xmlnode *error; const char *err_code; + account = purple_connection_get_account(js->gc); + /* FIXME: Why is this not in jabber_parse_error? */ if((error = xmlnode_get_child(packet, "error")) && (err_code = xmlnode_get_attrib(error, "code")) && g_str_equal(err_code, "401")) { reason = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED; /* Clear the pasword if it isn't being saved */ - if (!purple_account_get_remember_password(js->gc->account)) - purple_account_set_password(js->gc->account, NULL); + if (!purple_account_get_remember_password(account)) + purple_account_set_password(account, NULL); } purple_connection_error_reason(js->gc, reason, msg); @@ -663,16 +323,17 @@ jabber_iq_send(iq); } else if(xmlnode_get_child(query, "password")) { - if(!jabber_stream_is_ssl(js) && !purple_account_get_bool(js->gc->account, + PurpleAccount *account = purple_connection_get_account(js->gc); + if(!jabber_stream_is_ssl(js) && !purple_account_get_bool(account, "auth_plain_in_clear", FALSE)) { char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), - js->gc->account->username); + purple_account_get_username(account)); purple_request_yes_no(js->gc, _("Plaintext Authentication"), _("Plaintext Authentication"), msg, 1, - purple_connection_get_account(js->gc), NULL, NULL, - purple_connection_get_account(js->gc), allow_plaintext_auth, + account, NULL, NULL, + account, allow_plaintext_auth, disallow_plaintext_auth); g_free(msg); return; @@ -689,22 +350,30 @@ void jabber_auth_start_old(JabberStream *js) { + PurpleAccount *account; JabberIq *iq; xmlnode *query, *username; + account = purple_connection_get_account(js->gc); + /* * We can end up here without encryption if the server doesn't support * <stream:features/> and we're not using old-style SSL. If the user * is requiring SSL/TLS, we need to enforce it. */ if (!jabber_stream_is_ssl(js) && - purple_account_get_bool(purple_connection_get_account(js->gc), "require_tls", JABBER_DEFAULT_REQUIRE_TLS)) { + purple_account_get_bool(account, "require_tls", JABBER_DEFAULT_REQUIRE_TLS)) { purple_connection_error_reason(js->gc, PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, _("You require encryption, but it is not available on this server.")); return; } + if (js->registration) { + jabber_register_start(js); + return; + } + /* * IQ Auth doesn't have support for resource binding, so we need to pick a * default resource so it will work properly. jabberd14 throws an error and @@ -721,8 +390,8 @@ * password prompting here */ - if (!purple_account_get_password(js->gc->account)) { - purple_account_request_password(js->gc->account, G_CALLBACK(auth_old_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc); + if (!purple_account_get_password(account)) { + purple_account_request_password(account, G_CALLBACK(auth_old_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc); return; } #endif @@ -737,352 +406,61 @@ jabber_iq_send(iq); } -/* Parts of this algorithm are inspired by stuff in libgsasl */ -static GHashTable* parse_challenge(const char *challenge) -{ - const char *token_start, *val_start, *val_end, *cur; - GHashTable *ret = g_hash_table_new_full(g_str_hash, g_str_equal, - g_free, g_free); - - cur = challenge; - while(*cur != '\0') { - /* Find the end of the token */ - gboolean in_quotes = FALSE; - char *name, *value = NULL; - token_start = cur; - while(*cur != '\0' && (in_quotes || (!in_quotes && *cur != ','))) { - if (*cur == '"') - in_quotes = !in_quotes; - cur++; - } - - /* Find start of value. */ - val_start = strchr(token_start, '='); - if (val_start == NULL || val_start > cur) - val_start = cur; - - if (token_start != val_start) { - name = g_strndup(token_start, val_start - token_start); - - if (val_start != cur) { - val_start++; - while (val_start != cur && (*val_start == ' ' || *val_start == '\t' - || *val_start == '\r' || *val_start == '\n' - || *val_start == '"')) - val_start++; - - val_end = cur; - while (val_end != val_start && (*val_end == ' ' || *val_end == ',' || *val_end == '\t' - || *val_end == '\r' || *val_end == '\n' - || *val_end == '"' || *val_end == '\0')) - val_end--; - - if (val_start != val_end) - value = g_strndup(val_start, val_end - val_start + 1); - } - - g_hash_table_replace(ret, name, value); - } - - /* Find the start of the next token, if there is one */ - if (*cur != '\0') { - cur++; - while (*cur == ' ' || *cur == ',' || *cur == '\t' - || *cur == '\r' || *cur == '\n') - cur++; - } - } - - return ret; -} - -static char * -generate_response_value(JabberID *jid, const char *passwd, const char *nonce, - const char *cnonce, const char *a2, const char *realm) -{ - PurpleCipher *cipher; - PurpleCipherContext *context; - guchar result[16]; - size_t a1len; - - gchar *a1, *convnode=NULL, *convpasswd = NULL, *ha1, *ha2, *kd, *x, *z; - - if((convnode = g_convert(jid->node, -1, "iso-8859-1", "utf-8", - NULL, NULL, NULL)) == NULL) { - convnode = g_strdup(jid->node); - } - if(passwd && ((convpasswd = g_convert(passwd, -1, "iso-8859-1", - "utf-8", NULL, NULL, NULL)) == NULL)) { - convpasswd = g_strdup(passwd); - } - - cipher = purple_ciphers_find_cipher("md5"); - context = purple_cipher_context_new(cipher, NULL); - - x = g_strdup_printf("%s:%s:%s", convnode, realm, convpasswd ? convpasswd : ""); - purple_cipher_context_append(context, (const guchar *)x, strlen(x)); - purple_cipher_context_digest(context, sizeof(result), result, NULL); - - a1 = g_strdup_printf("xxxxxxxxxxxxxxxx:%s:%s", nonce, cnonce); - a1len = strlen(a1); - g_memmove(a1, result, 16); - - purple_cipher_context_reset(context, NULL); - purple_cipher_context_append(context, (const guchar *)a1, a1len); - purple_cipher_context_digest(context, sizeof(result), result, NULL); - - ha1 = purple_base16_encode(result, 16); - - purple_cipher_context_reset(context, NULL); - purple_cipher_context_append(context, (const guchar *)a2, strlen(a2)); - purple_cipher_context_digest(context, sizeof(result), result, NULL); - - ha2 = purple_base16_encode(result, 16); - - kd = g_strdup_printf("%s:%s:00000001:%s:auth:%s", ha1, nonce, cnonce, ha2); - - purple_cipher_context_reset(context, NULL); - purple_cipher_context_append(context, (const guchar *)kd, strlen(kd)); - purple_cipher_context_digest(context, sizeof(result), result, NULL); - purple_cipher_context_destroy(context); - - z = purple_base16_encode(result, 16); - - g_free(convnode); - g_free(convpasswd); - g_free(x); - g_free(a1); - g_free(ha1); - g_free(ha2); - g_free(kd); - - return z; -} - void jabber_auth_handle_challenge(JabberStream *js, xmlnode *packet) { - - if(js->auth_type == JABBER_AUTH_DIGEST_MD5) { - char *enc_in = xmlnode_get_data(packet); - char *dec_in; - char *enc_out; - GHashTable *parts; - - if(!enc_in) { - purple_connection_error_reason(js->gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Invalid response from server")); - return; - } - - dec_in = (char *)purple_base64_decode(enc_in, NULL); - purple_debug_misc("jabber", "decoded challenge (%" - G_GSIZE_FORMAT "): %s\n", strlen(dec_in), dec_in); - - parts = parse_challenge(dec_in); - - - if (g_hash_table_lookup(parts, "rspauth")) { - char *rspauth = g_hash_table_lookup(parts, "rspauth"); - - - if (rspauth && purple_strequal(rspauth, js->expected_rspauth)) { - jabber_send_raw(js, - "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", - -1); - } else { - purple_connection_error_reason(js->gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Invalid challenge from server")); - } - g_free(js->expected_rspauth); - js->expected_rspauth = NULL; - } else { - /* assemble a response, and send it */ - /* see RFC 2831 */ - char *realm; - char *nonce; - - /* Make sure the auth string contains everything that should be there. - This isn't everything in RFC2831, but it is what we need. */ - - nonce = g_hash_table_lookup(parts, "nonce"); - - /* we're actually supposed to prompt the user for a realm if - * the server doesn't send one, but that really complicates things, - * so i'm not gonna worry about it until is poses a problem to - * someone, or I get really bored */ - realm = g_hash_table_lookup(parts, "realm"); - if(!realm) - realm = js->user->domain; - - if (nonce == NULL || realm == NULL) - purple_connection_error_reason(js->gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Invalid challenge from server")); - else { - GString *response = g_string_new(""); - char *a2; - char *auth_resp; - char *buf; - char *cnonce; - - cnonce = g_strdup_printf("%x%u%x", g_random_int(), (int)time(NULL), - g_random_int()); - - a2 = g_strdup_printf("AUTHENTICATE:xmpp/%s", realm); - auth_resp = generate_response_value(js->user, - purple_connection_get_password(js->gc), nonce, cnonce, a2, realm); - g_free(a2); - - a2 = g_strdup_printf(":xmpp/%s", realm); - js->expected_rspauth = generate_response_value(js->user, - purple_connection_get_password(js->gc), nonce, cnonce, a2, realm); - g_free(a2); - - g_string_append_printf(response, "username=\"%s\"", js->user->node); - g_string_append_printf(response, ",realm=\"%s\"", realm); - g_string_append_printf(response, ",nonce=\"%s\"", nonce); - g_string_append_printf(response, ",cnonce=\"%s\"", cnonce); - g_string_append_printf(response, ",nc=00000001"); - g_string_append_printf(response, ",qop=auth"); - g_string_append_printf(response, ",digest-uri=\"xmpp/%s\"", realm); - g_string_append_printf(response, ",response=%s", auth_resp); - g_string_append_printf(response, ",charset=utf-8"); + const char *ns = xmlnode_get_namespace(packet); - g_free(auth_resp); - g_free(cnonce); - - enc_out = purple_base64_encode((guchar *)response->str, response->len); - - purple_debug_misc("jabber", "decoded response (%" - G_GSIZE_FORMAT "): %s\n", - response->len, response->str); - - buf = g_strdup_printf("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>%s</response>", enc_out); - - jabber_send_raw(js, buf, -1); - - g_free(buf); - - g_free(enc_out); - - g_string_free(response, TRUE); - } - } - - g_free(enc_in); - g_free(dec_in); - g_hash_table_destroy(parts); - } -#ifdef HAVE_CYRUS_SASL - else if (js->auth_type == JABBER_AUTH_CYRUS) { - char *enc_in = xmlnode_get_data(packet); - unsigned char *dec_in; - char *enc_out; - const char *c_out; - unsigned int clen; - gsize declen; - xmlnode *response; - - dec_in = purple_base64_decode(enc_in, &declen); - - js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, - NULL, &c_out, &clen); - g_free(enc_in); - g_free(dec_in); - if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) { - gchar *tmp = g_strdup_printf(_("SASL error: %s"), - sasl_errdetail(js->sasl)); - purple_debug_error("jabber", "Error is %d : %s\n", - js->sasl_state, sasl_errdetail(js->sasl)); - purple_connection_error_reason(js->gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); - g_free(tmp); - return; - } else { - response = xmlnode_new("response"); - xmlnode_set_namespace(response, "urn:ietf:params:xml:ns:xmpp-sasl"); - if (clen > 0) { - /* Cyrus SASL 2.1.22 appears to contain code to add the charset - * to the response for DIGEST-MD5 but there is no possibility - * it will be executed. - * - * My reading of the digestmd5 plugin indicates the username and - * realm are always encoded in UTF-8 (they seem to be the values - * we pass in), so we need to ensure charset=utf-8 is set. - */ - if (!purple_strequal(js->current_mech, "DIGEST-MD5") || - strstr(c_out, ",charset=")) - /* If we're not using DIGEST-MD5 or Cyrus SASL is fixed */ - enc_out = purple_base64_encode((unsigned char*)c_out, clen); - else { - char *tmp = g_strdup_printf("%s,charset=utf-8", c_out); - enc_out = purple_base64_encode((unsigned char*)tmp, clen + 14); - g_free(tmp); - } - - xmlnode_insert_data(response, enc_out, -1); - g_free(enc_out); - } - jabber_send(js, response); - xmlnode_free(response); - } - } -#endif -} - -void jabber_auth_handle_success(JabberStream *js, xmlnode *packet) -{ - const char *ns = xmlnode_get_namespace(packet); -#ifdef HAVE_CYRUS_SASL - const void *x; -#endif - - if (!purple_strequal(ns, "urn:ietf:params:xml:ns:xmpp-sasl")) { + if (!purple_strequal(ns, NS_XMPP_SASL)) { purple_connection_error_reason(js->gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Invalid response from server")); return; } -#ifdef HAVE_CYRUS_SASL - /* The SASL docs say that if the client hasn't returned OK yet, we - * should try one more round against it - */ - if (js->sasl_state != SASL_OK) { - char *enc_in = xmlnode_get_data(packet); - unsigned char *dec_in = NULL; - const char *c_out; - unsigned int clen; - gsize declen = 0; + if (js->auth_mech && js->auth_mech->handle_challenge) { + xmlnode *response = NULL; + const char *msg = NULL; + JabberSaslState state = js->auth_mech->handle_challenge(js, packet, &response, &msg); + if (state == JABBER_SASL_STATE_FAIL) { + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, + msg ? msg : _("Invalid challenge from server")); + } else if (response) { + jabber_send(js, response); + xmlnode_free(response); + } + } else + purple_debug_warning("jabber", "Received unexpected (and unhandled) <challenge/>\n"); +} + +void jabber_auth_handle_success(JabberStream *js, xmlnode *packet) +{ + const char *ns = xmlnode_get_namespace(packet); - if(enc_in != NULL) - dec_in = purple_base64_decode(enc_in, &declen); + if (!purple_strequal(ns, NS_XMPP_SASL)) { + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Invalid response from server")); + return; + } - js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, NULL, &c_out, &clen); + if (js->auth_mech && js->auth_mech->handle_success) { + const char *msg = NULL; + JabberSaslState state = js->auth_mech->handle_success(js, packet, &msg); - g_free(enc_in); - g_free(dec_in); - - if (js->sasl_state != SASL_OK) { - /* This should never happen! */ + if (state == JABBER_SASL_STATE_FAIL) { purple_connection_error_reason(js->gc, - PURPLE_CONNECTION_ERROR_NETWORK_ERROR, - _("Invalid response from server")); - g_return_if_reached(); + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, + msg ? msg : _("Invalid response from server")); + return; + } else if (state == JABBER_SASL_STATE_CONTINUE) { + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, + msg ? msg : _("Server thinks authentication is complete, but client does not")); + return; } } - /* If we've negotiated a security layer, we need to enable it */ - if (js->sasl) { - sasl_getprop(js->sasl, SASL_SSF, &x); - if (*(int *)x > 0) { - sasl_getprop(js->sasl, SASL_MAXOUTBUF, &x); - js->sasl_maxbuf = *(int *)x; - } - } -#endif /* * The stream will be reinitialized later in jabber_recv_cb_ssl() or @@ -1097,27 +475,18 @@ PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR; char *msg; -#ifdef HAVE_CYRUS_SASL - if(js->auth_fail_count++ < 5) { - if (js->current_mech && *js->current_mech) { - char *pos; - if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) { - g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(js->current_mech)); - } - /* Remove space which separated this mech from the next */ - if ((js->sasl_mechs->str)[0] == ' ') { - g_string_erase(js->sasl_mechs, 0, 1); - } - } - if (*js->sasl_mechs->str) { - /* If we have remaining mechs to try, do so */ - sasl_dispose(&js->sasl); + if (js->auth_mech && js->auth_mech->handle_failure) { + xmlnode *stanza = NULL; + const char *msg = NULL; + JabberSaslState state = js->auth_mech->handle_failure(js, packet, &stanza, &msg); - jabber_auth_start_cyrus(js); + if (state != JABBER_SASL_STATE_FAIL && stanza) { + jabber_send(js, stanza); + xmlnode_free(stanza); return; } } -#endif + msg = jabber_parse_error(js, packet, &reason); if(!msg) { purple_connection_error_reason(js->gc, @@ -1128,3 +497,39 @@ g_free(msg); } } + +static gint compare_mech(gconstpointer a, gconstpointer b) +{ + const JabberSaslMech *mech_a = a; + const JabberSaslMech *mech_b = b; + + /* higher priority comes *before* lower priority in the list */ + if (mech_a->priority > mech_b->priority) + return -1; + else if (mech_a->priority < mech_b->priority) + return 1; + /* This really shouldn't happen */ + return 0; +} + +void jabber_auth_init(void) +{ + JabberSaslMech **tmp; + gint count, i; + + auth_mechs = g_slist_insert_sorted(auth_mechs, jabber_auth_get_plain_mech(), compare_mech); + auth_mechs = g_slist_insert_sorted(auth_mechs, jabber_auth_get_digest_md5_mech(), compare_mech); +#ifdef HAVE_CYRUS_SASL + auth_mechs = g_slist_insert_sorted(auth_mechs, jabber_auth_get_cyrus_mech(), compare_mech); +#endif + + tmp = jabber_auth_get_scram_mechs(&count); + for (i = 0; i < count; ++i) + auth_mechs = g_slist_insert_sorted(auth_mechs, tmp[i], compare_mech); +} + +void jabber_auth_uninit(void) +{ + g_slist_free(auth_mechs); + auth_mechs = NULL; +}
--- a/libpurple/protocols/jabber/auth.h Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/auth.h Thu Dec 03 17:00:54 2009 +0000 @@ -24,9 +24,27 @@ #ifndef PURPLE_JABBER_AUTH_H_ #define PURPLE_JABBER_AUTH_H_ +typedef struct _JabberSaslMech JabberSaslMech; + #include "jabber.h" #include "xmlnode.h" +typedef enum { + JABBER_SASL_STATE_FAIL = -1, /* Abort, Retry, Fail? */ + JABBER_SASL_STATE_OK = 0, /* Hooray! */ + JABBER_SASL_STATE_CONTINUE = 1 /* More authentication required */ +} JabberSaslState; + +struct _JabberSaslMech { + gint8 priority; /* Higher priority will be tried before lower priority */ + const gchar *name; + JabberSaslState (*start)(JabberStream *js, xmlnode *mechanisms, xmlnode **reply, const char **msg); + JabberSaslState (*handle_challenge)(JabberStream *js, xmlnode *packet, xmlnode **reply, const char **msg); + JabberSaslState (*handle_success)(JabberStream *js, xmlnode *packet, const char **msg); + JabberSaslState (*handle_failure)(JabberStream *js, xmlnode *packet, xmlnode **reply, const char **msg); + void (*dispose)(JabberStream *js); +}; + gboolean jabber_process_starttls(JabberStream *js, xmlnode *packet); void jabber_auth_start(JabberStream *js, xmlnode *packet); void jabber_auth_start_old(JabberStream *js); @@ -34,4 +52,14 @@ void jabber_auth_handle_success(JabberStream *js, xmlnode *packet); void jabber_auth_handle_failure(JabberStream *js, xmlnode *packet); +JabberSaslMech *jabber_auth_get_plain_mech(void); +JabberSaslMech *jabber_auth_get_digest_md5_mech(void); +JabberSaslMech **jabber_auth_get_scram_mechs(gint *count); +#ifdef HAVE_CYRUS_SASL +JabberSaslMech *jabber_auth_get_cyrus_mech(void); +#endif + +void jabber_auth_init(void); +void jabber_auth_uninit(void); + #endif /* PURPLE_JABBER_AUTH_H_ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/auth_cyrus.c Thu Dec 03 17:00:54 2009 +0000 @@ -0,0 +1,552 @@ +/* + * 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 "core.h" +#include "debug.h" +#include "request.h" + +#include "auth.h" +#include "jabber.h" + +static xmlnode *jabber_auth_start_cyrus(JabberStream *); +static void jabber_sasl_build_callbacks(JabberStream *); + +static void disallow_plaintext_auth(PurpleAccount *account) +{ + purple_connection_error_reason(purple_account_get_connection(account), + PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, + _("Server requires plaintext authentication over an unencrypted stream")); +} + +/* Callbacks for Cyrus SASL */ + +static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result) +{ + JabberStream *js = ctx; + + if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM; + + *result = js->user->domain; + + return SASL_OK; +} + +static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len) +{ + JabberStream *js = ctx; + + switch(id) { + case SASL_CB_AUTHNAME: + *res = js->user->node; + break; + case SASL_CB_USER: + *res = ""; + break; + default: + return SASL_BADPARAM; + } + if (len) *len = strlen((char *)*res); + return SASL_OK; +} + +static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret) +{ + JabberStream *js = ctx; + PurpleAccount *account; + const char *pw; + size_t len; + static sasl_secret_t *x = NULL; + + account = purple_connection_get_account(js->gc); + pw = purple_account_get_password(account); + + if (!conn || !secret || id != SASL_CB_PASS) + return SASL_BADPARAM; + + len = strlen(pw); + x = (sasl_secret_t *) realloc(x, sizeof(sasl_secret_t) + len); + + if (!x) + return SASL_NOMEM; + + x->len = len; + strcpy((char*)x->data, pw); + + *secret = x; + return SASL_OK; +} + +static void allow_cyrus_plaintext_auth(PurpleAccount *account) +{ + PurpleConnection *gc; + JabberStream *js; + xmlnode *response; + + gc = purple_account_get_connection(account); + js = purple_connection_get_protocol_data(gc); + + purple_account_set_bool(account, "auth_plain_in_clear", TRUE); + + response = jabber_auth_start_cyrus(js); + if (response) { + jabber_send(js, response); + xmlnode_free(response); + } +} + +static void auth_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) +{ + PurpleAccount *account; + JabberStream *js; + xmlnode *response; + const char *entry; + gboolean remember; + + /* The password prompt dialog doesn't get disposed if the account disconnects */ + if (!PURPLE_CONNECTION_IS_VALID(gc)) + return; + + account = purple_connection_get_account(gc); + js = purple_connection_get_protocol_data(gc); + + entry = purple_request_fields_get_string(fields, "password"); + remember = purple_request_fields_get_bool(fields, "remember"); + + if (!entry || !*entry) + { + purple_notify_error(account, NULL, _("Password is required to sign on."), NULL); + return; + } + + if (remember) + purple_account_set_remember_password(account, TRUE); + + purple_account_set_password(account, entry); + + /* Rebuild our callbacks as we now have a password to offer */ + jabber_sasl_build_callbacks(js); + + /* Restart our connection */ + response = jabber_auth_start_cyrus(js); + if (response) { + jabber_send(js, response); + xmlnode_free(response); + } +} + +static void +auth_no_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) +{ + PurpleAccount *account; + JabberStream *js; + + /* The password prompt dialog doesn't get disposed if the account disconnects */ + if (!PURPLE_CONNECTION_IS_VALID(gc)) + return; + + account = purple_connection_get_account(gc); + js = purple_connection_get_protocol_data(gc); + + /* Disable the account as the user has canceled connecting */ + purple_account_set_enabled(account, purple_core_get_ui(), FALSE); +} + +static xmlnode *jabber_auth_start_cyrus(JabberStream *js) +{ + PurpleAccount *account; + const char *clientout = NULL; + char *enc_out; + unsigned coutlen = 0; + xmlnode *auth; + sasl_security_properties_t secprops; + gboolean again; + gboolean plaintext = TRUE; + + /* Set up security properties and options */ + secprops.min_ssf = 0; + secprops.security_flags = SASL_SEC_NOANONYMOUS; + + account = purple_connection_get_account(js->gc); + + if (!jabber_stream_is_ssl(js)) { + secprops.max_ssf = -1; + secprops.maxbufsize = 4096; + plaintext = purple_account_get_bool(account, "auth_plain_in_clear", FALSE); + if (!plaintext) + secprops.security_flags |= SASL_SEC_NOPLAINTEXT; + } else { + secprops.max_ssf = 0; + secprops.maxbufsize = 0; + plaintext = TRUE; + } + secprops.property_names = 0; + secprops.property_values = 0; + + do { + again = FALSE; + + js->sasl_state = sasl_client_new("xmpp", js->serverFQDN, NULL, NULL, js->sasl_cb, 0, &js->sasl); + if (js->sasl_state==SASL_OK) { + sasl_setprop(js->sasl, SASL_SEC_PROPS, &secprops); + purple_debug_info("sasl", "Mechs found: %s\n", js->sasl_mechs->str); + js->sasl_state = sasl_client_start(js->sasl, js->sasl_mechs->str, NULL, &clientout, &coutlen, &js->current_mech); + } + switch (js->sasl_state) { + /* Success */ + case SASL_OK: + case SASL_CONTINUE: + break; + case SASL_NOMECH: + /* No mechanisms have offered to help */ + + /* Firstly, if we don't have a password try + * to get one + */ + + if (!purple_account_get_password(account)) { + purple_account_request_password(account, G_CALLBACK(auth_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc); + return NULL; + + /* If we've got a password, but aren't sending + * it in plaintext, see if we can turn on + * plaintext auth + */ + } else if (!plaintext) { + char *msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), + purple_account_get_username(account)); + purple_request_yes_no(js->gc, _("Plaintext Authentication"), + _("Plaintext Authentication"), + msg, + 1, account, NULL, NULL, account, + allow_cyrus_plaintext_auth, + disallow_plaintext_auth); + g_free(msg); + return NULL; + + } else { + /* We have no mechs which can work. + * Try falling back on the old jabber:iq:auth method. We get here if the server supports + * one or more sasl mechs, we are compiled with cyrus-sasl support, but we support or can connect with none of + * the offerred mechs. jabberd 2.0 w/ SASL and Apple's iChat Server 10.5 both handle and expect + * jabber:iq:auth in this situation. iChat Server in particular offers SASL GSSAPI by default, which is often + * not configured on the client side, and expects a fallback to jabber:iq:auth when it (predictably) fails. + * + * Note: xep-0078 points out that using jabber:iq:auth after a sasl failure is wrong. However, + * I believe this refers to actual authentication failure, not a simple lack of concordant mechanisms. + * Doing otherwise means that simply compiling with SASL support renders the client unable to connect to servers + * which would connect without issue otherwise. -evands + */ + js->auth_mech = NULL; + jabber_auth_start_old(js); + return NULL; + } + /* not reached */ + break; + + /* Fatal errors. Give up and go home */ + case SASL_BADPARAM: + case SASL_NOMEM: + break; + + /* For everything else, fail the mechanism and try again */ + default: + purple_debug_info("sasl", "sasl_state is %d, failing the mech and trying again\n", js->sasl_state); + + /* + * DAA: is this right? + * The manpage says that "mech" will contain the chosen mechanism on success. + * Presumably, if we get here that isn't the case and we shouldn't try again? + * I suspect that this never happens. + */ + /* + * SXW: Yes, this is right. What this handles is the situation where a + * mechanism, say GSSAPI, is tried. If that mechanism fails, it may be + * due to mechanism specific issues, so we want to try one of the other + * supported mechanisms. This code handles that case + */ + if (js->current_mech && *js->current_mech) { + char *pos; + if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) { + g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(js->current_mech)); + } + /* Remove space which separated this mech from the next */ + if ((js->sasl_mechs->str)[0] == ' ') { + g_string_erase(js->sasl_mechs, 0, 1); + } + again = TRUE; + } + + sasl_dispose(&js->sasl); + } + } while (again); + + if (js->sasl_state == SASL_CONTINUE || js->sasl_state == SASL_OK) { + auth = xmlnode_new("auth"); + xmlnode_set_namespace(auth, NS_XMPP_SASL); + xmlnode_set_attrib(auth, "mechanism", js->current_mech); + + xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth"); + xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true"); + + if (clientout) { + if (coutlen == 0) { + xmlnode_insert_data(auth, "=", -1); + } else { + enc_out = purple_base64_encode((unsigned char*)clientout, coutlen); + xmlnode_insert_data(auth, enc_out, -1); + g_free(enc_out); + } + } + + return auth; + } else { + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, + _("SASL authentication failed")); + + return NULL; + } +} + +static int +jabber_sasl_cb_log(void *context, int level, const char *message) +{ + if(level <= SASL_LOG_TRACE) + purple_debug_info("sasl", "%s\n", message); + + return SASL_OK; +} + +static void +jabber_sasl_build_callbacks(JabberStream *js) +{ + PurpleAccount *account; + int id; + + /* Set up our callbacks structure */ + if (js->sasl_cb == NULL) + js->sasl_cb = g_new0(sasl_callback_t,6); + + id = 0; + js->sasl_cb[id].id = SASL_CB_GETREALM; + js->sasl_cb[id].proc = jabber_sasl_cb_realm; + js->sasl_cb[id].context = (void *)js; + id++; + + js->sasl_cb[id].id = SASL_CB_AUTHNAME; + js->sasl_cb[id].proc = jabber_sasl_cb_simple; + js->sasl_cb[id].context = (void *)js; + id++; + + js->sasl_cb[id].id = SASL_CB_USER; + js->sasl_cb[id].proc = jabber_sasl_cb_simple; + js->sasl_cb[id].context = (void *)js; + id++; + + account = purple_connection_get_account(js->gc); + if (purple_account_get_password(account) != NULL ) { + js->sasl_cb[id].id = SASL_CB_PASS; + js->sasl_cb[id].proc = jabber_sasl_cb_secret; + js->sasl_cb[id].context = (void *)js; + id++; + } + + js->sasl_cb[id].id = SASL_CB_LOG; + js->sasl_cb[id].proc = jabber_sasl_cb_log; + js->sasl_cb[id].context = (void*)js; + id++; + + js->sasl_cb[id].id = SASL_CB_LIST_END; +} + +static xmlnode *jabber_cyrus_start(JabberStream *js, xmlnode *mechanisms) +{ + xmlnode *mechnode; + + js->sasl_mechs = g_string_new(""); + + for(mechnode = xmlnode_get_child(mechanisms, "mechanism"); mechnode; + mechnode = xmlnode_get_next_twin(mechnode)) + { + char *mech_name = xmlnode_get_data(mechnode); + + if (!mech_name || !*mech_name) { + g_free(mech_name); + continue; + } + + /* Don't include Google Talk's X-GOOGLE-TOKEN mechanism, as we will not + * support it and including it gives a false fall-back to other mechs offerred, + * leading to incorrect error handling. + */ + if (g_str_equal(mech_name, "X-GOOGLE-TOKEN")) { + g_free(mech_name); + continue; + } + + g_string_append(js->sasl_mechs, mech_name); + g_string_append_c(js->sasl_mechs, ' '); + g_free(mech_name); + } + + jabber_sasl_build_callbacks(js); + return jabber_auth_start_cyrus(js); +} + +static xmlnode *jabber_cyrus_handle_challenge(JabberStream *js, xmlnode *packet) +{ + char *enc_in = xmlnode_get_data(packet); + unsigned char *dec_in; + char *enc_out; + const char *c_out; + unsigned int clen; + gsize declen; + xmlnode *response = NULL; + + dec_in = purple_base64_decode(enc_in, &declen); + + js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, + NULL, &c_out, &clen); + g_free(enc_in); + g_free(dec_in); + if (js->sasl_state != SASL_CONTINUE && js->sasl_state != SASL_OK) { + gchar *tmp = g_strdup_printf(_("SASL error: %s"), + sasl_errdetail(js->sasl)); + purple_debug_error("jabber", "Error is %d : %s\n", + js->sasl_state, sasl_errdetail(js->sasl)); + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp); + g_free(tmp); + } else { + response = xmlnode_new("response"); + xmlnode_set_namespace(response, NS_XMPP_SASL); + if (clen > 0) { + /* Cyrus SASL 2.1.22 appears to contain code to add the charset + * to the response for DIGEST-MD5 but there is no possibility + * it will be executed. + * + * My reading of the digestmd5 plugin indicates the username and + * realm are always encoded in UTF-8 (they seem to be the values + * we pass in), so we need to ensure charset=utf-8 is set. + */ + if (!purple_strequal(js->current_mech, "DIGEST-MD5") || + strstr(c_out, ",charset=")) + /* If we're not using DIGEST-MD5 or Cyrus SASL is fixed */ + enc_out = purple_base64_encode((unsigned char*)c_out, clen); + else { + char *tmp = g_strdup_printf("%s,charset=utf-8", c_out); + enc_out = purple_base64_encode((unsigned char*)tmp, clen + 14); + g_free(tmp); + } + + xmlnode_insert_data(response, enc_out, -1); + g_free(enc_out); + } + } + + return response; +} + +static gboolean jabber_cyrus_handle_success(JabberStream *js, xmlnode *packet) +{ + const void *x; + + /* The SASL docs say that if the client hasn't returned OK yet, we + * should try one more round against it + */ + if (js->sasl_state != SASL_OK) { + char *enc_in = xmlnode_get_data(packet); + unsigned char *dec_in = NULL; + const char *c_out; + unsigned int clen; + gsize declen = 0; + + if(enc_in != NULL) + dec_in = purple_base64_decode(enc_in, &declen); + + js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, NULL, &c_out, &clen); + + g_free(enc_in); + g_free(dec_in); + + if (js->sasl_state != SASL_OK) { + /* This should never happen! */ + purple_connection_error_reason(js->gc, + PURPLE_CONNECTION_ERROR_NETWORK_ERROR, + _("Invalid response from server")); + g_return_val_if_reached(FALSE); + } + } + + /* If we've negotiated a security layer, we need to enable it */ + if (js->sasl) { + sasl_getprop(js->sasl, SASL_SSF, &x); + if (*(int *)x > 0) { + sasl_getprop(js->sasl, SASL_MAXOUTBUF, &x); + js->sasl_maxbuf = *(int *)x; + } + } + + return TRUE; +} + +static xmlnode *jabber_cyrus_handle_failure(JabberStream *js, xmlnode *packet) +{ + if (js->auth_fail_count++ < 5) { + if (js->current_mech && *js->current_mech) { + char *pos; + if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) { + g_string_erase(js->sasl_mechs, pos-js->sasl_mechs->str, strlen(js->current_mech)); + } + /* Remove space which separated this mech from the next */ + if ((js->sasl_mechs->str)[0] == ' ') { + g_string_erase(js->sasl_mechs, 0, 1); + } + } + if (*js->sasl_mechs->str) { + /* If we have remaining mechs to try, do so */ + sasl_dispose(&js->sasl); + + return jabber_auth_start_cyrus(js); + } + } + + /* Nothing to send */ + return NULL; +} + +static JabberSaslMech cyrus_mech = { + 100, /* priority */ + "*", /* name; Cyrus provides a bunch of mechanisms, so use an invalid + * mechanism name (per rfc4422 3.1). */ + jabber_cyrus_start, + jabber_cyrus_handle_challenge, + jabber_cyrus_handle_success, + jabber_cyrus_handle_failure, + NULL, +}; + +JabberSaslMech *jabber_auth_get_cyrus_mech(void) +{ + return &cyrus_mech; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/auth_digest_md5.c Thu Dec 03 17:00:54 2009 +0000 @@ -0,0 +1,292 @@ +/* + * 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 "debug.h" +#include "cipher.h" +#include "util.h" +#include "xmlnode.h" + +#include "auth.h" +#include "jabber.h" + +static JabberSaslState +digest_md5_start(JabberStream *js, xmlnode *packet, xmlnode **response, + const char **msg) +{ + xmlnode *auth = xmlnode_new("auth"); + xmlnode_set_namespace(auth, NS_XMPP_SASL); + xmlnode_set_attrib(auth, "mechanism", "DIGEST-MD5"); + + *response = auth; + return JABBER_SASL_STATE_CONTINUE; +} + +/* Parts of this algorithm are inspired by stuff in libgsasl */ +static GHashTable* parse_challenge(const char *challenge) +{ + const char *token_start, *val_start, *val_end, *cur; + GHashTable *ret = g_hash_table_new_full(g_str_hash, g_str_equal, + g_free, g_free); + + cur = challenge; + while(*cur != '\0') { + /* Find the end of the token */ + gboolean in_quotes = FALSE; + char *name, *value = NULL; + token_start = cur; + while(*cur != '\0' && (in_quotes || (!in_quotes && *cur != ','))) { + if (*cur == '"') + in_quotes = !in_quotes; + cur++; + } + + /* Find start of value. */ + val_start = strchr(token_start, '='); + if (val_start == NULL || val_start > cur) + val_start = cur; + + if (token_start != val_start) { + name = g_strndup(token_start, val_start - token_start); + + if (val_start != cur) { + val_start++; + while (val_start != cur && (*val_start == ' ' || *val_start == '\t' + || *val_start == '\r' || *val_start == '\n' + || *val_start == '"')) + val_start++; + + val_end = cur; + while (val_end != val_start && (*val_end == ' ' || *val_end == ',' || *val_end == '\t' + || *val_end == '\r' || *val_end == '\n' + || *val_end == '"' || *val_end == '\0')) + val_end--; + + if (val_start != val_end) + value = g_strndup(val_start, val_end - val_start + 1); + } + + g_hash_table_replace(ret, name, value); + } + + /* Find the start of the next token, if there is one */ + if (*cur != '\0') { + cur++; + while (*cur == ' ' || *cur == ',' || *cur == '\t' + || *cur == '\r' || *cur == '\n') + cur++; + } + } + + return ret; +} + +static char * +generate_response_value(JabberID *jid, const char *passwd, const char *nonce, + const char *cnonce, const char *a2, const char *realm) +{ + PurpleCipher *cipher; + PurpleCipherContext *context; + guchar result[16]; + size_t a1len; + + gchar *a1, *convnode=NULL, *convpasswd = NULL, *ha1, *ha2, *kd, *x, *z; + + if((convnode = g_convert(jid->node, -1, "iso-8859-1", "utf-8", + NULL, NULL, NULL)) == NULL) { + convnode = g_strdup(jid->node); + } + if(passwd && ((convpasswd = g_convert(passwd, -1, "iso-8859-1", + "utf-8", NULL, NULL, NULL)) == NULL)) { + convpasswd = g_strdup(passwd); + } + + cipher = purple_ciphers_find_cipher("md5"); + context = purple_cipher_context_new(cipher, NULL); + + x = g_strdup_printf("%s:%s:%s", convnode, realm, convpasswd ? convpasswd : ""); + purple_cipher_context_append(context, (const guchar *)x, strlen(x)); + purple_cipher_context_digest(context, sizeof(result), result, NULL); + + a1 = g_strdup_printf("xxxxxxxxxxxxxxxx:%s:%s", nonce, cnonce); + a1len = strlen(a1); + g_memmove(a1, result, 16); + + purple_cipher_context_reset(context, NULL); + purple_cipher_context_append(context, (const guchar *)a1, a1len); + purple_cipher_context_digest(context, sizeof(result), result, NULL); + + ha1 = purple_base16_encode(result, 16); + + purple_cipher_context_reset(context, NULL); + purple_cipher_context_append(context, (const guchar *)a2, strlen(a2)); + purple_cipher_context_digest(context, sizeof(result), result, NULL); + + ha2 = purple_base16_encode(result, 16); + + kd = g_strdup_printf("%s:%s:00000001:%s:auth:%s", ha1, nonce, cnonce, ha2); + + purple_cipher_context_reset(context, NULL); + purple_cipher_context_append(context, (const guchar *)kd, strlen(kd)); + purple_cipher_context_digest(context, sizeof(result), result, NULL); + purple_cipher_context_destroy(context); + + z = purple_base16_encode(result, 16); + + g_free(convnode); + g_free(convpasswd); + g_free(x); + g_free(a1); + g_free(ha1); + g_free(ha2); + g_free(kd); + + return z; +} + +static JabberSaslState +digest_md5_handle_challenge(JabberStream *js, xmlnode *packet, + xmlnode **response, const char **msg) +{ + xmlnode *reply = NULL; + char *enc_in = xmlnode_get_data(packet); + char *dec_in; + char *enc_out; + GHashTable *parts; + JabberSaslState state = JABBER_SASL_STATE_CONTINUE; + + if (!enc_in) { + *msg = _("Invalid response from server"); + return JABBER_SASL_STATE_FAIL; + } + + dec_in = (char *)purple_base64_decode(enc_in, NULL); + purple_debug_misc("jabber", "decoded challenge (%" + G_GSIZE_FORMAT "): %s\n", strlen(dec_in), dec_in); + + parts = parse_challenge(dec_in); + + if (g_hash_table_lookup(parts, "rspauth")) { + char *rspauth = g_hash_table_lookup(parts, "rspauth"); + + if (rspauth && purple_strequal(rspauth, js->expected_rspauth)) { + reply = xmlnode_new("response"); + xmlnode_set_namespace(reply, NS_XMPP_SASL); + } else { + *msg = _("Invalid challenge from server"); + state = JABBER_SASL_STATE_FAIL; + } + g_free(js->expected_rspauth); + js->expected_rspauth = NULL; + } else { + /* assemble a response, and send it */ + /* see RFC 2831 */ + char *realm; + char *nonce; + + /* Make sure the auth string contains everything that should be there. + This isn't everything in RFC2831, but it is what we need. */ + + nonce = g_hash_table_lookup(parts, "nonce"); + + /* we're actually supposed to prompt the user for a realm if + * the server doesn't send one, but that really complicates things, + * so i'm not gonna worry about it until is poses a problem to + * someone, or I get really bored */ + realm = g_hash_table_lookup(parts, "realm"); + if(!realm) + realm = js->user->domain; + + if (nonce == NULL || realm == NULL) { + *msg = _("Invalid challenge from server"); + state = JABBER_SASL_STATE_FAIL; + } else { + GString *response = g_string_new(""); + char *a2; + char *auth_resp; + char *cnonce; + + cnonce = g_strdup_printf("%x%u%x", g_random_int(), (int)time(NULL), + g_random_int()); + + a2 = g_strdup_printf("AUTHENTICATE:xmpp/%s", realm); + auth_resp = generate_response_value(js->user, + purple_connection_get_password(js->gc), nonce, cnonce, a2, realm); + g_free(a2); + + a2 = g_strdup_printf(":xmpp/%s", realm); + js->expected_rspauth = generate_response_value(js->user, + purple_connection_get_password(js->gc), nonce, cnonce, a2, realm); + g_free(a2); + + g_string_append_printf(response, "username=\"%s\"", js->user->node); + g_string_append_printf(response, ",realm=\"%s\"", realm); + g_string_append_printf(response, ",nonce=\"%s\"", nonce); + g_string_append_printf(response, ",cnonce=\"%s\"", cnonce); + g_string_append_printf(response, ",nc=00000001"); + g_string_append_printf(response, ",qop=auth"); + g_string_append_printf(response, ",digest-uri=\"xmpp/%s\"", realm); + g_string_append_printf(response, ",response=%s", auth_resp); + g_string_append_printf(response, ",charset=utf-8"); + + g_free(auth_resp); + g_free(cnonce); + + enc_out = purple_base64_encode((guchar *)response->str, response->len); + + purple_debug_misc("jabber", "decoded response (%" + G_GSIZE_FORMAT "): %s\n", + response->len, response->str); + + reply = xmlnode_new("response"); + xmlnode_set_namespace(reply, NS_XMPP_SASL); + xmlnode_insert_data(reply, enc_out, -1); + + g_free(enc_out); + + g_string_free(response, TRUE); + } + } + + g_free(enc_in); + g_free(dec_in); + g_hash_table_destroy(parts); + + *response = reply; + return state; +} + +static JabberSaslMech digest_md5_mech = { + 10, /* priority */ + "DIGEST-MD5", /* name */ + digest_md5_start, + digest_md5_handle_challenge, + NULL, /* handle_success */ + NULL, /* handle_failure */ + NULL /* handle_dispose */ +}; + +JabberSaslMech *jabber_auth_get_digest_md5_mech(void) +{ + return &digest_md5_mech; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/auth_plain.c Thu Dec 03 17:00:54 2009 +0000 @@ -0,0 +1,119 @@ +/* + * 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 "account.h" +#include "debug.h" +#include "request.h" +#include "util.h" +#include "xmlnode.h" + +#include "jabber.h" +#include "auth.h" + +static xmlnode *finish_plaintext_authentication(JabberStream *js) +{ + xmlnode *auth; + GString *response; + gchar *enc_out; + + auth = xmlnode_new("auth"); + xmlnode_set_namespace(auth, NS_XMPP_SASL); + + xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth"); + xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true"); + + response = g_string_new(""); + response = g_string_append_len(response, "\0", 1); + response = g_string_append(response, js->user->node); + response = g_string_append_len(response, "\0", 1); + response = g_string_append(response, + purple_connection_get_password(js->gc)); + + enc_out = purple_base64_encode((guchar *)response->str, response->len); + + xmlnode_set_attrib(auth, "mechanism", "PLAIN"); + xmlnode_insert_data(auth, enc_out, -1); + g_free(enc_out); + g_string_free(response, TRUE); + + return auth; +} + +static void allow_plaintext_auth(PurpleAccount *account) +{ + PurpleConnection *gc = purple_account_get_connection(account); + JabberStream *js = purple_connection_get_protocol_data(gc); + xmlnode *response; + + purple_account_set_bool(account, "auth_plain_in_clear", TRUE); + + response = finish_plaintext_authentication(js); + jabber_send(js, response); + xmlnode_free(response); +} + +static void disallow_plaintext_auth(PurpleAccount *account) +{ + purple_connection_error_reason(purple_account_get_connection(account), + PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, + _("Server requires plaintext authentication over an unencrypted stream")); +} + +static JabberSaslState +jabber_plain_start(JabberStream *js, xmlnode *packet, xmlnode **response, const char **error) +{ + PurpleAccount *account = purple_connection_get_account(js->gc); + char *msg; + + if (jabber_stream_is_ssl(js) || purple_account_get_bool(account, "auth_plain_in_clear", FALSE)) { + *response = finish_plaintext_authentication(js); + return JABBER_SASL_STATE_OK; + } + + msg = g_strdup_printf(_("%s requires plaintext authentication over an unencrypted connection. Allow this and continue authentication?"), + purple_account_get_username(account)); + purple_request_yes_no(js->gc, _("Plaintext Authentication"), + _("Plaintext Authentication"), + msg, + 1, + account, NULL, NULL, + account, allow_plaintext_auth, disallow_plaintext_auth); + g_free(msg); + return JABBER_SASL_STATE_CONTINUE; +} + +static JabberSaslMech plain_mech = { + 0, /* priority */ + "PLAIN", /* name */ + jabber_plain_start, + NULL, /* handle_challenge */ + NULL, /* handle_success */ + NULL, /* handle_failure */ + NULL /* dispose */ +}; + +JabberSaslMech *jabber_auth_get_plain_mech(void) +{ + return &plain_mech; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/auth_scram.c Thu Dec 03 17:00:54 2009 +0000 @@ -0,0 +1,570 @@ +/* + * 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 JabberScramHash hashes[] = { + { "-SHA-1", "sha1", 20 }, +}; + +static const JabberScramHash *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(hashes); ++i) { + if (strstr(mech, hashes[i].mech_substr)) + return &(hashes[i]); + } + + 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) +{ + PurpleCipherContext *context; + guchar *result; + guint i; + 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); + + prev = g_new0(guint8, hash->size); + tmp = g_new0(guint8, hash->size); + result = g_new0(guint8, hash->size); + + 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->name); + 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->size, result, NULL); + + memcpy(prev, result, hash->size); + + /* Compute U1...Ui */ + for (i = 1; i < iterations; ++i) { + guint j; + purple_cipher_context_set_option(context, "hash", (gpointer)hash->name); + purple_cipher_context_set_key_with_len(context, (guchar *)str->str, str->len); + purple_cipher_context_append(context, prev, hash->size); + purple_cipher_context_digest(context, hash->size, tmp, NULL); + + for (j = 0; j < hash->size; ++j) + result[j] ^= tmp[j]; + + memcpy(prev, tmp, hash->size); + } + + 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. All buffers must be of the appropriate size + * according to the JabberScramHash. + * + * "str" is a NULL-terminated string for hmac(). + * + * Needless to say, these are fragile. + */ +static void +hmac(const JabberScramHash *hash, 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->name); + purple_cipher_context_set_key_with_len(context, key, hash->size); + purple_cipher_context_append(context, (guchar *)str, strlen(str)); + purple_cipher_context_digest(context, hash->size, out, NULL); + purple_cipher_context_destroy(context); +} + +static void +hash(const JabberScramHash *hash, guchar *out, const guchar *data) +{ + PurpleCipherContext *context; + + context = purple_cipher_context_new_by_name(hash->name, NULL); + purple_cipher_context_append(context, data, hash->size); + purple_cipher_context_digest(context, hash->size, out, NULL); + purple_cipher_context_destroy(context); +} + +gboolean +jabber_scram_calc_proofs(JabberScramData *data, GString *salt, guint iterations) +{ + guint hash_len = data->hash->size; + 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); + + memset(pass->str, 0, pass->allocated_len); + g_string_free(pass, TRUE); + + if (!salted_password) + return FALSE; + + /* client_key = HMAC(salted_password, "Client Key") */ + hmac(data->hash, client_key, salted_password, "Client Key"); + /* server_key = HMAC(salted_password, "Server Key") */ + hmac(data->hash, server_key, salted_password, "Server Key"); + g_free(salted_password); + + /* stored_key = HASH(client_key) */ + hash(data->hash, stored_key, client_key); + + /* client_signature = HMAC(stored_key, auth_message) */ + hmac(data->hash, client_signature, stored_key, data->auth_message->str); + /* server_signature = HMAC(server_key, auth_message) */ + 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]; + + 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 (0 != strncmp(data->cnonce, token + 2, strlen(data->cnonce))) + 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); + if (salt) + 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; +} + +gboolean +jabber_scram_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, ','); + + /* "biws" is the base64 encoding of "n,,". I promise. */ + g_string_append_printf(data->auth_message, "c=%s,r=%s", "biws", 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", "biws", 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 gchar *escape_username(const gchar *in) +{ + gchar *tmp, *tmp2; + + tmp = purple_strreplace(in, "=", "=3D"); + tmp2 = purple_strreplace(tmp, ",", "=2D"); + g_free(tmp); + return tmp2; +} + +static JabberSaslState +scram_start(JabberStream *js, xmlnode *mechanisms, xmlnode **out, const char **error) +{ + xmlnode *reply; + JabberScramData *data; + guint64 cnonce; +#ifdef CHANNEL_BINDING + gboolean binding_supported = TRUE; +#endif + gchar *dec_out, *enc_out; + gchar *prepped_node, *tmp; + gchar *prepped_pass; + + prepped_node = jabber_saslprep(js->user->node); + if (!prepped_node) { + *error = _("Unable to canonicalize username"); + return JABBER_SASL_STATE_FAIL; + } + + tmp = escape_username(prepped_node); + g_free(prepped_node); + prepped_node = tmp; + + prepped_pass = jabber_saslprep(purple_connection_get_password(js->gc)); + if (!prepped_pass) { + g_free(prepped_node); + *error = _("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; + +#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", + prepped_node, data->cnonce); + g_free(prepped_node); + + data->step = 1; + + reply = xmlnode_new("auth"); + xmlnode_set_namespace(reply, 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); + + *out = reply; + return JABBER_SASL_STATE_CONTINUE; +} + +static JabberSaslState +scram_handle_challenge(JabberStream *js, xmlnode *challenge, xmlnode **out, const char **error) +{ + JabberScramData *data = js->auth_mech_data; + xmlnode *reply; + gchar *enc_in, *dec_in; + gchar *enc_out = NULL, *dec_out = NULL; + gsize len; + JabberSaslState state = JABBER_SASL_STATE_FAIL; + + enc_in = xmlnode_get_data(challenge); + if (!enc_in || *enc_in == '\0') { + reply = xmlnode_new("abort"); + xmlnode_set_namespace(reply, NS_XMPP_SASL); + data->step = -1; + *error = _("Invalid challenge from server"); + 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, NS_XMPP_SASL); + data->step = -1; + *error = _("Malicious challenge from server"); + goto out; + } + + purple_debug_misc("jabber", "decoded challenge: %s\n", dec_in); + + if (!jabber_scram_feed_parser(data, dec_in, &dec_out)) { + reply = xmlnode_new("abort"); + xmlnode_set_namespace(reply, NS_XMPP_SASL); + data->step = -1; + *error = _("Invalid challenge from server"); + goto out; + } + + data->step += 1; + + reply = xmlnode_new("response"); + xmlnode_set_namespace(reply, 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); + } + + state = JABBER_SASL_STATE_CONTINUE; + +out: + g_free(enc_out); + g_free(dec_out); + + *out = reply; + return state; +} + +static JabberSaslState +scram_handle_success(JabberStream *js, xmlnode *packet, const char **error) +{ + 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 JABBER_SASL_STATE_OK; + + if (data->step != 2) { + *error = _("Unexpected response from server"); + return JABBER_SASL_STATE_FAIL; + } + + 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); + *error = _("Invalid 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) { + g_free(dec_out); + *error = _("Invalid challenge from server"); + return JABBER_SASL_STATE_FAIL; + } + + /* Hooray */ + return JABBER_SASL_STATE_OK; +} + +void jabber_scram_data_destroy(JabberScramData *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); + if (data->password) { + memset(data->password, 0, strlen(data->password)); + g_free(data->password); + } + + g_free(data); +} + +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 = { + 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 + +JabberSaslMech **jabber_auth_get_scram_mechs(gint *count) +{ + static JabberSaslMech *mechs[] = { + &scram_sha1_mech, +#ifdef CHANNEL_BINDING + &scram_sha1_plus_mech, +#endif + }; + + *count = G_N_ELEMENTS(mechs); + return mechs; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/protocols/jabber/auth_scram.h Thu Dec 03 17:00:54 2009 +0000 @@ -0,0 +1,95 @@ +/** + * @file auth_scram.h Implementation of SASL-SCRAM authentication + * + * purple + * + * 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 + */ +#ifndef PURPLE_JABBER_AUTH_SCRAM_H_ +#define PURPLE_JABBER_AUTH_SCRAM_H_ + +/* + * Every function in this file is ONLY exposed for tests. + * DO NOT USE ANYTHING HERE OR YOU WILL BE SENT TO THE PIT OF DESPAIR. + */ + +/* Per-connection state stored between messages. + * This is stored in js->auth_data_mech. + */ +typedef struct { + const char *mech_substr; + const char *name; + guint size; +} JabberScramHash; + +typedef struct { + const JabberScramHash *hash; + char *cnonce; + GString *auth_message; + + GString *client_proof; + GString *server_signature; + + gchar *password; + gboolean channel_binding; + int step; +} JabberScramData; + +#include "auth.h" + +/** + * Implements the Hi() function as described in the SASL-SCRAM I-D. + * + * @param hash The struct corresponding to the hash function to be used. + * @param str The string to perform the PBKDF2 operation on. + * @param salt The salt. + * @param iterations The number of iterations to perform. + * + * @returns A newly allocated string containing the result. The string is + * NOT null-terminated and its length is the length of the binary + * output of the hash function in-use. + */ +guchar *jabber_scram_hi(const JabberScramHash *hash, const GString *str, + GString *salt, guint iterations); + +/** + * Calculates the proofs as described in Section 3 of the SASL-SCRAM I-D. + * + * @param data A JabberScramData structure. hash and auth_message must be + * set. client_proof and server_signature will be set as a result + * of this function. + * @param salt The salt (as specified by the server) + * @param iterations The number of iterations to perform. + * + * @returns TRUE if the proofs were successfully calculated. FALSE otherwise. + */ +gboolean jabber_scram_calc_proofs(JabberScramData *data, GString *salt, + guint iterations); + +/** + * Feed the algorithm with the data from the server. + */ +gboolean jabber_scram_feed_parser(JabberScramData *data, gchar *in, gchar **out); + +/** + * Clean up and destroy the data struct + */ +void jabber_scram_data_destroy(JabberScramData *data); + +#endif /* PURPLE_JABBER_AUTH_SCRAM_H_ */
--- a/libpurple/protocols/jabber/jabber.c Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/jabber.c Thu Dec 03 17:00:54 2009 +0000 @@ -234,8 +234,8 @@ * an auth feature with namespace http://jabber.org/features/iq-auth * we should revert back to iq:auth authentication, even though we're * connecting to an XMPP server. */ - js->auth_type = JABBER_AUTH_IQ_AUTH; jabber_stream_set_state(js, JABBER_STREAM_AUTHENTICATING); + jabber_auth_start_old(js); } } @@ -1510,6 +1510,8 @@ purple_circ_buffer_destroy(js->write_buffer); if(js->writeh) purple_input_remove(js->writeh); + if (js->auth_mech && js->auth_mech->dispose) + js->auth_mech->dispose(js); #ifdef HAVE_CYRUS_SASL if(js->sasl) sasl_dispose(&js->sasl); @@ -1586,11 +1588,6 @@ case JABBER_STREAM_AUTHENTICATING: purple_connection_update_progress(js->gc, _("Authenticating"), js->gsc ? 7 : 3, JABBER_CONNECT_STEPS); - if(js->protocol_version == JABBER_PROTO_0_9 && js->registration) { - jabber_register_start(js); - } else if(js->auth_type == JABBER_AUTH_IQ_AUTH) { - jabber_auth_start_old(js); - } break; case JABBER_STREAM_POST_AUTH: purple_connection_update_progress(js->gc, _("Re-initializing Stream"), @@ -3519,6 +3516,8 @@ jabber_add_feature(JINGLE_TRANSPORT_ICEUDP, 0); #endif + jabber_auth_init(); + /* IPC functions */ purple_plugin_ipc_register(plugin, "contact_has_feature", PURPLE_CALLBACK(jabber_ipc_contact_has_feature), purple_marshal_BOOLEAN__POINTER_POINTER_POINTER, @@ -3553,6 +3552,7 @@ { purple_plugin_ipc_unregister_all(plugin); + jabber_auth_uninit(); jabber_features_destroy(); jabber_identities_destroy(); }
--- a/libpurple/protocols/jabber/jabber.h Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/jabber.h Thu Dec 03 17:00:54 2009 +0000 @@ -66,6 +66,7 @@ #include "namespaces.h" +#include "auth.h" #include "iq.h" #include "jutil.h" #include "xmlnode.h" @@ -106,13 +107,9 @@ JABBER_PROTO_0_9, JABBER_PROTO_1_0 } protocol_version; - enum { - JABBER_AUTH_UNKNOWN, - JABBER_AUTH_DIGEST_MD5, - JABBER_AUTH_PLAIN, - JABBER_AUTH_IQ_AUTH, - JABBER_AUTH_CYRUS - } auth_type; + + JabberSaslMech *auth_mech; + gpointer auth_mech_data; char *stream_id; JabberStreamState state;
--- a/libpurple/protocols/jabber/jutil.c Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/jutil.c Thu Dec 03 17:00:54 2009 +0000 @@ -276,6 +276,42 @@ #endif /* USE_IDN */ } +char *jabber_saslprep(const char *in) +{ +#ifdef USE_IDN + char *out; + + g_return_val_if_fail(in != NULL, NULL); + g_return_val_if_fail(strlen(in) <= sizeof(idn_buffer) - 1, NULL); + + strncpy(idn_buffer, in, sizeof(idn_buffer) - 1); + idn_buffer[sizeof(idn_buffer) - 1] = '\0'; + + if (STRINGPREP_OK != stringprep(idn_buffer, sizeof(idn_buffer), 0, + stringprep_saslprep)) { + memset(idn_buffer, 0, sizeof(idn_buffer)); + return NULL; + } + + out = g_strdup(idn_buffer); + memset(idn_buffer, 0, sizeof(idn_buffer)); + return out; +#else /* USE_IDN */ + /* TODO: Something better than disallowing all non-ASCII characters */ + /* TODO: Is this even correct? */ + const guchar *c; + + c = (const guchar *)in; + while (*c) { + if (*c > 0x7f || + (*c < 0x20 && *c != '\t' && *c != '\n' && *c != '\r')) + return NULL; + } + + return g_strdup(in); +#endif /* USE_IDN */ +} + static JabberID* jabber_id_new_internal(const char *str, gboolean allow_terminating_slash) {
--- a/libpurple/protocols/jabber/jutil.h Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/jutil.h Thu Dec 03 17:00:54 2009 +0000 @@ -54,6 +54,15 @@ gboolean jabber_domain_validate(const char *); gboolean jabber_resourceprep_validate(const char *); +/** + * Apply the SASLprep profile of stringprep to the string passed in. + * + * @returns A newly allocated string containing the normalized version + * of the input, or NULL if an error occurred (the string could + * not be normalized) + */ +char *jabber_saslprep(const char *); + PurpleConversation *jabber_find_unnormalized_conv(const char *name, PurpleAccount *account); char *jabber_calculate_data_sha1sum(gconstpointer data, size_t len);
--- a/libpurple/protocols/jabber/parser.c Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/protocols/jabber/parser.c Thu Dec 03 17:00:54 2009 +0000 @@ -263,8 +263,8 @@ * the opening <stream:stream> and there was no version, we need to * immediately start legacy IQ auth. */ - js->auth_type = JABBER_AUTH_IQ_AUTH; jabber_stream_set_state(js, JABBER_STREAM_AUTHENTICATING); + jabber_auth_start_old(js); } }
--- a/libpurple/tests/Makefile.am Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/tests/Makefile.am Thu Dec 03 17:00:54 2009 +0000 @@ -11,6 +11,7 @@ tests.h \ test_cipher.c \ test_jabber_jutil.c \ + test_jabber_scram.c \ test_qq.c \ test_yahoo_util.c \ test_util.c \
--- a/libpurple/tests/check_libpurple.c Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/tests/check_libpurple.c Thu Dec 03 17:00:54 2009 +0000 @@ -76,6 +76,7 @@ srunner_add_suite(sr, cipher_suite()); srunner_add_suite(sr, jabber_jutil_suite()); + srunner_add_suite(sr, jabber_scram_suite()); srunner_add_suite(sr, qq_suite()); srunner_add_suite(sr, yahoo_util_suite()); srunner_add_suite(sr, util_suite());
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_jabber_scram.c Thu Dec 03 17:00:54 2009 +0000 @@ -0,0 +1,117 @@ +#include <string.h> + +#include "tests.h" +#include "../util.h" +#include "../protocols/jabber/auth_scram.h" +#include "../protocols/jabber/jutil.h" + +static JabberScramHash sha1_mech = { "-SHA-1", "sha1", 20 }; + +#define assert_pbkdf2_equal(password, salt, count, expected) { \ + GString *p = g_string_new(password); \ + GString *s = g_string_new(salt); \ + guchar *result = jabber_scram_hi(&sha1_mech, p, s, count); \ + fail_if(result == NULL, "Hi() returned NULL"); \ + fail_if(0 != memcmp(result, expected, 20), "Hi() returned invalid result"); \ + g_string_free(s, TRUE); \ + g_string_free(p, TRUE); \ +} + +START_TEST(test_pbkdf2) +{ + assert_pbkdf2_equal("password", "salt", 1, "\x0c\x60\xc8\x0f\x96\x1f\x0e\x71\xf3\xa9\xb5\x24\xaf\x60\x12\x06\x2f\xe0\x37\xa6"); + assert_pbkdf2_equal("password", "salt", 2, "\xea\x6c\x01\x4d\xc7\x2d\x6f\x8c\xcd\x1e\xd9\x2a\xce\x1d\x41\xf0\xd8\xde\x89\x57"); + assert_pbkdf2_equal("password", "salt", 4096, "\x4b\x00\x79\x01\xb7\x65\x48\x9a\xbe\xad\x49\xd9\x26\xf7\x21\xd0\x65\xa4\x29\xc1"); + +#if 0 + /* This causes libcheck to time out :-D */ + assert_pbkdf2_equal("password", "salt", 16777216, "\xee\xfe\x3d\x61\xcd\x4d\xa4\xe4\xe9\x94\x5b\x3d\x6b\xa2\x15\x8c\x26\x34\xe9\x84"); +#endif +} +END_TEST + +START_TEST(test_proofs) +{ + JabberScramData *data = g_new0(JabberScramData, 1); + gboolean ret; + GString *salt; + const char *client_proof; +/* const char *server_signature; */ + + data->hash = &sha1_mech; + data->password = g_strdup("password"); + data->auth_message = g_string_new("n=username@jabber.org,r=8jLxB5515dhFxBil5A0xSXMH," + "r=8jLxB5515dhFxBil5A0xSXMHabc,s=c2FsdA==,i=1," + "c=biws,r=8jLxB5515dhFxBil5A0xSXMHabc"); + client_proof = "\x48\x61\x30\xa5\x61\x0b\xae\xb9\xe4\x11\xa8\xfd\xa5\xcd\x34\x1d\x8a\x3c\x28\x17"; + + salt = g_string_new("salt"); + ret = jabber_scram_calc_proofs(data, salt, 1); + fail_if(ret == FALSE, "Failed to calculate SCRAM proofs!"); + + fail_unless(0 == memcmp(client_proof, data->client_proof->str, 20)); + g_string_free(salt, TRUE); + + jabber_scram_data_destroy(data); +} +END_TEST + +#define assert_successful_exchange(pw, nonce, start_data, challenge1, response1, success) { \ + JabberScramData *data = g_new0(JabberScramData, 1); \ + gboolean ret; \ + gchar *out; \ + \ + data->step = 1; \ + data->hash = &sha1_mech; \ + data->password = jabber_saslprep(pw); \ + fail_if(data->password == NULL); \ + data->cnonce = g_strdup(nonce); \ + data->auth_message = g_string_new(start_data); \ + \ + ret = jabber_scram_feed_parser(data, challenge1, &out); \ + fail_unless(ret == TRUE); \ + fail_unless(g_str_equal(out, response1), "Got unexpected response to challenge. Expected %s, got %s", response1, out); \ + g_free(out); \ + \ + data->step = 2; \ + ret = jabber_scram_feed_parser(data, success, &out); \ + fail_unless(ret == TRUE); \ + fail_unless(out == NULL); \ + \ + jabber_scram_data_destroy(data); \ +} + +START_TEST(test_mech) +{ + assert_successful_exchange("password", "H7yDYKAWBCrM2Fa5SxGa4iez", + "n=paul,r=H7yDYKAWBCrM2Fa5SxGa4iez", + "r=H7yDYKAWBCrM2Fa5SxGa4iezFPVDPpDUcGxPkH3RzP,s=3rXeErP/os7jUNqU,i=4096", + "c=biws,r=H7yDYKAWBCrM2Fa5SxGa4iezFPVDPpDUcGxPkH3RzP,p=pXkak78EuwwOEwk2/h/OzD7NkEI=", + "v=ldX4EBNnOgDnNTOCmbSfBHAUCOs="); + + assert_successful_exchange("pass½word", "GNb2HsNI7VnTv8ABsE5AnY8W", + "n=paul,r=GNb2HsNI7VnTv8ABsE5AnY8W", + "r=GNb2HsNI7VnTv8ABsE5AnY8W/w/I3eRKM0I7jxFWOH,s=ysAriUjPzFqOXnMQ,i=4096", + "c=biws,r=GNb2HsNI7VnTv8ABsE5AnY8W/w/I3eRKM0I7jxFWOH,p=n/CtgdWjOYnLQ4m9Na+wPn9D2uY=", + "v=4TkZwKWy6JHNmrUbU2+IdAaXtos="); +} +END_TEST + +Suite * +jabber_scram_suite(void) +{ + Suite *s = suite_create("Jabber SASL SCRAM functions"); + + TCase *tc = tcase_create("PBKDF2 Functionality"); + tcase_add_test(tc, test_pbkdf2); + suite_add_tcase(s, tc); + + tc = tcase_create("SCRAM Proofs"); + tcase_add_test(tc, test_proofs); + suite_add_tcase(s, tc); + + tc = tcase_create("SCRAM exchange"); + tcase_add_test(tc, test_mech); + suite_add_tcase(s, tc); + return s; +}
--- a/libpurple/tests/tests.h Thu Dec 03 08:02:54 2009 +0000 +++ b/libpurple/tests/tests.h Thu Dec 03 17:00:54 2009 +0000 @@ -10,6 +10,7 @@ Suite * master_suite(void); Suite * cipher_suite(void); Suite * jabber_jutil_suite(void); +Suite * jabber_scram_suite(void); Suite * qq_suite(void); Suite * yahoo_util_suite(void); Suite * util_suite(void);
--- a/po/POTFILES.in Thu Dec 03 08:02:54 2009 +0000 +++ b/po/POTFILES.in Thu Dec 03 17:00:54 2009 +0000 @@ -88,6 +88,9 @@ libpurple/protocols/irc/parse.c libpurple/protocols/jabber/adhoccommands.c libpurple/protocols/jabber/auth.c +libpurple/protocols/jabber/auth_cyrus.c +libpurple/protocols/jabber/auth_digest_md5.c +libpurple/protocols/jabber/auth_plain.c libpurple/protocols/jabber/bosh.c libpurple/protocols/jabber/buddy.c libpurple/protocols/jabber/chat.c