* purple - Jabber Protocol Plugin * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA static JabberSaslState jabber_auth_start_cyrus(JabberStream *js, PurpleXmlNode **reply, static void jabber_sasl_build_callbacks(JabberStream *); static void disallow_plaintext_auth(PurpleAccount *account) purple_connection_error(purple_account_get_connection(account), PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR, _("Server may require plaintext authentication over an unencrypted stream")); static void start_cyrus_wrapper(JabberStream *js) PurpleXmlNode *response = NULL; JabberSaslState state = jabber_auth_start_cyrus(js, &response, &error); if (state == JABBER_SASL_STATE_FAIL) { purple_connection_error(js->gc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, jabber_send(js, response); purple_xmlnode_free(response); /* Callbacks for Cyrus SASL */ static int jabber_sasl_cb_realm(void *ctx, int id, const char **avail, const char **result) if (id != SASL_CB_GETREALM || !result) return SASL_BADPARAM; *result = js->user->domain; static int jabber_sasl_cb_simple(void *ctx, int id, const char **res, unsigned *len) if (len) *len = strlen((char *)*res); static int jabber_sasl_cb_secret(sasl_conn_t *conn, void *ctx, int id, sasl_secret_t **secret) if (!conn || !secret || id != SASL_CB_PASS) len = strlen(js->sasl_password); /* Not an off-by-one because sasl_secret_t defines char data[1] */ /* TODO: This can probably be moved to glib's allocator */ js->sasl_secret = malloc(sizeof(sasl_secret_t) + len); js->sasl_secret->len = len; strcpy((char*)js->sasl_secret->data, js->sasl_password); *secret = js->sasl_secret; static void allow_cyrus_plaintext_auth(PurpleAccount *account) gc = purple_account_get_connection(account); js = purple_connection_get_protocol_data(gc); purple_account_set_bool(account, "auth_plain_in_clear", TRUE); static void auth_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) /* TODO: the password prompt dialog doesn't get disposed if the account disconnects */ PURPLE_ASSERT_CONNECTION_IS_VALID(gc); 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"); purple_notify_error(account, NULL, _("Password is required to sign on."), NULL, purple_request_cpar_from_connection(gc)); purple_account_set_remember_password(account, TRUE); purple_account_set_password(account, entry, NULL, NULL); js->sasl_password = g_strdup(entry); /* Rebuild our callbacks as we now have a password to offer */ jabber_sasl_build_callbacks(js); /* Restart our negotiation */ auth_no_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields) /* TODO: the password prompt dialog doesn't get disposed if the account disconnects */ PURPLE_ASSERT_CONNECTION_IS_VALID(gc); account = purple_connection_get_account(gc); /* Disable the account as the user has cancelled connecting */ purple_account_set_enabled(account, purple_core_get_ui(), FALSE); static gboolean remove_current_mech(JabberStream *js) { if ((pos = strstr(js->sasl_mechs->str, js->current_mech))) { size_t len = strlen(js->current_mech); /* Clean up space that separated this Mech from the one before or after it */ if (pos > js->sasl_mechs->str && *(pos - 1) == ' ') { /* Handle removing space before when current_mech isn't the first mech in the list */ } else if (strlen(pos) > len && *(pos + len) == ' ') { /* Handle removing space after */ g_string_erase(js->sasl_mechs, pos - js->sasl_mechs->str, len); jabber_auth_start_cyrus(JabberStream *js, PurpleXmlNode **reply, char **error) const char *clientout = NULL; sasl_security_properties_t secprops; gboolean plaintext = TRUE; /* Set up security properties and options */ secprops.security_flags = SASL_SEC_NOANONYMOUS; account = purple_connection_get_account(js->gc); if (!jabber_stream_is_ssl(js)) { secprops.maxbufsize = 4096; plaintext = purple_account_get_bool(account, "auth_plain_in_clear", FALSE); secprops.security_flags |= SASL_SEC_NOPLAINTEXT; secprops.property_names = 0; secprops.property_values = 0; 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) { /* No mechanisms have offered to help */ /* Firstly, if we don't have a password try if (!js->sasl_password) { purple_account_request_password(account, G_CALLBACK(auth_pass_cb), G_CALLBACK(auth_no_pass_cb), js->gc); return JABBER_SASL_STATE_CONTINUE; /* If we've got a password, but aren't sending * it in plaintext, see if we can turn on /* XXX Should we just check for PLAIN/LOGIN being offered mechanisms? */ char *msg = g_strdup_printf(_("%s may require 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"), 1, purple_request_cpar_from_account(account), account, allow_cyrus_plaintext_auth, disallow_plaintext_auth); return JABBER_SASL_STATE_CONTINUE; if (js->auth_fail_count == 1 && purple_strequal(js->sasl_mechs->str, "GSSAPI")) { /* If we tried GSSAPI first, it failed, and it was the only method we had to try, try jabber:iq:auth * for compatibility with iChat 10.5 Server and other jabberd based servers. * iChat Server 10.5 and certain other corporate servers offer 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 jabber_auth_start_old(js); return JABBER_SASL_STATE_CONTINUE; /* Fatal errors. Give up and go home */ *error = g_strdup(_("SASL authentication failed")); /* For everything else, fail the mechanism and try again */ purple_debug_info("sasl", "sasl_state is %d, failing the mech and trying again\n", js->sasl_state); * 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) { /* Should we only try again if we've removed the mech? */ if (js->sasl_state == SASL_CONTINUE || js->sasl_state == SASL_OK) { PurpleXmlNode *auth = purple_xmlnode_new("auth"); purple_xmlnode_set_namespace(auth, NS_XMPP_SASL); purple_xmlnode_set_attrib(auth, "mechanism", js->current_mech); purple_xmlnode_set_attrib(auth, "xmlns:ga", "http://www.google.com/talk/protocol/auth"); purple_xmlnode_set_attrib(auth, "ga:client-uses-full-bind-result", "true"); purple_xmlnode_insert_data(auth, "=", -1); enc_out = g_base64_encode((unsigned char*)clientout, coutlen); purple_xmlnode_insert_data(auth, enc_out, -1); return JABBER_SASL_STATE_CONTINUE; return JABBER_SASL_STATE_FAIL; jabber_sasl_cb_log(void *context, int level, const char *message) if(level <= SASL_LOG_TRACE) purple_debug_info("sasl", "%s\n", message); jabber_sasl_build_callbacks(JabberStream *js) /* Set up our callbacks structure */ js->sasl_cb = g_new0(sasl_callback_t,6); js->sasl_cb[id].id = SASL_CB_GETREALM; js->sasl_cb[id].proc = (void *)jabber_sasl_cb_realm; js->sasl_cb[id].context = (void *)js; js->sasl_cb[id].id = SASL_CB_AUTHNAME; js->sasl_cb[id].proc = (void *)jabber_sasl_cb_simple; js->sasl_cb[id].context = (void *)js; js->sasl_cb[id].id = SASL_CB_USER; js->sasl_cb[id].proc = (void *)jabber_sasl_cb_simple; js->sasl_cb[id].context = (void *)js; if (js->sasl_password != NULL) { js->sasl_cb[id].id = SASL_CB_PASS; js->sasl_cb[id].proc = (void *)jabber_sasl_cb_secret; js->sasl_cb[id].context = (void *)js; js->sasl_cb[id].id = SASL_CB_LOG; js->sasl_cb[id].proc = (void *)jabber_sasl_cb_log; js->sasl_cb[id].context = (void*)js; js->sasl_cb[id].id = SASL_CB_LIST_END; jabber_cyrus_start(JabberStream *js, PurpleXmlNode *mechanisms, PurpleXmlNode **reply, char **error) js->sasl_mechs = g_string_new(""); js->sasl_password = g_strdup(purple_connection_get_password(js->gc)); for(mechnode = purple_xmlnode_get_child(mechanisms, "mechanism"); mechnode; mechnode = purple_xmlnode_get_next_twin(mechnode)) char *mech_name = purple_xmlnode_get_data(mechnode); /* Ignore blank mechanisms and EXTERNAL. External isn't * supported, and Cyrus SASL's mechanism returns * SASL_NOMECH when the caller (us) doesn't configure it. * Except SASL_NOMECH is supposed to mean "no concordant * mechanisms"... Easiest just to blacklist it (for now). if (!mech_name || !*mech_name || purple_strequal(mech_name, "EXTERNAL")) { g_string_append(js->sasl_mechs, mech_name); g_string_append_c(js->sasl_mechs, ' '); /* Strip off the trailing ' ' */ if (js->sasl_mechs->len > 1) g_string_truncate(js->sasl_mechs, js->sasl_mechs->len - 1); jabber_sasl_build_callbacks(js); ret = jabber_auth_start_cyrus(js, reply, error); * Triggered if no overlap between server and client if (ret == JABBER_SASL_STATE_FAIL && *error == NULL) *error = g_strdup(_("Server does not use any supported authentication method")); jabber_cyrus_handle_challenge(JabberStream *js, PurpleXmlNode *packet, PurpleXmlNode **reply, char **error) char *enc_in = purple_xmlnode_get_data(packet); dec_in = g_base64_decode(enc_in, &declen); js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, 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)); return JABBER_SASL_STATE_FAIL; PurpleXmlNode *response = purple_xmlnode_new("response"); purple_xmlnode_set_namespace(response, NS_XMPP_SASL); /* Cyrus SASL 2.1.22 appears to contain code to add the charset * to the response for DIGEST-MD5 but there is no possibility * 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 = g_base64_encode((unsigned char*)c_out, clen); char *tmp = g_strdup_printf("%s,charset=utf-8", c_out); enc_out = g_base64_encode((unsigned char*)tmp, clen + 14); purple_xmlnode_insert_data(response, enc_out, -1); return JABBER_SASL_STATE_CONTINUE; jabber_cyrus_handle_success(JabberStream *js, PurpleXmlNode *packet, /* 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 = purple_xmlnode_get_data(packet); unsigned char *dec_in = NULL; dec_in = g_base64_decode(enc_in, &declen); js->sasl_state = sasl_client_step(js->sasl, (char*)dec_in, declen, NULL, &c_out, &clen); if (js->sasl_state != SASL_OK) { /* This happens when the server sends back jibberish * in the "additional data with success" case. * Seen with Wildfire 3.0.1. *error = g_strdup(_("Invalid response from server")); return JABBER_SASL_STATE_FAIL; /* If we've negotiated a security layer, we need to enable it */ sasl_getprop(js->sasl, SASL_SSF, &x); sasl_getprop(js->sasl, SASL_MAXOUTBUF, &x); js->sasl_maxbuf = *(int *)x; return JABBER_SASL_STATE_OK; jabber_cyrus_handle_failure(JabberStream *js, PurpleXmlNode *packet, PurpleXmlNode **reply, char **error) if (js->auth_fail_count++ < 5) { if (js->current_mech && *js->current_mech) { /* Should we only try again if we've actually removed a mech? */ if (*js->sasl_mechs->str) { /* If we have remaining mechs to try, do so */ return jabber_auth_start_cyrus(js, reply, error); } else if ((js->auth_fail_count == 1) && purple_strequal(js->current_mech, "GSSAPI")) { /* If we tried GSSAPI first, it failed, and it was the only method we had to try, try jabber:iq:auth * for compatibility with iChat 10.5 Server and other jabberd based servers. * iChat Server 10.5 and certain other corporate servers offer 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 jabber_auth_start_old(js); return JABBER_SASL_STATE_CONTINUE; return JABBER_SASL_STATE_FAIL; static JabberSaslMech cyrus_mech = { "*", /* name; Cyrus provides a bunch of mechanisms, so use an invalid * mechanism name (per rfc4422 3.1). */ jabber_cyrus_handle_challenge, jabber_cyrus_handle_success, jabber_cyrus_handle_failure, JabberSaslMech *jabber_auth_get_cyrus_mech(void)