From 92eb27fe3fe4a7e1f270b9358542b33ca03f7ea5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Gomez?= <bip@kyoshiro.org>
Date: Sun, 2 Jan 2022 14:16:10 +0100
Subject: [PATCH 01/01] Add SASL authentication support (EXTERNAL, PLAIN)

---
 AUTHORS            |   1 +
 bip.conf.5         |  14 ++++
 samples/bip.conf   |   5 ++
 samples/bip.vim    |   3 +-
 src/Makefile.am    |   3 +-
 src/bip.c          |  33 +++++++++
 src/conf.y         |   8 ++-
 src/irc.c          | 174 +++++++++++++++++++++++++++++++++++++++++++++
 src/irc.h          |   6 ++
 src/lex.l          |   3 +
 src/utils/base64.c |  93 ++++++++++++++++++++++++
 src/utils/base64.h |  15 ++++
 12 files changed, 355 insertions(+), 3 deletions(-)
 create mode 100644 src/utils/base64.c
 create mode 100644 src/utils/base64.h

diff --git a/AUTHORS b/AUTHORS
index c044fe2..affc47e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -10,3 +10,4 @@ Thanks to our marketting and management team:
 ack|, ato, blackmore, lafouine, Gaston & gromit 
 
 Crypto shamelessly stolen from Christophe 'sexy' Devine.
+Credits to Jouni Malinen for base64 library (http://web.mit.edu/freebsd/head/contrib/wpa/src/utils/)
diff --git a/bip.conf.5 b/bip.conf.5
index 8da8e50..25c6268 100644
--- a/bip.conf.5
+++ b/bip.conf.5
@@ -402,6 +402,20 @@ BIP will send that string as the realname part (description in whois result)
 upon connect. If not specified and if \fBdefault_realname\fP is specified in
 the \fBuser\fP section, BIP will use that default realname string.
 
+.TP
+\fBsasl_mechanism\fP
+Tells BIP to use specified SASL mechanism. Currently supported: PLAIN, EXTERNAL.
+PLAIN mechanism requires \fBsasl_username\fP and \fBsasl_password\fP and is the
+default if these are set.
+
+.TP
+\fBsasl_username\fP
+This connection's username to pass on using SASL authentication.
+
+.TP
+\fBsasl_password\fP
+This connection's password to pass on using SASL authentication.
+
 .TP
 \fBsource_port\fP
 If specified, tells BIP to connect from this port to the IRC server.
diff --git a/samples/bip.conf b/samples/bip.conf
index 97c8a52..37af202 100644
--- a/samples/bip.conf
+++ b/samples/bip.conf
@@ -240,6 +240,11 @@
 		#You can specify this field more than once. BIP will send the text as is to the server.
 		#on_connect_send = "PRIVMSG NickServ :IDENTIFY nspassword";
 
+		# You can connect with SASL on networks supporting it
+		#sasl_username = "username";
+		#sasl_password = "sikioure password";
+		#sasl_mechanism = "PLAIN";
+
 		# Some options:
 		#away_nick = "bip`away";
 		# Away message to be set when no client is connected
diff --git a/samples/bip.vim b/samples/bip.vim
index 7b2585e..4f30b66 100644
--- a/samples/bip.vim
+++ b/samples/bip.vim
@@ -91,7 +91,8 @@ syn keyword	bipCoKeyword	contained nextgroup=bipBoolV autojoin_on_kick
 	\ follow_nick ignore_first_nick log ignore_server_capab
 syn keyword	bipCoKeyword	contained nextgroup=bipStringV name user nick
 	\ network password vhost away_nick on_connect_send realname
-	\ no_client_away_msg ssl_check_mode
+	\ no_client_away_msg ssl_check_mode sasl_username sasl_password
+	\ sasl_mechanism
 syn keyword	bipCoKeyword	contained nextgroup=bipNumericV source_port
 
 " Channel elements (lvl 2)
diff --git a/src/Makefile.am b/src/Makefile.am
index d6e3e86..7489033 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -10,7 +10,8 @@ libbip_a_SOURCES = conf.y lex.l \
 	md5.c md5.h \
 	path_util.c path_util.h \
 	tuple.h \
-	util.c util.h
+	util.c util.h \
+	utils/base64.c utils/base64.h
 
 libbip_a_CFLAGS = ${OPENSSL_CFLAGS} $(AM_CFLAGS)
 
diff --git a/src/bip.c b/src/bip.c
index 3ca2d80..05b74e3 100644
--- a/src/bip.c
+++ b/src/bip.c
@@ -480,6 +480,22 @@ static int add_connection(bip_t *bip, struct bipuser *user, list_t *data)
 		case LEX_PASSWORD:
 			MOVE_STRING(l->s_password, t->pdata);
 			break;
+		case LEX_SASL_USERNAME:
+			MOVE_STRING(l->sasl_username, t->pdata);
+			break;
+		case LEX_SASL_PASSWORD:
+			MOVE_STRING(l->sasl_password, t->pdata);
+			break;
+		case LEX_SASL_MECHANISM:
+			if (strcmp(t->pdata, "PLAIN") == 0) {
+				l->sasl_mechanism = SASL_AUTH_PLAIN;
+			} else if (strcmp(t->pdata, "EXTERNAL") == 0) {
+				l->sasl_mechanism = SASL_AUTH_EXTERNAL;
+			} else {
+				conf_die(bip, "Unsupported SASL mechanism %s.", t->pdata);
+				return 0;
+			}
+			break;
 		case LEX_VHOST:
 			MOVE_STRING(l->vhost, t->pdata);
 			break;
@@ -595,6 +611,23 @@ static int add_connection(bip_t *bip, struct bipuser *user, list_t *data)
 		l->realname = bip_strdup(user->default_realname);
 	}
 
