From 60d9ec621ff81ac68b9d2066fb77eaefc8d02ccb Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Sun, 3 Jul 2016 22:02:02 -0700 Subject: SASL: handle fragmentation The IRCv3 SASL extension says that AUTHENTICATION payloads of exactly 400 bytes in length indicate that the message is fragmented and will continue in a subsequent message. Handle the reassembly and splitting of these messages so that we are compliant with the specification. --- src/irc/core/irc-servers.c | 2 + src/irc/core/irc-servers.h | 3 +- src/irc/core/sasl.c | 172 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 154 insertions(+), 23 deletions(-) diff --git a/src/irc/core/irc-servers.c b/src/irc/core/irc-servers.c index feb7cee8..9a3e9a67 100644 --- a/src/irc/core/irc-servers.c +++ b/src/irc/core/irc-servers.c @@ -444,6 +444,8 @@ static void sig_disconnected(IRC_SERVER_REC *server) gslist_free_full(server->cap_queue, (GDestroyNotify) g_free); server->cap_queue = NULL; + g_free_and_null(server->sasl_buffer); + /* these are dynamically allocated only if isupport was sent */ g_hash_table_foreach(server->isupport, (GHFunc) isupport_destroy_hash, server); diff --git a/src/irc/core/irc-servers.h b/src/irc/core/irc-servers.h index 41c4b9c2..ecf65ac2 100644 --- a/src/irc/core/irc-servers.h +++ b/src/irc/core/irc-servers.h @@ -78,7 +78,8 @@ struct _IRC_SERVER_REC { GSList *cap_queue; /* A list of caps to request on connection */ int cap_complete:1; /* We've done the initial CAP negotiation */ - guint sasl_timeout; /* Holds the source id of the running timeout */ + GString *sasl_buffer; /* Buffer used to reassemble a fragmented SASL payload */ + guint sasl_timeout; /* Holds the source id of the running timeout */ /* Command sending queue */ int cmdcount; /* number of commands in `cmdqueue'. Can be more than diff --git a/src/irc/core/sasl.c b/src/irc/core/sasl.c index f080ae59..a1c16cdd 100644 --- a/src/irc/core/sasl.c +++ b/src/irc/core/sasl.c @@ -26,6 +26,19 @@ #include "irc-servers.h" #include "sasl.h" +/* + * Based on IRCv3 SASL Extension Specification: + * http://ircv3.net/specs/extensions/sasl-3.1.html + */ +#define AUTHENTICATE_CHUNK_SIZE 400 // bytes + +/* + * Maximum size to allow the buffer to grow to before the next fragment comes in. Note that + * due to the way fragmentation works, the maximum message size will actually be: + * floor(AUTHENTICATE_MAX_SIZE / AUTHENTICATE_CHUNK_SIZE) + AUTHENTICATE_CHUNK_SIZE - 1 + */ +#define AUTHENTICATE_MAX_SIZE 8192 // bytes + #define SASL_TIMEOUT (20 * 1000) // ms static gboolean sasl_timeout(IRC_SERVER_REC *server) @@ -105,19 +118,106 @@ static void sasl_success(IRC_SERVER_REC *server, const char *data, const char *f cap_finish_negotiation(server); } -static void sasl_step(IRC_SERVER_REC *server, const char *data, const char *from) +/* + * Responsible for reassembling incoming SASL requests. SASL requests must be split + * into 400 byte requests to stay below the IRC command length limit of 512 bytes. + * The spec says that if there is 400 bytes, then there is expected to be a + * continuation in the next chunk. If a message is exactly a multiple of 400 bytes, + * there must be a blank message of "AUTHENTICATE +" to indicate the end. + * + * This function returns the fully reassembled and decoded AUTHENTICATION message if + * completed or NULL if there are more messages expected. + */ +static gboolean sasl_reassemble_incoming(IRC_SERVER_REC *server, const char *fragment, GString **decoded) { - IRC_SERVER_CONNECT_REC *conn; - GString *req; - char *enc_req; + GString *enc_req; + gsize fragment_len; + + fragment_len = strlen(fragment); + + /* Check if there is an existing fragment to prepend. */ + if (server->sasl_buffer != NULL) { + if (g_strcmp0("+", fragment) == 0) { + enc_req = server->sasl_buffer; + } else { + enc_req = g_string_append_len(server->sasl_buffer, fragment, fragment_len); + } + server->sasl_buffer = NULL; + } else { + enc_req = g_string_new_len(fragment, fragment_len); + } - conn = server->connrec; + /* + * Fail authentication with this server. They have sent too much data. + */ + if (enc_req->len > AUTHENTICATE_MAX_SIZE) { + return FALSE; + } - /* Stop the timer */ - if (server->sasl_timeout != 0) { - g_source_remove(server->sasl_timeout); - server->sasl_timeout = 0; + /* + * If the the request is exactly the chunk size, this is a fragment + * and more data is expected. + */ + if (fragment_len == AUTHENTICATE_CHUNK_SIZE) { + server->sasl_buffer = enc_req; + return TRUE; + } + + if (enc_req->len == 1 && *enc_req->str == '+') { + *decoded = g_string_new_len("", 0); + } else { + gsize dec_len; + gchar *tmp; + + tmp = (gchar *) g_base64_decode(enc_req->str, &dec_len); + *decoded = g_string_new_len(tmp, dec_len); + } + + g_string_free(enc_req, TRUE); + return TRUE; +} + +/* + * Splits the response into appropriately sized chunks for the AUTHENTICATION + * command to be sent to the IRC server. If |response| is NULL, then the empty + * response is sent to the server. + */ +void sasl_send_response(IRC_SERVER_REC *server, GString *response) +{ + char *enc; + size_t offset, enc_len, chunk_len; + + if (response == NULL) { + irc_send_cmdv(server, "AUTHENTICATE +"); + return; + } + + enc = g_base64_encode((guchar *) response->str, response->len); + enc_len = strlen(enc); + + for (offset = 0; offset < enc_len; offset += AUTHENTICATE_CHUNK_SIZE) { + chunk_len = enc_len - offset; + if (chunk_len > AUTHENTICATE_CHUNK_SIZE) + chunk_len = AUTHENTICATE_CHUNK_SIZE; + + irc_send_cmdv(server, "AUTHENTICATE %.*s", (int) chunk_len, enc + offset); + } + + if (offset == enc_len) { + irc_send_cmdv(server, "AUTHENTICATE +"); } + g_free(enc); +} + +/* + * Called when the incoming SASL request is completely received. + */ +static void sasl_step_complete(IRC_SERVER_REC *server, GString *data) +{ + IRC_SERVER_CONNECT_REC *conn; + GString *resp; + + conn = server->connrec; switch (conn->sasl_mechanism) { case SASL_MECHANISM_PLAIN: @@ -125,29 +225,57 @@ static void sasl_step(IRC_SERVER_REC *server, const char *data, const char *from * The PLAIN mechanism expects a NULL-separated string composed by the authorization identity, the * authentication identity and the password. * The authorization identity field is explicitly set to the user provided username. - * The whole request is then encoded in base64. */ - - req = g_string_new(NULL); + */ - g_string_append(req, conn->sasl_username); - g_string_append_c(req, '\0'); - g_string_append(req, conn->sasl_username); - g_string_append_c(req, '\0'); - g_string_append(req, conn->sasl_password); + resp = g_string_new(NULL); - enc_req = g_base64_encode((const guchar *)req->str, req->len); + g_string_append(resp, conn->sasl_username); + g_string_append_c(resp, '\0'); + g_string_append(resp, conn->sasl_username); + g_string_append_c(resp, '\0'); + g_string_append(resp, conn->sasl_password); - irc_send_cmdv(server, "AUTHENTICATE %s", enc_req); + sasl_send_response(server, resp); + g_string_free(resp, TRUE); - g_free(enc_req); - g_string_free(req, TRUE); break; case SASL_MECHANISM_EXTERNAL: /* Empty response */ - irc_send_cmdv(server, "AUTHENTICATE +"); + sasl_send_response(server, NULL); break; } +} + +static void sasl_step_fail(IRC_SERVER_REC *server) +{ + irc_send_cmd_now(server, "AUTHENTICATE *"); + cap_finish_negotiation(server); + + server->sasl_timeout = 0; + + signal_emit("server sasl failure", 2, server, "The server sent an invalid payload"); +} + +static void sasl_step(IRC_SERVER_REC *server, const char *data, const char *from) +{ + GString *req = NULL; + + /* Stop the timer */ + if (server->sasl_timeout != 0) { + g_source_remove(server->sasl_timeout); + server->sasl_timeout = 0; + } + + if (!sasl_reassemble_incoming(server, data, &req)) { + sasl_step_fail(server); + return; + } + + if (req != NULL) { + sasl_step_complete(server, req); + g_string_free(req, TRUE); + } /* We expect a response within a reasonable time */ server->sasl_timeout = g_timeout_add(SASL_TIMEOUT, (GSourceFunc) sasl_timeout, server); -- cgit v1.2.3