aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarius Halden <marius.h@lden.org>2016-06-13 01:35:48 +0200
committerMarius Halden <marius.h@lden.org>2016-06-26 14:28:58 +0200
commita110ae7c40ddd8ad099f94d7197848cf26f4511a (patch)
tree1a4ba4cb0c13be1f9fa3e56428bf3622bbd9db66
parent7949d5a9e21f48f4a41522d3c0377d0ea29b3eac (diff)
Add initial support for SCRAM-SHA-1
-rw-r--r--protocols/jabber/jabber.c2
-rw-r--r--protocols/jabber/jabber.h12
-rw-r--r--protocols/jabber/sasl.c230
3 files changed, 231 insertions, 13 deletions
diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c
index 11a90ff4..2fa19c0f 100644
--- a/protocols/jabber/jabber.c
+++ b/protocols/jabber/jabber.c
@@ -377,6 +377,8 @@ static void jabber_logout(struct im_connection *ic)
g_free(jd->muc_host);
g_free(jd->username);
g_free(jd->me);
+ g_free(jd->challenge.cnonce);
+ g_free(jd->challenge.server_signature);
g_free(jd);
jabber_connections = g_slist_remove(jabber_connections, ic);
diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h
index d76ee08f..5412a08f 100644
--- a/protocols/jabber/jabber.h
+++ b/protocols/jabber/jabber.h
@@ -77,6 +77,11 @@ typedef enum {
JCFLAG_ALWAYS_USE_NICKS = 2,
} jabber_chat_flags_t;
+typedef enum {
+ JCHALLENGE_DIGEST_MD5,
+ JCHALLENGE_SCRAM
+} jabber_challenge_t;
+
struct jabber_data {
struct im_connection *ic;
@@ -94,6 +99,13 @@ struct jabber_data {
char *me; /* bare jid */
char *internal_jid;
+ struct {
+ jabber_challenge_t type;
+ int scram_algo;
+ char *cnonce;
+ char *server_signature;
+ } challenge;
+
const struct oauth2_service *oauth2_service;
char *oauth2_access_token;
diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c
index 7778af1f..de91034b 100644
--- a/protocols/jabber/sasl.c
+++ b/protocols/jabber/sasl.c
@@ -22,11 +22,13 @@
\***************************************************************************/
#include <ctype.h>
+#include <gcrypt.h>
#include "jabber.h"
#include "base64.h"
#include "oauth2.h"
#include "oauth.h"
+#include "sha1.h"
const struct oauth2_service oauth2_service_google =
{
@@ -44,7 +46,7 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
struct jabber_data *jd = ic->proto_data;
struct xt_node *c, *reply;
char *s;
- int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0;
+ int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0;
int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE;
GString *mechs;
@@ -71,24 +73,26 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
mechs = g_string_new("");
c = node->children;
while ((c = xt_find_node(c, "mechanism"))) {
- if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
- sup_plain = 1;
- } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
- sup_digest = 1;
- } else if (c->text && g_strcasecmp(c->text, "ANONYMOUS") == 0) {
- sup_anonymous = 1;
- } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
- sup_gtalk = 1;
- }
-
if (c->text) {
+ if (g_strcasecmp(c->text, "PLAIN") == 0) {
+ sup_plain = 1;
+ } else if (g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
+ sup_digest = 1;
+ } else if (g_strcasecmp(c->text, "ANONYMOUS") == 0) {
+ sup_anonymous = 1;
+ } else if (g_strcasecmp(c->text, "X-OAUTH2") == 0) {
+ sup_gtalk = 1;
+ } else if (g_strcasecmp(c->text, "SCRAM-SHA-1") == 0) {
+ sup_scram = 1;
+ }
+
g_string_append_printf(mechs, " %s", c->text);
}
c = c->next;
}
- if (!want_oauth && !sup_plain && !sup_digest) {
+ if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) {
if (sup_gtalk) {
imcb_error(ic, "This server requires OAuth "
"(supported schemes:%s)", mechs->str);
@@ -139,11 +143,38 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
imc_logout(ic, FALSE);
xt_free_node(reply);
return XT_ABORT;
+ } else if (sup_scram) {
+ GString *gs;
+ int len;
+ unsigned char cnonce_bin[30];
+ char *cnonce;
+
+ jd->challenge.type = JCHALLENGE_SCRAM;
+ jd->challenge.scram_algo = GCRY_MD_SHA1;
+ xt_add_attr(reply, "mechanism", "SCRAM-SHA-1");
+
+ gs = g_string_sized_new(128);
+
+ random_bytes(cnonce_bin, sizeof(cnonce_bin));
+ cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
+
+ jd->challenge.cnonce = cnonce;
+
+ /* XXX: Saslprep username */
+ g_string_append_printf(gs, "n,,n=%s,r=%s", jd->username, cnonce);
+
+ len = gs->len;
+ s = g_string_free(gs, FALSE);
+
+ reply->text = base64_encode((unsigned char *)s , len);
+ reply->text_len = strlen(reply->text);
+ g_free(s);
} else if (sup_digest && !(jd->ssl && sup_plain)) {
/* Only try DIGEST-MD5 if there's no SSL/TLS or if PLAIN isn't supported.
* Which in practice means "don't bother with DIGEST-MD5 most of the time".
* It's weak, pointless over TLS, and often breaks with some servers (hi openfire) */
+ jd->challenge.type = JCHALLENGE_DIGEST_MD5;
xt_add_attr(reply, "mechanism", "DIGEST-MD5");
/* The rest will be done later, when we receive a <challenge/>. */
@@ -266,7 +297,7 @@ char *sasl_get_part(char *data, char *field)
}
}
-xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
+static xt_status sasl_pkt_challenge_digest_md5(struct xt_node *node, gpointer data)
{
struct im_connection *ic = data;
struct jabber_data *jd = ic->proto_data;
@@ -385,6 +416,175 @@ silent_error:
return ret;
}
+static void hmac(int algo, unsigned char *key, size_t key_len, unsigned char *buf, size_t buf_len, unsigned char *out)
+{
+ gcry_md_hd_t digest;
+ unsigned char *tmp;
+
+ gcry_md_open(&digest, algo, GCRY_MD_FLAG_HMAC);
+ gcry_md_setkey(digest, key, key_len);
+ gcry_md_write(digest, buf, buf_len);
+
+ tmp = gcry_md_read(digest, 0);
+ memcpy(out, tmp, gcry_md_get_algo_dlen(algo));
+
+ gcry_md_close(digest);
+}
+
+static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data)
+{
+ struct im_connection *ic = data;
+ struct jabber_data *jd = ic->proto_data;
+ struct xt_node *reply_pkt = NULL;
+ xt_status ret = XT_ABORT;
+ char *random = NULL, *salt64 = NULL, *iter_str = NULL, *encoded_proof = NULL,
+ *reply = NULL, *client_first_bare = NULL, *server_first = NULL,
+ *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL;
+ unsigned char *salt = NULL;
+ size_t salt_len, iter_count;
+
+ int algo = jd->challenge.scram_algo;
+ size_t md_len = gcry_md_get_algo_dlen(algo);
+
+ unsigned char client_key[md_len];
+ unsigned char client_proof[md_len];
+ unsigned char client_signature[md_len];
+ unsigned char salted_password[md_len];
+ unsigned char server_key[md_len];
+ unsigned char server_signature[md_len];
+ unsigned char stored_key[md_len];
+
+ if (node->text_len == 0) {
+ goto error;
+ }
+
+ server_first = frombase64(node->text);
+
+ random = sasl_get_part(server_first, "r");
+ salt64 = sasl_get_part(server_first, "s");
+ iter_str = sasl_get_part(server_first, "i");
+ iter_count = atoi(iter_str);
+
+ if (strncmp(random, jd->challenge.cnonce, strlen(jd->challenge.cnonce)) != 0) {
+ imcb_error(ic, "Server nonce doesn't start with client nonce");
+ goto error;
+ }
+
+ // XXX: Normalize password with saslprep
+
+ salt_len = base64_decode(salt64, &salt);
+
+ if (gcry_kdf_derive(ic->acc->pass, strlen(ic->acc->pass),
+ GCRY_KDF_PBKDF2, algo,
+ salt, salt_len, iter_count,
+ sizeof(salted_password), salted_password) != 0) {
+ imcb_error(ic, "gcrypt went booboo :(");
+ goto error;
+ }
+
+ hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Client Key", 10, client_key);
+
+ gcry_md_hash_buffer(algo, stored_key, client_key, sizeof(client_key));
+
+ client_first_bare = g_strdup_printf("n=%s,r=%s", jd->username, jd->challenge.cnonce);
+ client_final_noproof = g_strdup_printf("c=biws,r=%s", random);
+
+ auth_message = g_strdup_printf("%s,%s,%s", client_first_bare, server_first, client_final_noproof);
+
+ hmac(algo, stored_key, sizeof(stored_key), (unsigned char *)auth_message, strlen(auth_message), client_signature);
+
+ for (size_t i = 0; i < sizeof(client_key); i++) {
+ client_proof[i] = client_key[i] ^ client_signature[i];
+ }
+
+ encoded_proof = base64_encode(client_proof, sizeof(client_proof));
+
+ hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Server Key", 10, server_key);
+ hmac(algo, server_key, sizeof(server_key), (unsigned char *)auth_message, strlen(auth_message), server_signature);
+
+ jd->challenge.server_signature = base64_encode(server_signature, sizeof(server_signature));
+
+ client_final = g_strdup_printf("%s,p=%s", client_final_noproof, encoded_proof);
+ reply = tobase64(client_final);
+ reply_pkt = xt_new_node("response", reply, NULL);
+ xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL);
+
+ if (!jabber_write_packet(ic, reply_pkt)) {
+ goto silent_error;
+ }
+
+ ret = XT_HANDLED;
+ goto silent_error;
+
+error:
+ imcb_error(ic, "Incorrect SASL challenge received");
+ imc_logout(ic, FALSE);
+
+silent_error:
+ g_free(random);
+ g_free(salt64);
+ g_free(salt);
+ g_free(iter_str);
+ g_free(encoded_proof);
+ g_free(client_first_bare);
+ g_free(client_final_noproof);
+ g_free(client_final);
+ g_free(auth_message);
+ g_free(reply);
+ g_free(jd->challenge.cnonce);
+ jd->challenge.cnonce = NULL;
+ xt_free_node(reply_pkt);
+ return ret;
+}
+
+xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
+{
+ struct im_connection *ic = data;
+ struct jabber_data *jd = ic->proto_data;
+
+ switch (jd->challenge.type) {
+ case JCHALLENGE_DIGEST_MD5:
+ return sasl_pkt_challenge_digest_md5(node, data);
+
+ case JCHALLENGE_SCRAM:
+ return sasl_pkt_challenge_scram(node, data);
+
+ default:
+ imcb_error(ic, "Invalid challenge type");
+ return XT_ABORT;
+ }
+}
+
+static xt_status sasl_pkt_validate_scram_success(struct xt_node *node, gpointer data)
+{
+ struct im_connection *ic = data;
+ struct jabber_data *jd = ic->proto_data;
+ char *v, *dec;
+ xt_status ret = XT_HANDLED;
+
+ if (node->text_len == 0) {
+ imcb_error(ic, "Server didn't send signature");
+ imc_logout(ic, FALSE);
+ ret = XT_ABORT;
+ goto error;
+ }
+
+ dec = frombase64(node->text);
+ v = sasl_get_part(dec, "v");
+
+ if (g_strcmp0(v, jd->challenge.server_signature) != 0) {
+ imcb_error(ic, "Invalid server signature");
+ imc_logout(ic, FALSE);
+ ret = XT_ABORT;
+ goto error;
+ }
+
+error:
+ g_free(jd->challenge.server_signature);
+ jd->challenge.server_signature = NULL;
+ return ret;
+}
+
xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
{
struct im_connection *ic = data;
@@ -402,6 +602,10 @@ xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
imcb_log(ic, "Authentication finished");
jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
+ if (jd->challenge.type == JCHALLENGE_SCRAM) {
+ return sasl_pkt_validate_scram_success(node, data);
+ }
+
if (jd->flags & JFLAG_HIPCHAT) {
return hipchat_handle_success(ic, node);
}