+	if (l->sasl_username && !l->sasl_password) {
+		conf_die(bip, "sasl_username set without sasl_password.");
+		return 0;
+	}
+
+	if (!l->sasl_username && l->sasl_password) {
+		conf_die(bip, "sasl_password set without sasl_username.");
+		return 0;
+	}
+
+	if (l->sasl_mechanism == SASL_AUTH_PLAIN && (!l->sasl_username || !l->sasl_password)) {
+		conf_die(bip, "SASL mechanism PLAIN requires username and password.");
+		return 0;
+	}
+	if (l->sasl_username && !l->sasl_mechanism)
+		l->sasl_mechanism = SASL_AUTH_PLAIN;
+
 	l->in_use = 1;
 	return 1;
 }
diff --git a/src/conf.y b/src/conf.y
index 3cf56cb..ca735ee 100644
--- a/src/conf.y
+++ b/src/conf.y
@@ -68,7 +68,7 @@ struct tuple *tuple_l_new(int type, void *p)
 
 %}
 
-%token LEX_IP LEX_EQ LEX_PORT LEX_CSS LEX_SEMICOLON LEX_CONNECTION LEX_NETWORK LEX_LBRA LEX_RBRA LEX_USER LEX_NAME LEX_NICK LEX_SERVER LEX_PASSWORD LEX_SRCIP LEX_HOST LEX_VHOST LEX_SOURCE_PORT LEX_NONE LEX_COMMENT LEX_BUNCH LEX_REALNAME LEX_SSL LEX_SSL_CHECK_MODE LEX_SSL_CHECK_STORE LEX_SSL_CLIENT_CERTFILE  LEX_CIPHERS LEX_CSS_CIPHERS LEX_DEFAULT_CIPHERS LEX_DH_PARAM LEX_CHANNEL LEX_KEY LEX_LOG_ROOT LEX_LOG_FORMAT LEX_LOG_LEVEL LEX_BACKLOG_LINES LEX_BACKLOG_TIMESTAMP LEX_BACKLOG_NO_TIMESTAMP LEX_BACKLOG LEX_LOG LEX_LOG_SYSTEM LEX_LOG_SYNC_INTERVAL LEX_FOLLOW_NICK LEX_ON_CONNECT_SEND LEX_AWAY_NICK LEX_PID_FILE LEX_WRITE_OIDENTD LEX_OIDENTD_FILE LEX_IGN_FIRST_NICK LEX_ALWAYS_BACKLOG LEX_BLRESET_ON_TALK LEX_BLRESET_CONNECTION LEX_DEFAULT_USER LEX_DEFAULT_NICK LEX_DEFAULT_REALNAME LEX_NO_CLIENT_AWAY_MSG LEX_BL_MSG_ONLY LEX_ADMIN LEX_BIP_USE_NOTICE LEX_CSS_PEM LEX_AUTOJOIN_ON_KICK LEX_IGNORE_CAPAB LEX_RECONN_TIMER
+%token LEX_IP LEX_EQ LEX_PORT LEX_CSS LEX_SEMICOLON LEX_CONNECTION LEX_NETWORK LEX_LBRA LEX_RBRA LEX_USER LEX_NAME LEX_NICK LEX_SERVER LEX_PASSWORD LEX_SRCIP LEX_HOST LEX_VHOST LEX_SOURCE_PORT LEX_NONE LEX_COMMENT LEX_BUNCH LEX_REALNAME LEX_SSL LEX_SSL_CHECK_MODE LEX_SSL_CHECK_STORE LEX_SSL_CLIENT_CERTFILE  LEX_CIPHERS LEX_CSS_CIPHERS LEX_DEFAULT_CIPHERS LEX_DH_PARAM LEX_CHANNEL LEX_KEY LEX_LOG_ROOT LEX_LOG_FORMAT LEX_LOG_LEVEL LEX_BACKLOG_LINES LEX_BACKLOG_TIMESTAMP LEX_BACKLOG_NO_TIMESTAMP LEX_BACKLOG LEX_LOG LEX_LOG_SYSTEM LEX_LOG_SYNC_INTERVAL LEX_FOLLOW_NICK LEX_ON_CONNECT_SEND LEX_AWAY_NICK LEX_PID_FILE LEX_WRITE_OIDENTD LEX_OIDENTD_FILE LEX_IGN_FIRST_NICK LEX_ALWAYS_BACKLOG LEX_BLRESET_ON_TALK LEX_BLRESET_CONNECTION LEX_DEFAULT_USER LEX_DEFAULT_NICK LEX_DEFAULT_REALNAME LEX_NO_CLIENT_AWAY_MSG LEX_BL_MSG_ONLY LEX_ADMIN LEX_BIP_USE_NOTICE LEX_CSS_PEM LEX_AUTOJOIN_ON_KICK LEX_IGNORE_CAPAB LEX_RECONN_TIMER LEX_SASL_USERNAME LEX_SASL_PASSWORD LEX_SASL_MECHANISM
 
 %union {
 	int number;
@@ -208,6 +208,12 @@ con_command:
 		 $3); }
 	   | LEX_PASSWORD LEX_EQ LEX_STRING { $$ = tuple_s_new(LEX_PASSWORD,
 		 $3); }
