Wed, 08 Feb 2023 06:17:57 -0600
IRCv3: Add an account option to specify the SASL mechanisms
This allows the user to only try the SASL mechanism they want to use instead of
potentially trying a few that won't work based on what the server advertised.
This also allows us to use SASL mechanisms that the server supports but doesn't
advertise for some reason.
Testing Done:
Set the field to `SCRAM-SHA-256` and connected to my local ergo. I verified that ONLY `SCRAM-SHA-256` was attempted (and failed see [PIDGIN-17744](https://issues.imfreedom.org/issue/PIDGIN-17744).
I deleted the value from the entry as well as from accounts.xml and verified that the server advertised values were what were used.
Bugs closed: PIDGIN-17740
Reviewed at https://reviews.imfreedom.org/r/2222/
/* * Purple - Internet Messaging Library * Copyright (C) Pidgin Developers <devel@pidgin.im> * * 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, see <https://www.gnu.org/licenses/>. */ #include <glib/gi18n-lib.h> #include <gsasl.h> #include "purpleircv3sasl.h" #include "purpleircv3capabilities.h" #include "purpleircv3connection.h" #include "purpleircv3core.h" #define PURPLE_IRCV3_SASL_DATA_KEY ("sasl-data") typedef struct { PurpleConnection *connection; Gsasl *ctx; Gsasl_session *session; char *current_mechanism; GString *mechanisms; GString *server_in_buffer; } PurpleIRCv3SASLData; /****************************************************************************** * Helpers *****************************************************************************/ static const char * purple_ircv3_sasl_get_username(PurpleConnection *connection) { PurpleAccount *account = NULL; const char *username = NULL; account = purple_connection_get_account(connection); username = purple_account_get_string(account, "sasl-login-name", ""); if(username != NULL && username[0] != '\0') { return username; } return purple_connection_get_display_name(connection); } /****************************************************************************** * SASL Callbacks *****************************************************************************/ static int purple_ircv3_sasl_callback(G_GNUC_UNUSED Gsasl *ctx, Gsasl_session *session, Gsasl_property property) { PurpleIRCv3SASLData *data = NULL; int res = GSASL_NO_CALLBACK; data = gsasl_session_hook_get(session); switch(property) { case GSASL_AUTHID: gsasl_property_set(session, GSASL_AUTHID, purple_ircv3_sasl_get_username(data->connection)); res = GSASL_OK; break; case GSASL_AUTHZID: /* AUTHZID is typically set to empty string because it's the user * logging in on their own behalf. Since IRCv3 doesn't really let * an admin login as a normal user, we always set it to empty * string. * * See https://www.gnu.org/software/gsasl/manual/gsasl.html#PLAIN * for further explanation. */ gsasl_property_set(session, GSASL_AUTHZID, ""); res = GSASL_OK; break; case GSASL_PASSWORD: gsasl_property_set(session, GSASL_PASSWORD, purple_connection_get_password(data->connection)); res = GSASL_OK; break; default: g_warning("Unknown property %d", property); break; } return res; } /****************************************************************************** * SASL Helpers *****************************************************************************/ static void purple_ircv3_sasl_connection_error(PurpleConnection *connection, int res, int err, const char *msg) { GError *error = NULL; error = g_error_new(PURPLE_CONNECTION_ERROR, err, "%s: %s", msg, gsasl_strerror(res)); purple_connection_take_error(connection, error); } static void purple_ircv3_sasl_data_free(PurpleIRCv3SASLData *data) { g_clear_object(&data->connection); g_clear_pointer(&data->session, gsasl_finish); g_clear_pointer(&data->ctx, gsasl_done); g_clear_pointer(&data->current_mechanism, g_free); g_string_free(data->mechanisms, TRUE); data->mechanisms = NULL; g_free(data); } static void purple_ircv3_sasl_data_add(PurpleConnection *connection, Gsasl *ctx, const char *mechanisms) { PurpleIRCv3SASLData *data = NULL; GStrv parts = NULL; data = g_new0(PurpleIRCv3SASLData, 1); g_object_set_data_full(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY, data, (GDestroyNotify)purple_ircv3_sasl_data_free); /* We don't reference this because the life cycle of this data is tied * directly to the connection and adding a reference to the connection * would keep both alive forever. */ data->connection = connection; data->ctx = ctx; gsasl_callback_set(data->ctx, purple_ircv3_sasl_callback); /* Create a GString for the mechanisms with a leading and trailing ` `. * This is so we can easily remove attempted mechanism by removing * ` <attempted_mechanism> ` from the string which will make sure we're * always removing the proper mechanism. This is necessary because some * mechanisms have the same prefix, and others have the same suffix, which * could lead to incorrect removals. * * For example, if the list contains `EAP-AES128-PLUS EAP-AES128` and we * try `EAP-AES128` first, that means we would remove `EAP-AES128` from the * list which would first find `EAP-AES128-PLUS` and replace it with an * empty string, leaving the list as `-PLUS EAP-AES128` which is obviously * wrong. Instead the additional spaces mean our replace can be * ` EAP-AES128 `, which will get the proper value and update the * list to just contain ` EAP-AES128-PLUS `. * * For a list of mechanisms see * https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml * */ data->mechanisms = g_string_new(""); parts = g_strsplit(mechanisms, ",", -1); for(int i = 0; parts[i] != NULL; i++) { g_string_append_printf(data->mechanisms, " %s ", parts[i]); } g_strfreev(parts); } static void purple_ircv3_sasl_attempt_mechanism(PurpleIRCv3Connection *connection, PurpleIRCv3SASLData *data, const char *next_mechanism) { int res = GSASL_OK; g_free(data->current_mechanism); data->current_mechanism = g_strdup(next_mechanism); res = gsasl_client_start(data->ctx, next_mechanism, &data->session); if(res != GSASL_OK) { purple_ircv3_sasl_connection_error(PURPLE_CONNECTION(connection), res, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, _("Failed to setup SASL client")); return; } gsasl_session_hook_set(data->session, data); purple_ircv3_connection_writef(connection, "AUTHENTICATE %s", next_mechanism); } static void purple_ircv3_sasl_attempt(PurpleIRCv3Connection *connection) { PurpleIRCv3SASLData *data = NULL; PurpleAccount *account = NULL; const char *next_mechanism = NULL; gboolean allow_plain = TRUE; gboolean good_mechanism = FALSE; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); /* If this is not our first attempt, remove the previous mechanism from the * list of mechanisms to try. */ if(data->current_mechanism != NULL) { char *to_remove = g_strdup_printf(" %s ", data->current_mechanism); g_message("SASL '%s' mechanism failed", data->current_mechanism); g_string_replace(data->mechanisms, to_remove, "", 0); g_free(to_remove); } account = purple_connection_get_account(PURPLE_CONNECTION(connection)); if(!purple_account_get_bool(account, "use-tls", TRUE)) { if(!purple_account_get_bool(account, "plain-sasl-in-clear", FALSE)) { allow_plain = FALSE; } } while(!good_mechanism) { good_mechanism = TRUE; next_mechanism = gsasl_client_suggest_mechanism(data->ctx, data->mechanisms->str); if(next_mechanism == NULL) { GError *error = g_error_new(PURPLE_CONNECTION_ERROR, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, _("No valid SASL mechanisms found")); purple_connection_take_error(PURPLE_CONNECTION(connection), error); return; } if(purple_strequal(next_mechanism, "PLAIN") && !allow_plain) { g_message("skipping SASL 'PLAIN' as it's not allowed without tls"); good_mechanism = FALSE; g_string_replace(data->mechanisms, " PLAIN ", "", 0); } } g_message("trying SASL '%s' mechanism", next_mechanism); purple_ircv3_sasl_attempt_mechanism(connection, data, next_mechanism); } static void purple_ircv3_sasl_start(PurpleIRCv3Capabilities *caps) { PurpleIRCv3Connection *connection = NULL; PurpleAccount *account = NULL; PurpleConnection *purple_connection = NULL; Gsasl *ctx = NULL; const char *mechanisms = NULL; gint res; connection = purple_ircv3_capabilities_get_connection(caps); purple_connection = PURPLE_CONNECTION(connection); account = purple_connection_get_account(purple_connection); res = gsasl_init(&ctx); if(res != GSASL_OK) { purple_ircv3_sasl_connection_error(purple_connection, res, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, _("Failed to initialize SASL")); return; } /* At this point we are ready to start our sasl negotiation, so add a wait * counter to the capabilities and start the negotiations! */ purple_ircv3_capabilities_add_wait(caps); mechanisms = purple_account_get_string(account, "sasl-mechanisms", ""); if(purple_strempty(mechanisms)) { /* If the user didn't specify any mechanisms, grab the mechanisms that * the server advertised. */ mechanisms = purple_ircv3_capabilities_lookup(caps, "sasl", NULL); } /* Create our SASLData object, add it to the connection. */ purple_ircv3_sasl_data_add(purple_connection, ctx, mechanisms); /* Make it go! */ purple_ircv3_sasl_attempt(connection); } /****************************************************************************** * Callbacks *****************************************************************************/ static void purple_ircv3_sasl_ack_cb(PurpleIRCv3Capabilities *caps, G_GNUC_UNUSED const char *capability, G_GNUC_UNUSED gpointer data) { purple_ircv3_sasl_start(caps); } /****************************************************************************** * Internal API *****************************************************************************/ void purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities) { purple_ircv3_capabilities_request(capabilities, "sasl"); g_signal_connect(capabilities, "ack::sasl", G_CALLBACK(purple_ircv3_sasl_ack_cb), NULL); } gboolean purple_ircv3_sasl_logged_in(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "RPL_LOGGEDIN received with no SASL data " "present"); return FALSE; } /* At this point, we have the users authenticated username, we _may_ want * to update the account's ID to this, but we'll need more testing to * verify that. * -- GK 2023-01-12 */ return TRUE; } gboolean purple_ircv3_sasl_logged_out(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "RPL_LOGGEDOUT received with no SASL data " "present"); return FALSE; } /* Not sure how to trigger this or what we should do in this case to be * honest, so just note it for now. * -- GK 2023-01-12 */ g_warning("Server sent SASL logged out"); return TRUE; } gboolean purple_ircv3_sasl_nick_locked(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, GStrv params, GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; char *message = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "ERR_NICKLOCKED received with no SASL data " "present"); return FALSE; } message = g_strjoinv(" ", params); g_set_error(error, PURPLE_CONNECTION_ERROR, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, _("Nick name is locked: %s"), message); g_free(message); return FALSE; } gboolean purple_ircv3_sasl_success(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, GError **error, gpointer user_data) { PurpleIRCv3Capabilities *capabilities = NULL; PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; capabilities = purple_ircv3_connection_get_capabilities(connection); data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "RPL_SASLSUCCESS received with no SASL data " "present"); return FALSE; } /* This needs to be after we've checked the SASL data otherwise we might * end up removing a wait that we don't own. */ purple_ircv3_capabilities_remove_wait(capabilities); g_message("successfully authenticated with SASL '%s' mechanism.", data->current_mechanism); return TRUE; } gboolean purple_ircv3_sasl_failed(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "ERR_SASLFAIL received with no SASL data present"); return FALSE; } purple_ircv3_sasl_attempt(connection); return TRUE; } gboolean purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "ERR_SASLTOOLONG received with no SASL data " "present"); return FALSE; } return TRUE; } gboolean purple_ircv3_sasl_aborted(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "ERR_SASLABORTED received with no SASL data " "present"); return FALSE; } /* This is supposed to get sent, when the client sends `AUTHENTICATE *`, * but we don't do this, so I guess we'll note it for now...? * --GK 2023-01-12 */ g_warning("The server claims we aborted SASL authentication."); return TRUE; } gboolean purple_ircv3_sasl_already_authed(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "ERR_SASLALREADY received with no SASL data " "present"); return FALSE; } /* Similar to aborted above, we don't allow this, so just note that it * happened. * -- GK 2023-01-12 */ g_warning("Server claims we tried to SASL authenticate again."); return TRUE; } gboolean purple_ircv3_sasl_mechanisms(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, guint n_params, GStrv params, G_GNUC_UNUSED GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "RPL_SASLMECHS received with no SASL data " "present"); return FALSE; } /* We need to find a server that sends this message. The specification says * it _may_ be sent when the client sends AUTHENTICATE with an unknown * mechanism, but ergo doesn't. * * We can't just blindly accept the new list either, as depending on how * the server implements it, we'll need to remove mechanisms we've already * tried in the event the server just dumps the entire list. As we're not * currently tracking which mechanisms we've tried, this will have to be * addressed as well. */ if(n_params > 0) { char *message = g_strjoinv(" ", params); g_message("Server sent the following SASL mechanisms: %s", message); g_free(message); } else { g_message("Server sent an empty list of SASL mechanisms"); } return TRUE; } gboolean purple_ircv3_sasl_authenticate(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const char *source, G_GNUC_UNUSED const char *command, guint n_params, GStrv params, GError **error, gpointer user_data) { PurpleIRCv3Connection *connection = user_data; PurpleIRCv3SASLData *data = NULL; char *payload = NULL; gboolean done = FALSE; if(n_params != 1) { g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "ignoring AUTHENTICATE with %d parameters", n_params); return FALSE; } payload = params[0]; data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY); if(data == NULL) { g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0, "AUTHENTICATE received with no SASL data present"); return FALSE; } /* If the server sent us a payload, combine the chunks. */ if(payload[0] != '+') { if(data->server_in_buffer == NULL) { data->server_in_buffer = g_string_new(payload); } else { g_string_append(data->server_in_buffer, payload); } if(strlen(payload) < 400) { done = TRUE; } } else { /* The server sent a + which is an empty message or the final message * ended on a 400 byte barrier. */ done = TRUE; } if(done) { char *server_in = NULL; char *client_out = NULL; gsize server_in_length = 0; size_t client_out_length = 0; int res = 0; /* If we have a buffer, base64 decode it, and then free it. */ if(data->server_in_buffer != NULL) { server_in = (char *)g_base64_decode(data->server_in_buffer->str, &server_in_length); g_string_free(data->server_in_buffer, TRUE); } /* Try to move to the next step of the sasl client. */ res = gsasl_step(data->session, server_in, server_in_length, &client_out, &client_out_length); /* We should be done with server_in, so free it.*/ g_clear_pointer(&server_in, g_free); /* If we didn't get ok or continue, it's an error. */ if(res != GSASL_OK) { g_set_error(error, PURPLE_CONNECTION_ERROR, PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE, _("SASL authentication failed: %s"), gsasl_strerror(res)); return FALSE; } /* If we got an output for the client, write it out. */ if(client_out_length > 0) { char *encoded = g_base64_encode((guchar *)client_out, client_out_length); purple_ircv3_connection_writef(connection, "AUTHENTICATE %s", encoded); g_free(encoded); } else { purple_ircv3_connection_writef(connection, "AUTHENTICATE +"); } } return TRUE; }