pidgin/pidgin

closing merged branch
port-changes-from-branch-2.x.y-to-default
2020-02-03, Gary Kramlich
2f836435c33c
closing merged branch
/*
* 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 JabberSaslState jabber_auth_start_cyrus(JabberStream *js, PurpleXmlNode **reply,
char **error);
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)
{
char *error = NULL;
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,
error);
g_free(error);
} else if (response) {
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)
{
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;
size_t len;
if (!conn || !secret || id != SASL_CB_PASS)
return SASL_BADPARAM;
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);
if (!js->sasl_secret)
return SASL_NOMEM;
js->sasl_secret->len = len;
strcpy((char*)js->sasl_secret->data, js->sasl_password);
*secret = js->sasl_secret;
return SASL_OK;
}
static void allow_cyrus_plaintext_auth(PurpleAccount *account)
{
PurpleConnection *gc;
JabberStream *js;
gc = purple_account_get_connection(account);
js = purple_connection_get_protocol_data(gc);
purple_account_set_bool(account, "auth_plain_in_clear", TRUE);
start_cyrus_wrapper(js);
}
static void auth_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields)
{
PurpleAccount *account;
JabberStream *js;
const char *entry;
gboolean remember;
/* 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");
if (!entry || !*entry)
{
purple_notify_error(account, NULL,
_("Password is required to sign on."), NULL,
purple_request_cpar_from_connection(gc));
return;
}
if (remember)
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 */
start_cyrus_wrapper(js);
}
static void
auth_no_pass_cb(PurpleConnection *gc, PurpleRequestFields *fields)
{
PurpleAccount *account;
/* 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) {
char *pos;
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 */
pos--;
len++;
} else if (strlen(pos) > len && *(pos + len) == ' ') {
/* Handle removing space after */
len++;
}
g_string_erase(js->sasl_mechs, pos - js->sasl_mechs->str, len);
return TRUE;
}
return FALSE;
}
static JabberSaslState
jabber_auth_start_cyrus(JabberStream *js, PurpleXmlNode **reply, char **error)
{
PurpleAccount *account;
const char *clientout = NULL;
char *enc_out;
unsigned coutlen = 0;
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 (!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
* plaintext auth
*/
/* XXX Should we just check for PLAIN/LOGIN being offered mechanisms? */
} else if (!plaintext) {
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"),
msg,
1, purple_request_cpar_from_account(account), account,
allow_cyrus_plaintext_auth,
disallow_plaintext_auth);
g_free(msg);
return JABBER_SASL_STATE_CONTINUE;
} else
js->auth_fail_count++;
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
*/
js->auth_mech = NULL;
jabber_auth_start_old(js);
return JABBER_SASL_STATE_CONTINUE;
}
break;
/* Fatal errors. Give up and go home */
case SASL_BADPARAM:
case SASL_NOMEM:
*error = g_strdup(_("SASL authentication failed"));
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);
js->auth_fail_count++;
/*
* 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) {
remove_current_mech(js);
/* Should we only try again if we've removed the mech? */
again = TRUE;
}
sasl_dispose(&js->sasl);
}
} while (again);
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");
if (clientout) {
if (coutlen == 0) {
purple_xmlnode_insert_data(auth, "=", -1);
} else {
enc_out = g_base64_encode((unsigned char*)clientout, coutlen);
purple_xmlnode_insert_data(auth, enc_out, -1);
g_free(enc_out);
}
}
*reply = auth;
return JABBER_SASL_STATE_CONTINUE;
} else {
return JABBER_SASL_STATE_FAIL;
}
}
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)
{
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 = (void *)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 = (void *)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 = (void *)jabber_sasl_cb_simple;
js->sasl_cb[id].context = (void *)js;
id++;
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;
id++;
}
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;
id++;
js->sasl_cb[id].id = SASL_CB_LIST_END;
}
static JabberSaslState
jabber_cyrus_start(JabberStream *js, PurpleXmlNode *mechanisms,
PurpleXmlNode **reply, char **error)
{
PurpleXmlNode *mechnode, *hostname;
JabberSaslState ret;
js->sasl_mechs = g_string_new("");
js->sasl_password = g_strdup(purple_connection_get_password(js->gc));
/* XEP-0233 says we should grab the hostname for Kerberos v5, but there
* is no claim about other SASL mechanisms. Fortunately, most don't
* care what we use, so just use the domainpart. */
hostname = purple_xmlnode_get_child_with_namespace(
mechanisms, "hostname", NS_XMPP_SERVER_REGISTRATION);
if (hostname) {
js->serverFQDN = purple_xmlnode_get_data(hostname);
}
if (js->serverFQDN == NULL) {
js->serverFQDN = g_strdup(js->user->domain);
}
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_free(mech_name);
continue;
}
g_string_append(js->sasl_mechs, mech_name);
g_string_append_c(js->sasl_mechs, ' ');
g_free(mech_name);
}
/* 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
* supported mechanisms.
*/
if (ret == JABBER_SASL_STATE_FAIL && *error == NULL)
*error = g_strdup(_("Server does not use any supported authentication method"));
return ret;
}
static JabberSaslState
jabber_cyrus_handle_challenge(JabberStream *js, PurpleXmlNode *packet,
PurpleXmlNode **reply, char **error)
{
char *enc_in = purple_xmlnode_get_data(packet);
unsigned char *dec_in;
char *enc_out;
const char *c_out;
unsigned int clen;
gsize declen;
dec_in = g_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));
*error = tmp;
return JABBER_SASL_STATE_FAIL;
} else {
PurpleXmlNode *response = purple_xmlnode_new("response");
purple_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 = g_base64_encode((unsigned char*)c_out, clen);
else {
char *tmp = g_strdup_printf("%s,charset=utf-8", c_out);
enc_out = g_base64_encode((unsigned char*)tmp, clen + 14);
g_free(tmp);
}
purple_xmlnode_insert_data(response, enc_out, -1);
g_free(enc_out);
}
*reply = response;
return JABBER_SASL_STATE_CONTINUE;
}
}
static JabberSaslState
jabber_cyrus_handle_success(JabberStream *js, PurpleXmlNode *packet,
char **error)
{
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 = purple_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 = g_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 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 */
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 JABBER_SASL_STATE_OK;
}
static JabberSaslState
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) {
remove_current_mech(js);
}
/* 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 */
sasl_dispose(&js->sasl);
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
*/
sasl_dispose(&js->sasl);
js->sasl = NULL;
js->auth_mech = NULL;
jabber_auth_start_old(js);
return JABBER_SASL_STATE_CONTINUE;
}
}
/* Nothing to send */
return JABBER_SASL_STATE_FAIL;
}
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;
}