+	   | LEX_SASL_USERNAME LEX_EQ LEX_STRING { $$ = tuple_s_new(
+	     LEX_SASL_USERNAME, $3); }
+	   | LEX_SASL_PASSWORD LEX_EQ LEX_STRING { $$ = tuple_s_new(
+	     LEX_SASL_PASSWORD, $3); }
+	   | LEX_SASL_MECHANISM LEX_EQ LEX_STRING { $$ = tuple_s_new(
+	     LEX_SASL_MECHANISM, $3); }
 	   | LEX_VHOST LEX_EQ LEX_STRING { $$ = tuple_s_new(LEX_VHOST, $3); }
 	   | LEX_SOURCE_PORT LEX_EQ LEX_INT {
 		   $$ = tuple_i_new(LEX_SOURCE_PORT, $3); }
diff --git a/src/irc.c b/src/irc.c
index e9aa884..d812ae0 100644
--- a/src/irc.c
+++ b/src/irc.c
@@ -21,6 +21,7 @@
 #include "log.h"
 #include "connection.h"
 #include "md5.h"
+#include "utils/base64.h"
 
 #define S_CONN_DELAY (10)
 
@@ -67,6 +68,8 @@ static void irc_copy_cli(struct link_client *src, struct link_client *dest,
 static void irc_cli_make_join(struct link_client *ic);
 static void server_setup_reconnect_timer(struct link *link);
 int irc_cli_bip(bip_t *bip, struct link_client *ic, struct line *line);
+static int irc_server_sasl_authenticate(struct link_server *ircs);
+static char *sasl_mechanism_to_text(int sasl_mechanism);
 
 #define LAGOUT_TIME 480
 #define LAGCHECK_TIME (90)
@@ -386,6 +389,92 @@ int irc_dispatch_server(bip_t *bip, struct link_server *server,
 			}
 			ret = OK_FORGET;
 		}
