diff --git a/src/common/hexchat.h b/src/common/hexchat.h index 074d5a22..3946b643 100644 --- a/src/common/hexchat.h +++ b/src/common/hexchat.h @@ -461,6 +461,12 @@ struct msproxy_state_t unsigned char seq_sent; /* seq number of last packet sent. */ }; +/* SASL Mechanisms */ +#define MECH_PLAIN 0 +#define MECH_BLOWFISH 1 +#define MECH_AES 2 +#define MECH_EXTERNAL 3 + typedef struct server { /* server control operations (in server*.c) */ @@ -598,9 +604,13 @@ typedef struct server unsigned int have_sasl:1; /* SASL capability */ unsigned int have_except:1; /* ban exemptions +e */ unsigned int have_invite:1; /* invite exemptions +I */ + unsigned int have_cert:1; /* have loaded a cert */ unsigned int using_cp1255:1; /* encoding is CP1255/WINDOWS-1255? */ unsigned int using_irc:1; /* encoding is "IRC" (CP1252/UTF-8 hybrid)? */ unsigned int use_who:1; /* whether to use WHO command to get dcc_ip */ + unsigned int sasl_mech; /* mechanism for sasl auth */ + unsigned int sent_saslauth:1; /* have sent AUTHENICATE yet */ + unsigned int sent_capend:1; /* have sent CAP END yet */ #ifdef USE_OPENSSL unsigned int use_ssl:1; /* is server SSL capable? */ unsigned int accept_invalid_cert:1;/* ignore result of server's cert. verify */ diff --git a/src/common/inbound.c b/src/common/inbound.c index bdac83f7..fca5f071 100644 --- a/src/common/inbound.c +++ b/src/common/inbound.c @@ -1566,8 +1566,6 @@ void inbound_cap_ack (server *serv, char *nick, char *extensions, const message_tags_data *tags_data) { - char *pass; /* buffer for SASL password */ - EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions, NULL, NULL, 0, tags_data->timestamp); @@ -1603,20 +1601,25 @@ inbound_cap_ack (server *serv, char *nick, char *extensions, if (strstr (extensions, "sasl") != NULL) { - char *user; - serv->have_sasl = TRUE; + serv->sent_saslauth = FALSE; - user = (((ircnet *)serv->network)->user) - ? (((ircnet *)serv->network)->user) : prefs.hex_irc_user_name; - - EMIT_SIGNAL_TIMESTAMP (XP_TE_SASLAUTH, serv->server_session, user, NULL, - NULL, NULL, 0, tags_data->timestamp); +#ifdef USE_OPENSSL + if (serv->loginmethod == LOGIN_SASLEXTERNAL) + { + serv->sasl_mech = MECH_EXTERNAL; + tcp_send_len (serv, "AUTHENTICATE EXTERNAL\r\n", 23); + } + else + { + /* default to most secure, it will fallback if not supported */ + serv->sasl_mech = MECH_AES; + tcp_send_len (serv, "AUTHENTICATE DH-AES\r\n", 21); + } +#else + serv->sasl_mech = MECH_PLAIN; tcp_send_len (serv, "AUTHENTICATE PLAIN\r\n", 20); - - pass = encode_sasl_pass (user, serv->password); - tcp_sendf (serv, "AUTHENTICATE %s\r\n", pass); - free (pass); +#endif } } @@ -1687,9 +1690,9 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str, } /* if the SASL password is set AND auth mode is set to SASL, request SASL auth */ - if (serv->loginmethod == LOGIN_SASL - && strcmp (extension, "sasl") == 0 - && strlen (serv->password) != 0) + if (!strcmp (extension, "sasl") + && ((serv->loginmethod == LOGIN_SASL && strlen (serv->password) != 0) + || (serv->loginmethod == LOGIN_SASLEXTERNAL && serv->have_cert))) { strcat (buffer, "sasl "); want_cap = 1; @@ -1710,6 +1713,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str, if (!want_sasl) { /* if we use SASL, CAP END is dealt via raw numerics */ + serv->sent_capend = TRUE; tcp_send_len (serv, "CAP END\r\n", 9); } } @@ -1717,6 +1721,7 @@ inbound_cap_ls (server *serv, char *nick, char *extensions_str, void inbound_cap_nak (server *serv, const message_tags_data *tags_data) { + serv->sent_capend = TRUE; tcp_send_len (serv, "CAP END\r\n", 9); } @@ -1727,3 +1732,69 @@ inbound_cap_list (server *serv, char *nick, char *extensions, EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions, NULL, NULL, 0, tags_data->timestamp); } + +static const char *sasl_mechanisms[] = +{ + "PLAIN", + "DH-BLOWFISH", + "DH-AES", + "EXTERNAL" +}; + +void +inbound_sasl_authenticate (server *serv, char *data) +{ + char *user, *pass = NULL; + const char *mech = sasl_mechanisms[serv->sasl_mech]; + + user = (((ircnet*)serv->network)->user) + ? (((ircnet*)serv->network)->user) : prefs.hex_irc_user_name; + + switch (serv->sasl_mech) + { + case MECH_PLAIN: + pass = encode_sasl_pass_plain (user, serv->password); + break; +#ifdef USE_OPENSSL + case MECH_BLOWFISH: + pass = encode_sasl_pass_blowfish (user, serv->password, data); + break; + case MECH_AES: + pass = encode_sasl_pass_aes (user, serv->password, data); + break; + case MECH_EXTERNAL: + pass = g_strdup ("+"); + break; +#endif + } + + if (pass == NULL) + { + /* something went wrong abort */ + serv->sent_saslauth = TRUE; /* prevent trying PLAIN */ + tcp_sendf (serv, "AUTHENTICATE *\r\n"); + return; + } + + serv->sent_saslauth = TRUE; + tcp_sendf (serv, "AUTHENTICATE %s\r\n", pass); + g_free (pass); + + + EMIT_SIGNAL_TIMESTAMP (XP_TE_SASLAUTH, serv->server_session, user, (char*)mech, + NULL, NULL, 0, 0); +} + +int +inbound_sasl_error (server *serv) +{ + /* If server sent 904 before we sent password, + * mech not support so fallback to next mech */ + if (!serv->sent_saslauth && serv->sasl_mech != MECH_EXTERNAL && serv->sasl_mech != MECH_PLAIN) + { + serv->sasl_mech -= 1; + tcp_sendf (serv, "AUTHENTICATE %s\r\n", sasl_mechanisms[serv->sasl_mech]); + return 1; + } + return 0; +} diff --git a/src/common/inbound.h b/src/common/inbound.h index cbb04890..40eeb8f6 100644 --- a/src/common/inbound.h +++ b/src/common/inbound.h @@ -95,6 +95,8 @@ void inbound_cap_ls (server *serv, char *nick, char *extensions, void inbound_cap_nak (server *serv, const message_tags_data *tags_data); void inbound_cap_list (server *serv, char *nick, char *extensions, const message_tags_data *tags_data); +void inbound_sasl_authenticate (server *serv, char *data); +int inbound_sasl_error (server *serv); void do_dns (session *sess, char *nick, char *host, const message_tags_data *tags_data); gboolean alert_match_word (char *word, char *masks); diff --git a/src/common/proto-irc.c b/src/common/proto-irc.c index 527a7605..250a2937 100644 --- a/src/common/proto-irc.c +++ b/src/common/proto-irc.c @@ -45,11 +45,11 @@ #include "url.h" #include "servlist.h" - static void irc_login (server *serv, char *user, char *realname) { tcp_sendf (serv, "CAP LS\r\n"); /* start with CAP LS as Charybdis sasl.txt suggests */ + serv->sent_capend = FALSE; /* track if we have finished */ if (serv->password[0] && serv->loginmethod == LOGIN_PASS) { @@ -953,14 +953,20 @@ process_numeric (session * sess, int n, tags_data->timestamp); break; case 903: /* successful SASL auth */ - case 904: /* aborted SASL auth */ + case 904: /* failed SASL auth */ + if (inbound_sasl_error (serv)) + break; /* might retry */ case 905: /* failed SASL auth */ - case 906: /* registration completes before SASL auth */ + case 906: /* aborted */ case 907: /* attempting to re-auth after a successful auth */ EMIT_SIGNAL_TIMESTAMP (XP_TE_SASLRESPONSE, serv->server_session, word[1], word[2], word[3], ++word_eol[4], 0, tags_data->timestamp); - tcp_send_len (serv, "CAP END\r\n", 9); + if (!serv->sent_capend) + { + serv->sent_capend = TRUE; + tcp_send_len (serv, "CAP END\r\n", 9); + } break; default: @@ -1145,7 +1151,6 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], case WORDL('N','O','T','I'): { int id = FALSE; /* identified */ - char *response; text = word_eol[4]; if (*text == ':') @@ -1153,9 +1158,10 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], text++; } +#ifdef USE_OPENSSL if (!strncmp (text, "CHALLENGE ", 10)) /* QuakeNet CHALLENGE upon our request */ { - response = challengeauth_response (((ircnet *)serv->network)->user ? ((ircnet *)serv->network)->user : prefs.hex_irc_user_name, serv->password, word[5]); + char *response = challengeauth_response (((ircnet *)serv->network)->user ? ((ircnet *)serv->network)->user : prefs.hex_irc_user_name, serv->password, word[5]); tcp_sendf (serv, "PRIVMSG %s :CHALLENGEAUTH %s %s %s\r\n", CHALLENGEAUTH_NICK, @@ -1166,6 +1172,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], g_free (response); return; /* omit the CHALLENGE ALGOS message */ } +#endif if (serv->have_idmsg) { @@ -1321,8 +1328,9 @@ process_named_servermsg (session *sess, char *buf, char *rawname, char *word_eol tags_data->timestamp); return; } - if (!strncmp (buf, "AUTHENTICATE +", 14)) /* omit SASL "empty" responses */ + if (!strncmp (buf, "AUTHENTICATE", 12)) { + inbound_sasl_authenticate (sess->server, word_eol[2]); return; } diff --git a/src/common/server.c b/src/common/server.c index e59a7ee3..eea7ce08 100644 --- a/src/common/server.c +++ b/src/common/server.c @@ -1049,7 +1049,8 @@ server_cleanup (server * serv) #ifdef USE_OPENSSL if (serv->ssl) { - _SSL_close (serv->ssl); + SSL_shutdown (serv->ssl); + SSL_free (serv->ssl); serv->ssl = NULL; } #endif @@ -1705,18 +1706,25 @@ server_connect (server *serv, char *hostname, int port, int no_login) if (serv->use_ssl) { char *cert_file; + serv->have_cert = FALSE; /* first try network specific cert/key */ cert_file = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "certs" G_DIR_SEPARATOR_S "%s.pem", get_xdir (), server_get_network (serv, TRUE)); if (SSL_CTX_use_certificate_file (ctx, cert_file, SSL_FILETYPE_PEM) == 1) - SSL_CTX_use_PrivateKey_file (ctx, cert_file, SSL_FILETYPE_PEM); + { + if (SSL_CTX_use_PrivateKey_file (ctx, cert_file, SSL_FILETYPE_PEM) == 1) + serv->have_cert = TRUE; + } else { /* if that doesn't exist, try /certs/client.pem */ cert_file = g_build_filename (get_xdir (), "certs", "client.pem", NULL); if (SSL_CTX_use_certificate_file (ctx, cert_file, SSL_FILETYPE_PEM) == 1) - SSL_CTX_use_PrivateKey_file (ctx, cert_file, SSL_FILETYPE_PEM); + { + if (SSL_CTX_use_PrivateKey_file (ctx, cert_file, SSL_FILETYPE_PEM) == 1) + serv->have_cert = TRUE; + } } g_free (cert_file); } diff --git a/src/common/servlist.h b/src/common/servlist.h index 45b6dad6..6d6f1bd3 100644 --- a/src/common/servlist.h +++ b/src/common/servlist.h @@ -79,6 +79,7 @@ extern GSList *network_list; #define LOGIN_PASS 7 #define LOGIN_CHALLENGEAUTH 8 #define LOGIN_CUSTOM 9 +#define LOGIN_SASLEXTERNAL 10 #define CHALLENGEAUTH_ALGO "HMAC-SHA-256" #define CHALLENGEAUTH_NICK "Q@CServe.quakenet.org" diff --git a/src/common/text.c b/src/common/text.c index b6ad378d..e0cdb5ee 100644 --- a/src/common/text.c +++ b/src/common/text.c @@ -1297,7 +1297,8 @@ static char * const pevt_generic_channel_help[] = { }; static char * const pevt_saslauth_help[] = { - N_("Username") + N_("Username"), + N_("Mechanism") }; static char * const pevt_saslresponse_help[] = { diff --git a/src/common/textevents.in b/src/common/textevents.in index 52b9e2ff..3631e363 100644 --- a/src/common/textevents.in +++ b/src/common/textevents.in @@ -703,8 +703,8 @@ pevt_resolvinguser_help SASL Authenticating XP_TE_SASLAUTH pevt_saslauth_help -%C23*%O$tAuthenticating via SASL as %C18$1%O -1 +%C23*%O$tAuthenticating via SASL as %C18$1%O (%C24$2%O) +2 SASL Response XP_TE_SASLRESPONSE diff --git a/src/common/util.c b/src/common/util.c index 52464621..9771b1f6 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -58,6 +58,17 @@ #include #endif +/* SASL mechanisms */ +#ifdef USE_OPENSSL +#include +#include +#include +#include +#ifndef WIN32 +#include +#endif +#endif + #ifndef HAVE_SNPRINTF #define snprintf g_snprintf #endif @@ -1929,7 +1940,7 @@ get_subdirs (const char *path) } char * -encode_sasl_pass (char *user, char *pass) +encode_sasl_pass_plain (char *user, char *pass) { int authlen; char *buffer; @@ -1944,6 +1955,230 @@ encode_sasl_pass (char *user, char *pass) return encoded; } +#ifdef USE_OPENSSL +/* Adapted from ZNC's SASL module */ + +static int +parse_dh (char *str, DH **dh_out, unsigned char **secret_out, int *keysize_out) +{ + DH *dh; + guchar *data, *decoded_data; + guchar *secret; + gsize data_len; + guint size; + guint16 size16; + BIGNUM *pubkey; + gint key_size; + + dh = DH_new(); + data = decoded_data = g_base64_decode (str, &data_len); + if (data_len < 2) + goto fail; + + /* prime number */ + memcpy (&size16, data, sizeof(size16)); + size = ntohs (size16); + data += 2; + data_len -= 2; + + if (size > data_len) + goto fail; + + dh->p = BN_bin2bn (data, size, NULL); + data += size; + + /* Generator */ + if (data_len < 2) + goto fail; + + memcpy (&size16, data, sizeof(size16)); + size = ntohs (size16); + data += 2; + data_len -= 2; + + if (size > data_len) + goto fail; + + dh->g = BN_bin2bn (data, size, NULL); + data += size; + + /* pub key */ + if (data_len < 2) + goto fail; + + memcpy (&size16, data, sizeof(size16)); + size = ntohs(size16); + data += 2; + data_len -= 2; + + pubkey = BN_bin2bn (data, size, NULL); + if (!(DH_generate_key (dh))) + goto fail; + + secret = (unsigned char*)malloc (DH_size(dh)); + key_size = DH_compute_key (secret, pubkey, dh); + if (key_size == -1) + goto fail; + + g_free (decoded_data); + + *dh_out = dh; + *secret_out = secret; + *keysize_out = key_size; + return 1; + +fail: + if (decoded_data) + g_free (decoded_data); + return 0; +} + +char * +encode_sasl_pass_blowfish (char *user, char *pass, char *data) +{ + DH *dh; + char *response, *ret; + unsigned char *secret; + unsigned char *encrypted_pass; + char *plain_pass; + BF_KEY key; + int key_size, length; + int pass_len = strlen (pass) + (8 - (strlen (pass) % 8)); + int user_len = strlen (user); + guint16 size16; + char *in_ptr, *out_ptr; + + if (!parse_dh (data, &dh, &secret, &key_size)) + return NULL; + BF_set_key (&key, key_size, secret); + + encrypted_pass = (guchar*)malloc (pass_len); + memset (encrypted_pass, 0, pass_len); + plain_pass = (char*)malloc (pass_len); + memset (plain_pass, 0, pass_len); + memcpy (plain_pass, pass, pass_len); + out_ptr = (char*)encrypted_pass; + in_ptr = (char*)plain_pass; + + for (length = pass_len; length; length -= 8, in_ptr += 8, out_ptr += 8) + BF_ecb_encrypt ((unsigned char*)in_ptr, (unsigned char*)out_ptr, &key, BF_ENCRYPT); + + /* Create response */ + length = 2 + BN_num_bytes (dh->pub_key) + pass_len + user_len + 1; + response = (char*)malloc (length); + out_ptr = response; + + /* our key */ + size16 = htons ((guint16)BN_num_bytes (dh->pub_key)); + memcpy (out_ptr, &size16, sizeof(size16)); + out_ptr += 2; + BN_bn2bin (dh->pub_key, (guchar*)out_ptr); + out_ptr += BN_num_bytes (dh->pub_key); + + /* username */ + memcpy (out_ptr, user, user_len + 1); + out_ptr += user_len + 1; + + /* pass */ + memcpy (out_ptr, encrypted_pass, pass_len); + + ret = g_base64_encode ((const guchar*)response, length); + + DH_free (dh); + free (plain_pass); + free (encrypted_pass); + free (secret); + free (response); + + return ret; +} + +char * +encode_sasl_pass_aes (char *user, char *pass, char *data) +{ + DH *dh; + AES_KEY key; + char *response = NULL; + char *out_ptr, *ret = NULL; + unsigned char *secret, *ptr; + unsigned char *encrypted_userpass, *plain_userpass; + int key_size, length; + guint16 size16; + unsigned char iv[16], iv_copy[16]; + int user_len = strlen (user) + 1; + int pass_len = strlen (pass) + 1; + int len = user_len + pass_len; + int padlen = 16 - (len % 16); + int userpass_len = len + padlen; + + if (!parse_dh (data, &dh, &secret, &key_size)) + return NULL; + + encrypted_userpass = (guchar*)malloc (userpass_len); + memset (encrypted_userpass, 0, userpass_len); + plain_userpass = (guchar*)malloc (userpass_len); + memset (plain_userpass, 0, userpass_len); + + /* create message */ + /* format of: \0\0 */ + ptr = plain_userpass; + memcpy (ptr, user, user_len); + ptr += user_len; + memcpy (ptr, pass, pass_len); + ptr += pass_len; + if (padlen) + { + /* Padding */ + unsigned char randbytes[16]; + if (!RAND_bytes (randbytes, padlen)) + goto end; + + memcpy (ptr, randbytes, padlen); + } + + if (!RAND_bytes (iv, sizeof (iv))) + goto end; + + memcpy (iv_copy, iv, sizeof(iv)); + + /* Encrypt */ + AES_set_encrypt_key (secret, key_size * 8, &key); + AES_cbc_encrypt(plain_userpass, encrypted_userpass, userpass_len, &key, iv_copy, AES_ENCRYPT); + + /* Create response */ + /* format of: */ + length = 2 + key_size + sizeof(iv) + userpass_len; + response = (char*)malloc (length); + out_ptr = response; + + /* our key */ + size16 = htons ((guint16)key_size); + memcpy (out_ptr, &size16, sizeof(size16)); + out_ptr += 2; + BN_bn2bin (dh->pub_key, (guchar*)out_ptr); + out_ptr += key_size; + + /* iv */ + memcpy (out_ptr, iv, sizeof(iv)); + out_ptr += sizeof(iv); + + /* userpass */ + memcpy (out_ptr, encrypted_userpass, userpass_len); + + ret = g_base64_encode ((const guchar*)response, length); + +end: + DH_free (dh); + free (plain_userpass); + free (encrypted_userpass); + free (secret); + if (response) + free (response); + + return ret; +} +#endif + #ifdef WIN32 int find_font (const char *fontname) @@ -1975,6 +2210,7 @@ find_font (const char *fontname) } #endif +#ifdef USE_OPENSSL static char * str_sha256hash (char *string) { @@ -2050,3 +2286,4 @@ challengeauth_response (char *username, char *password, char *challenge) return (char *) digest; } +#endif diff --git a/src/common/util.h b/src/common/util.h index 0ebd89d4..6b8d359c 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -79,7 +79,9 @@ void canonalize_key (char *key); int portable_mode (); int unity_mode (); GSList *get_subdirs (const char *path); -char *encode_sasl_pass (char *user, char *pass); +char *encode_sasl_pass_plain (char *user, char *pass); +char *encode_sasl_pass_blowfish (char *user, char *pass, char *data); +char *encode_sasl_pass_aes (char *user, char *pass, char *data); char *challengeauth_response (char *username, char *password, char *challenge); #endif diff --git a/src/fe-gtk/servlistgui.c b/src/fe-gtk/servlistgui.c index 94cb209f..ef5b57cd 100644 --- a/src/fe-gtk/servlistgui.c +++ b/src/fe-gtk/servlistgui.c @@ -120,10 +120,15 @@ static int login_types_conf[] = { LOGIN_DEFAULT, /* default entry - we don't use this but it makes indexing consistent with login_types[] so it's nice */ LOGIN_SASL, +#ifdef USE_OPENSSL + LOGIN_SASLEXTERNAL, +#endif LOGIN_PASS, LOGIN_MSG_NICKSERV, LOGIN_NICKSERV, +#ifdef USE_OPENSSL LOGIN_CHALLENGEAUTH, +#endif LOGIN_CUSTOM #if 0 LOGIN_NS, @@ -136,10 +141,15 @@ static const char *login_types[]= { "Default", "SASL (username + password)", +#ifdef USE_OPENSSL + "SASL EXTERNAL (cert)", +#endif "Server Password (/PASS password)", "NickServ (/MSG NickServ + password)", "NickServ (/NICKSERV + password)", +#ifdef USE_OPENSSL "Challenge Auth (username + password)", +#endif "Custom... (connect commands)", #if 0 "NickServ (/NS + password)", @@ -1513,6 +1523,12 @@ servlist_logintypecombo_cb (GtkComboBox *cb, gpointer *userdata) { gtk_notebook_set_current_page (GTK_NOTEBOOK (userdata), 2); /* FIXME avoid hardcoding? */ } + + /* EXTERNAL uses a cert, not a pass */ + if (login_types_conf[index] == LOGIN_SASLEXTERNAL) + gtk_widget_set_sensitive (edit_entry_pass, FALSE); + else + gtk_widget_set_sensitive (edit_entry_pass, TRUE); } @@ -1816,6 +1832,8 @@ servlist_open_edit (GtkWidget *parent, ircnet *net) edit_entry_pass = servlist_create_entry (table3, _("Password:"), 11, net->pass, 0, _("Password used for login. If in doubt, leave blank.")); gtk_entry_set_visibility (GTK_ENTRY (edit_entry_pass), FALSE); + if (selected_net && selected_net->logintype == LOGIN_SASLEXTERNAL) + gtk_widget_set_sensitive (edit_entry_pass, FALSE); label34 = gtk_label_new (_("Character set:")); gtk_table_attach (GTK_TABLE (table3), label34, 0, 1, 12, 13, (GtkAttachOptions) (GTK_FILL), (GtkAttachOptions) (0), SERVLIST_X_PADDING, SERVLIST_Y_PADDING);