protocols/ircv3/purpleircv3sasl.c

changeset 42652
225762d4e206
parent 42568
31e8c7c92e2f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/protocols/ircv3/purpleircv3sasl.c	Mon Mar 25 21:43:28 2024 -0500
@@ -0,0 +1,563 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 library 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 library 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 library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include <hasl.h>
+
+#include "purpleircv3sasl.h"
+
+#include "purpleircv3capabilities.h"
+#include "purpleircv3connection.h"
+#include "purpleircv3constants.h"
+#include "purpleircv3core.h"
+
+#define PURPLE_IRCV3_SASL_DATA_KEY ("sasl-data")
+
+typedef struct {
+	PurpleConnection *connection;
+
+	HaslContext *ctx;
+
+	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 Helpers
+ *****************************************************************************/
+static void
+purple_ircv3_sasl_data_free(PurpleIRCv3SASLData *data) {
+	g_clear_object(&data->ctx);
+
+	g_string_free(data->server_in_buffer, TRUE);
+
+	g_free(data);
+}
+
+static void
+purple_ircv3_sasl_data_add(PurpleConnection *connection, HaslContext *ctx) {
+	PurpleIRCv3SASLData *data = 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;
+
+	/* We truncate the server_in_buffer when we need to so that we can minimize
+	 * allocations and simplify the logic involved with it.
+	 */
+	data->server_in_buffer = g_string_new("");
+}
+
+static void
+purple_ircv3_sasl_attempt(PurpleIRCv3Connection *connection) {
+	PurpleIRCv3SASLData *data = NULL;
+	const char *next_mechanism = NULL;
+	const char *current = NULL;
+
+	data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
+
+	current = hasl_context_get_current_mechanism(data->ctx);
+	if(current != NULL) {
+		g_message("SASL '%s' mechanism failed", current);
+	}
+
+	next_mechanism = hasl_context_next(data->ctx);
+	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;
+	}
+
+	g_message("trying SASL '%s' mechanism", next_mechanism);
+
+	purple_ircv3_connection_writef(connection, "%s %s",
+	                               PURPLE_IRCV3_MSG_AUTHENTICATE,
+	                               next_mechanism);
+}
+
+static void
+purple_ircv3_sasl_start(PurpleIRCv3Capabilities *caps) {
+	PurpleIRCv3Connection *connection = NULL;
+	PurpleAccount *account = NULL;
+	PurpleConnection *purple_connection = NULL;
+	HaslContext *ctx = NULL;
+	const char *mechanisms = NULL;
+	gboolean toggle = FALSE;
+
+	connection = purple_ircv3_capabilities_get_connection(caps);
+	purple_connection = PURPLE_CONNECTION(connection);
+	account = purple_connection_get_account(purple_connection);
+
+	ctx = hasl_context_new();
+
+	/* 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);
+
+	/* Determine what mechanisms we're allowing and tell the context. */
+	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);
+	}
+	hasl_context_set_allowed_mechanisms(ctx, mechanisms);
+
+	/* Add the values we know to the context. */
+	hasl_context_set_username(ctx, purple_ircv3_sasl_get_username(purple_connection));
+	hasl_context_set_password(ctx, purple_connection_get_password(purple_connection));
+
+	toggle = purple_account_get_bool(account, "use-tls", TRUE);
+	hasl_context_set_tls(ctx, toggle);
+
+	toggle = purple_account_get_bool(account, "plain-sasl-in-clear", FALSE);
+	hasl_context_set_allow_clear_text(ctx, toggle);
+
+	/* Create our SASLData object, add it to the connection. */
+	purple_ircv3_sasl_data_add(purple_connection, ctx);
+
+	/* 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,
+	                                  PURPLE_IRCV3_CAPABILITY_SASL);
+
+	g_signal_connect(capabilities, "ack::" PURPLE_IRCV3_CAPABILITY_SASL,
+	                 G_CALLBACK(purple_ircv3_sasl_ack_cb), NULL);
+}
+
+gboolean
+purple_ircv3_sasl_logged_in(G_GNUC_UNUSED PurpleIRCv3Message *message,
+                            G_GNUC_UNUSED GError **error,
+                            gpointer user_data)
+{
+	PurpleIRCv3Connection *connection = user_data;
+	PurpleIRCv3SASLData *data = NULL;
+	PurpleAccount *account = NULL;
+	const char *sasl_name = 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;
+	}
+
+	/* Check if the SASL login name is not set. If it is not set, set it to the
+	 * current nick as it was successful.
+	 */
+	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
+	sasl_name = purple_account_get_string(account, "sasl-login-name", "");
+	if(purple_strempty(sasl_name)) {
+		char **userparts = NULL;
+		const char *username = NULL;
+
+		username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
+		userparts = g_strsplit(username, "@", 2);
+
+		purple_account_set_string(account, "sasl-login-name", userparts[0]);
+
+		g_strfreev(userparts);
+	}
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_logged_out(G_GNUC_UNUSED PurpleIRCv3Message *message,
+                             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(PurpleIRCv3Message *v3_message,
+                              GError **error,
+                              gpointer user_data)
+{
+	PurpleIRCv3Connection *connection = user_data;
+	PurpleIRCv3SASLData *data = NULL;
+	GStrv params = 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;
+	}
+
+	params = purple_ircv3_message_get_params(v3_message);
+	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 PurpleIRCv3Message *message,
+                          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.",
+	          hasl_context_get_current_mechanism(data->ctx));
+
+	return TRUE;
+}
+
+gboolean
+purple_ircv3_sasl_failed(G_GNUC_UNUSED PurpleIRCv3Message *message,
+                         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 PurpleIRCv3Message *message,
+                                   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 PurpleIRCv3Message *message,
+                          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 PurpleIRCv3Message *message,
+                                 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(PurpleIRCv3Message *message,
+                             G_GNUC_UNUSED GError **error,
+                             gpointer user_data)
+{
+	PurpleIRCv3Connection *connection = user_data;
+	PurpleIRCv3SASLData *data = NULL;
+	GStrv params = NULL;
+	guint n_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,
+		                    "RPL_SASLMECHS received with no SASL data "
+		                    "present");
+
+		return FALSE;
+	}
+
+	params = purple_ircv3_message_get_params(message);
+	if(params != NULL) {
+		n_params = g_strv_length(params);
+	}
+
+	/* 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(PurpleIRCv3Message *message,
+                               GError **error,
+                               gpointer user_data)
+{
+	PurpleIRCv3Connection *connection = user_data;
+	PurpleIRCv3SASLData *data = NULL;
+	GStrv params = NULL;
+	char *payload = NULL;
+	gboolean done = FALSE;
+	guint n_params = 0;
+
+	params = purple_ircv3_message_get_params(message);
+	if(params != NULL) {
+		n_params = g_strv_length(params);
+	}
+
+	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] != '+') {
+		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) {
+		HaslMechanismResult res = 0;
+		GError *local_error = NULL;
+		guint8 *server_in = NULL;
+		guint8 *client_out = NULL;
+		gsize server_in_length = 0;
+		size_t client_out_length = 0;
+
+		/* If we have a buffer, base64 decode it, and then truncate it. */
+		if(data->server_in_buffer->len > 0) {
+			server_in = g_base64_decode(data->server_in_buffer->str,
+			                            &server_in_length);
+			g_string_truncate(data->server_in_buffer, 0);
+		}
+
+		/* Try to move to the next step of the sasl client. */
+		res = hasl_context_step(data->ctx, server_in, server_in_length,
+		                        &client_out, &client_out_length, &local_error);
+
+		/* We should be done with server_in, so free it.*/
+		g_clear_pointer(&server_in, g_free);
+
+		if(res == HASL_MECHANISM_RESULT_ERROR) {
+			g_propagate_error(error, local_error);
+
+			return FALSE;
+		}
+
+		if(local_error != NULL) {
+			g_warning("hasl_context_step returned an error without an error "
+			          "status: %s", local_error->message);
+
+			g_clear_error(&local_error);
+		}
+
+		/* If we got an output for the client, write it out. */
+		if(client_out_length > 0) {
+			char *encoded = NULL;
+
+			encoded = g_base64_encode(client_out, client_out_length);
+			g_clear_pointer(&client_out, g_free);
+
+			purple_ircv3_connection_writef(connection, "%s %s",
+			                               PURPLE_IRCV3_MSG_AUTHENTICATE,
+			                               encoded);
+			g_free(encoded);
+		} else {
+			purple_ircv3_connection_writef(connection, "%s +",
+			                               PURPLE_IRCV3_MSG_AUTHENTICATE);
+		}
+	}
+
+	return TRUE;
+}

mercurial