+	} else if (irc_line_elem_equals(line, 0, "CAP")) {
+		if (LINK(server)->sasl_mechanism) {
+			if (irc_line_elem_equals(line, 2, "ACK") && irc_line_elem_equals(line, 3, "sasl")) {
+				// Server is answering our CAP REQ :sasl and is SASL capable
+				char *sasl_mech = sasl_mechanism_to_text(LINK(server)->sasl_mechanism);
+				mylog(LOG_INFO, "[%s] Server is SASL capable, starting %s authentication.",
+					LINK(server)->name, sasl_mech);
+				WRITE_LINE1(CONN(server), NULL, "AUTHENTICATE", sasl_mech);
+				ret = OK_FORGET;
+			} else if (irc_line_elem_equals(line, 2, "NAK") && irc_line_elem_equals(line, 3, "sasl")) {
+				// Server is answering our CAP REQ :sasl and isn't SASL capable
+				mylog(LOG_INFO, "[%s] Server is not SASL capable.", LINK(server)->name);
+				ret = ERR_PROTOCOL;
+			} else {
+				// Unhandled CAP message
+				mylog(LOG_ERROR, "[%s] Unhandled CAP message: %s",
+						LINK(server)->name, irc_line_to_string(line));
+				ret = OK_FORGET;
+			}
+		} else {
+			// Unhandled CAP message
+			mylog(LOG_ERROR, "[%s] Unhandled CAP message: %s", LINK(server)->name,
+					irc_line_to_string(line));
+			ret = OK_FORGET;
+		}
+	} else if (irc_line_elem_equals(line, 0, "AUTHENTICATE")) {
+		if (LINK(server)->sasl_mechanism) {
+			if (irc_line_count(line) == 2 && irc_line_elem_equals(line, 1, "+")) {
+				// Server is waiting for us to authenticate, let's do it
+				mylog(LOG_INFO, "[%s] Server accepted our authentication mechanism.",
+						LINK(server)->name);
+				ret = irc_server_sasl_authenticate(server);
+			} else {
+				// Anything else than "AUTHENTICATE +" is unknown to us
+				mylog(LOG_ERROR, "[%s] Server sent gibberish: %s",
+						LINK(server)->name, irc_line_to_string(line));
+				ret = ERR_PROTOCOL;
+			}
+		} else {
+			// Unhandled AUTHENTICATE message
+			mylog(LOG_ERROR, "[%s] Unhandled AUTHENTICATE message: %s",
+					LINK(server)->name, irc_line_to_string(line));
+			ret = OK_FORGET;
+		}
+	} else if (irc_line_elem_equals(line, 0, "900")) {
+		if (irc_line_count(line) >= 5) {
+			mylog(LOG_INFO, "[%s] Logged in as %s(%s): %s", LINK(server)->name,
+				irc_line_elem(line, 3), irc_line_elem(line, 2), irc_line_elem(line, 4));
+		} else {
+			mylog(LOG_INFO, "[%s] Logged in: %s", LINK(server)->name, irc_line_to_string(line));
+		}
+		ret = OK_FORGET;
+	} else if (irc_line_elem_equals(line, 0, "901")) {
+		if (irc_line_count(line) >= 4) {
+			mylog(LOG_INFO, "[%s] Logged out: %s",
+				LINK(server)->name, irc_line_elem(line, 3));
+		} else {
+			mylog(LOG_INFO, "[%s] Logged out: %s", LINK(server)->name, irc_line_to_string(line));
+		}
+		ret = OK_FORGET;
+	} else if (irc_line_elem_equals(line, 0, "902")) {
+		mylog(LOG_INFO, "[%s] Account unavailable: %s",
+			LINK(server)->name, irc_line_to_string(line));
+		ret = OK_FORGET;
+	} else if (irc_line_elem_equals(line, 0, "903")) {
+		mylog(LOG_INFO, "[%s] SASL authentication successful", LINK(server)->name);
+		WRITE_LINE1(CONN(server), NULL, "CAP", "END");
+		ret = OK_FORGET;
+	} else if (irc_line_elem_equals(line, 0, "904")) {
+		mylog(LOG_ERROR, "[%s] SASL authentication failed", LINK(server)->name);
+		ret = ERR_AUTH;
+	} else if (irc_line_elem_equals(line, 0, "905")) {
+		mylog(LOG_ERROR, "[%s] SASL message too long", LINK(server)->name);
+		ret = ERR_AUTH;
+	} else if (irc_line_elem_equals(line, 0, "906")) {
+		mylog(LOG_ERROR, "[%s] SASL authentication aborted by client",
+			LINK(server)->name);
+		ret = ERR_AUTH;
+	} else if (irc_line_elem_equals(line, 0, "907")) {
+		mylog(LOG_ERROR, "[%s] SASL authentication has already been completed",
+			LINK(server)->name);
+		ret = OK_FORGET;
+	} else if (irc_line_elem_equals(line, 0, "908")) {
+		mylog(LOG_ERROR, "[%s] Server only accepts following authentication mechanisms: %s",
+			LINK(server)->name, irc_line_elem(line, 2));
+		ret = ERR_AUTH;
 	} else if (irc_line_elem_equals(line, 0, "433")) {
 		if (LINK(server)->s_state != IRCS_CONNECTED) {
 			size_t nicklen = strlen(server->nick);
@@ -1927,6 +2016,83 @@ fake:
 #endif
 }
 
+static char *sasl_mechanism_to_text(int sasl_mechanism)
+{
+	switch (sasl_mechanism) {
+	case SASL_AUTH_EXTERNAL:
+		return "EXTERNAL";
+	case SASL_AUTH_PLAIN:
+		return "PLAIN";
+	default:
+		return "UNKOWN_MECHANISM";
+	}
+}
+
+// Per RFC send packets of max 400 chars at a time
+#define SASL_AUTH_CHUNK_SZ 400
+static int irc_server_sasl_authenticate(struct link_server *ircs)
+{
+	char *sasl_username = LINK(ircs)->sasl_username;
+	char *sasl_password = LINK(ircs)->sasl_password;
+
+	if (LINK(ircs)->sasl_mechanism == SASL_AUTH_EXTERNAL) {
+		WRITE_LINE1(CONN(ircs), NULL, "AUTHENTICATE", "+");
+		return OK_FORGET;
+	}
+
+	// Should not happen, but we never know right ?
+	if (!sasl_username || !sasl_password) {
+		mylog(LOG_ERROR, "[%s] Missing SASL username or password.", LINK(ircs)->name);
+		return ERR_AUTH;
+	}
+
+	/*
+	 * Other than EXTERNAL we only support PLAIN.
+	 */
+
+	size_t chunk_chars = SASL_AUTH_CHUNK_SZ;
+	char chunk[SASL_AUTH_CHUNK_SZ + 1];
+	size_t u_len = strlen(sasl_username);
+	size_t p_len = strlen(sasl_password);
+	size_t raw_len = u_len*2 + p_len + 2;
+	size_t enc_len;
+	unsigned char *raw_str = bip_malloc(raw_len + 1);
+	unsigned char *enc_str;
+
+	memcpy(raw_str, sasl_username, u_len);
+	raw_str[u_len] = '\0';
+	memcpy(raw_str + u_len + 1, sasl_username, u_len);
+	raw_str[u_len*2 + 1] = '\0';
+	memcpy(raw_str + u_len*2 + 2, sasl_password, p_len);
+	enc_str = base64_encode(raw_str, raw_len, &enc_len);
+	mylog(LOG_DEBUG, "[%s] Base64 encoded SASL auth token (len %d): %s", LINK(ircs)->name, enc_len, enc_str);
+
+	for (size_t i = 0; i < enc_len; i += chunk_chars) {
+		size_t remaining = enc_len - i;
+		if (remaining < chunk_chars) {
+			memcpy(chunk, &enc_str[i], remaining);
+			chunk[remaining]= '\0';
+		} else {
+			memcpy(chunk, &enc_str[i], chunk_chars);
+			chunk[chunk_chars]= '\0';
+		}
+		mylog(LOG_DEBUG, "[%s] SASL AUTHENTICATE chunk %d, len %d: %s",
+			LINK(ircs)->name, i/chunk_chars, strlen(chunk), chunk);
+		WRITE_LINE1(CONN(ircs), NULL, "AUTHENTICATE", chunk);
+
+		// Send a closing AUTHENTICATE line if last chunk size was exactly 400
+		if (remaining == chunk_chars) {
+			mylog(LOG_DEBUG, "[%s] Last SASL chunk was exactly 400, sending +",
+				LINK(ircs)->name);
+			WRITE_LINE1(CONN(ircs), NULL, "AUTHENTICATE", "+");
+			break;
+		}
+	}
+	free(enc_str);
+
+	return OK_FORGET;
+}
+
 static void irc_server_startup(struct link_server *ircs)
 {
 	char *nick;
@@ -2195,6 +2361,12 @@ connection_t *irc_server_connect(struct link *link)
 
 	list_add_last(&_bip->conn_list, conn);
 	oidentd_dump(_bip);
+
+	if (link->sasl_mechanism) {
+		mylog(LOG_INFO, "[%s] SASL (%s) enabled, sending CAP REQ.",
+			link->name, sasl_mechanism_to_text(link->sasl_mechanism));
+		WRITE_LINE2(conn, NULL, "CAP", "REQ", ":sasl");
+	}
 	irc_server_startup(ls);
 	return conn;
 }
@@ -2635,6 +2807,8 @@ void link_kill(bip_t *bip, struct link *link)
 	MAYFREE(link->username);
 	MAYFREE(link->realname);
 	MAYFREE(link->s_password);
+	MAYFREE(link->sasl_username);
+	MAYFREE(link->sasl_password);
 	MAYFREE(link->connect_nick);
 	MAYFREE(link->vhost);
 #ifdef HAVE_LIBSSL
diff --git a/src/irc.h b/src/irc.h
index 334e864..cda83ca 100644
--- a/src/irc.h
+++ b/src/irc.h
@@ -106,6 +106,9 @@ struct network
 	struct server *serverv;
 };
 
+#define SASL_AUTH_EXTERNAL 1
+#define SASL_AUTH_PLAIN 2
+
 struct link {
 	char *name;	/* id */
 
@@ -153,6 +156,9 @@ struct link {
 	char *username;
 	char *realname;
 	char *s_password;
+	char *sasl_username;
+	char *sasl_password;
+	int sasl_mechanism;
 	char *connect_nick;
 
 	/* socket creation info */
diff --git a/src/lex.l b/src/lex.l
index 80e68d9..e828a59 100644
--- a/src/lex.l
+++ b/src/lex.l
@@ -115,6 +115,9 @@ list_t *parse_conf(FILE *file, int *err)
 "client_side_ssl_pem"	{ return LEX_CSS_PEM; }
 "client_side_ciphers"	{ return LEX_CSS_CIPHERS; }
 "client_side_dh_param"	{ return LEX_DH_PARAM; }
+"sasl_username"	{ return LEX_SASL_USERNAME; }
+"sasl_password"	{ return LEX_SASL_PASSWORD; }
+"sasl_mechanism"	{ return LEX_SASL_MECHANISM; }
 "ignore_server_capab"	{ return LEX_IGNORE_CAPAB; }
 "reconn_timer"		{ return LEX_RECONN_TIMER; }
 \"[^"]*\" 		{
diff --git a/src/utils/base64.c b/src/utils/base64.c
new file mode 100644
index 0000000..add00ae
--- /dev/null
+++ b/src/utils/base64.c
@@ -0,0 +1,93 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include "base64.h"
+
+static const unsigned char base64_table[65] =
+	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/**
+ * base64_encode - Base64 encode
+ * @src: Data to be encoded
+ * @len: Length of the data to be encoded
+ * @out_len: Pointer to output length variable, or %NULL if not used
+ * Returns: Allocated buffer of out_len bytes of encoded data,
+ * or %NULL on failure
+ *
+ * Caller is responsible for freeing the returned buffer. Returned buffer is
+ * nul terminated to make it easier to use as a C string. The nul terminator is
+ * not included in out_len.
+ *
+ * BIP change: remove line returns.
+ */
+unsigned char * base64_encode(const unsigned char *src, size_t len,
+			      size_t *out_len)
+{
+	unsigned char *out, *pos;
+	const unsigned char *end, *in;
+	size_t olen;
+	int line_len;
+
+	olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
+	olen += olen / 72; /* line feeds */
+	olen++; /* nul termination */
+	if (olen < len)
+		return NULL; /* integer overflow */
+	out = malloc(olen);
+	if (out == NULL)
+		return NULL;
+
+	end = src + len;
+	in = src;
+	pos = out;
+	line_len = 0;
+	while (end - in >= 3) {
+		*pos++ = base64_table[in[0] >> 2];
+		*pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
+		*pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
+		*pos++ = base64_table[in[2] & 0x3f];
+		in += 3;
+		line_len += 4;
+ 		/*
+		 *  BIP change: remove line returns.
+		if (line_len >= 72) {
+			*pos++ = '\n';
+			line_len = 0;
+		}
+		*/
+	}
+
+	if (end - in) {
+		*pos++ = base64_table[in[0] >> 2];
+		if (end - in == 1) {
+			*pos++ = base64_table[(in[0] & 0x03) << 4];
+			*pos++ = '=';
+		} else {
+			*pos++ = base64_table[((in[0] & 0x03) << 4) |
+					      (in[1] >> 4)];
+			*pos++ = base64_table[(in[1] & 0x0f) << 2];
+		}
+		*pos++ = '=';
+		line_len += 4;
+	}
+
+ 	/*
+	 *  BIP change: remove line returns.
+	if (line_len)
+		*pos++ = '\n';
+	*/
+
+	*pos = '\0';
+	if (out_len)
+		*out_len = (size_t)(pos - out);
+	return out;
+}
+
diff --git a/src/utils/base64.h b/src/utils/base64.h
new file mode 100644
index 0000000..81e1800
--- /dev/null
+++ b/src/utils/base64.h
@@ -0,0 +1,15 @@
+/*
+ * Base64 encoding/decoding (RFC1341)
+ * Copyright (c) 2005, Jouni Malinen <j@w1.fi>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef BASE64_H
+#define BASE64_H
+
+unsigned char * base64_encode(const unsigned char *src, size_t len,
+			      size_t *out_len);
+
+#endif /* BASE64_H */
-- 
2.30.2

