From a110ae7c40ddd8ad099f94d7197848cf26f4511a Mon Sep 17 00:00:00 2001 From: Marius Halden Date: Mon, 13 Jun 2016 01:35:48 +0200 Subject: Add initial support for SCRAM-SHA-1 --- protocols/jabber/jabber.c | 2 + protocols/jabber/jabber.h | 12 +++ protocols/jabber/sasl.c | 230 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 231 insertions(+), 13 deletions(-) (limited to 'protocols') 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 +#include #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 . */ @@ -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); } -- cgit v1.2.3 From 8f02b17f4244f07d60cbedd2db039b616210d1f9 Mon Sep 17 00:00:00 2001 From: Marius Halden Date: Mon, 20 Jun 2016 18:20:11 +0200 Subject: Disable SCRAM if username/password contains non-ascii characters as saslprep is not yet used --- protocols/jabber/sasl.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'protocols') diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index de91034b..b60bdc7e 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -40,6 +40,20 @@ const struct oauth2_service oauth2_service_google = "6C-Zgf7Tr7gEQTPlBhMUgo7R", }; +static int is_ascii(const char *str) +{ + if (!str) { + return 0; + } + + while (*str) { + if (*str++ & 0x80) + return 0; + } + + return 1; +} + xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) { struct im_connection *ic = data; @@ -92,6 +106,11 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) c = c->next; } + if (sup_scram && (!is_ascii(jd->username) || !is_ascii(ic->acc->pass))) { + imcb_log(ic, "Username/password contains non-ascii characters, SCRAM authentication disabled"); + sup_scram = 0; + } + if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) { if (sup_gtalk) { imcb_error(ic, "This server requires OAuth " -- cgit v1.2.3 From 3fa2246938b57a2b110714aaf6b931cbef309ff0 Mon Sep 17 00:00:00 2001 From: Marius Halden Date: Mon, 20 Jun 2016 18:41:00 +0200 Subject: Add support for SCRAM-SHA-256 With this aproach it will be simple to add any hash to the scram implementation with some simple boilerplate. --- protocols/jabber/jabber.h | 5 +++++ protocols/jabber/sasl.c | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) (limited to 'protocols') diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h index 5412a08f..3445e4d7 100644 --- a/protocols/jabber/jabber.h +++ b/protocols/jabber/jabber.h @@ -82,6 +82,11 @@ typedef enum { JCHALLENGE_SCRAM } jabber_challenge_t; +typedef enum { + JSCRAM_SHA1 = 0x0001, + JSCRAM_SHA256 = 0x0002 +} jabber_scram_t; + struct jabber_data { struct im_connection *ic; diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index b60bdc7e..c8aad776 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -97,7 +97,9 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) } 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; + sup_scram = JSCRAM_SHA1; + } else if (g_strcasecmp(c->text, "SCRAM-SHA-256") == 0) { + sup_scram = JSCRAM_SHA256; } g_string_append_printf(mechs, " %s", c->text); @@ -168,9 +170,18 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) 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"); + if (sup_scram & JSCRAM_SHA256) { + jd->challenge.type = JCHALLENGE_SCRAM; + jd->challenge.scram_algo = GCRY_MD_SHA256; + xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); + } else if (sup_scram & JSCRAM_SHA1) { + jd->challenge.type = JCHALLENGE_SCRAM; + jd->challenge.scram_algo = GCRY_MD_SHA1; + xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); + } else { + imcb_error(ic, "Unknown scram method"); /* Just in case, but we should not get here */ + return XT_ABORT; + } gs = g_string_sized_new(128); -- cgit v1.2.3 From 311758008fbbe3eed1f50fdea4dc62644df9e455 Mon Sep 17 00:00:00 2001 From: Marius Halden Date: Mon, 20 Jun 2016 18:50:46 +0200 Subject: Follow c89/90 --- protocols/jabber/sasl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'protocols') diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index c8aad776..10fbe270 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -471,7 +471,7 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) *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; + size_t salt_len, iter_count, i; int algo = jd->challenge.scram_algo; size_t md_len = gcry_md_get_algo_dlen(algo); @@ -523,7 +523,7 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) 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++) { + for (i = 0; i < sizeof(client_key); i++) { client_proof[i] = client_key[i] ^ client_signature[i]; } -- cgit v1.2.3 From b3f154dd5c4d14fb0b466ffe347084ae7eac2295 Mon Sep 17 00:00:00 2001 From: jgeboski Date: Wed, 22 Jun 2016 14:54:52 -0400 Subject: Added an interface for the listing of existing chatrooms Several protocols can provide a list of existing chatrooms that a user is able join. This is crucial for the usage of several protocols, most notably Purple and Facebook. Plugins wishing to support this extended functionality must implement the new prpl->chat_list() function. This implemented function will in most cases send a remote request for the list of chatrooms. Once the list of chatrooms is obtained, a bee_chat_info_t GSList must be created and assigned to the im_connection->chatlist field. Then a call to the bee_chat_list_finish() is needed to display the list to the user. The chat list is maintained entirely by the plugin, so it is important to ensure all pointers related to the chat list remain valid until the chat list is set to NULL. This list is used internally by bitlbee to calculate indexes, which then allows the user to join a chat with an index, rather than some random identifier. It also important to ensure the list is properly freed whenever it is updated, or when the account is disconnect via the prpl->logout() function. On the user interface side of things, the 'chat list' subcommand was recommissioned. For a user to list the existing chat rooms: chat list Afterwards a user can join a chatroom in the list with its index. This extends the functionality of the 'chat add' subcommand by adding in support for the exclamation point operator to denote an index. chat add ! [channel] --- protocols/bee.h | 10 ++++++++++ protocols/bee_chat.c | 5 +++++ protocols/nogaim.h | 8 ++++++++ 3 files changed, 23 insertions(+) (limited to 'protocols') diff --git a/protocols/bee.h b/protocols/bee.h index d22e4d85..8e665ac8 100644 --- a/protocols/bee.h +++ b/protocols/bee.h @@ -83,6 +83,14 @@ typedef struct bee_user { void *data; /* Can be used by the IM module. */ } bee_user_t; +typedef struct bee_chat_info { + char *title; + char *topic; + + /* If less than zero, the user count is ignored when displaying */ + int userc; +} bee_chat_info_t; + /* This one's mostly used so save space and make it easier (cheaper) to compare groups of contacts, etc. */ typedef struct bee_group { @@ -184,4 +192,6 @@ G_MODULE_EXPORT int bee_chat_msg(bee_t *bee, struct groupchat *c, const char *ms G_MODULE_EXPORT struct groupchat *bee_chat_by_title(bee_t *bee, struct im_connection *ic, const char *title); G_MODULE_EXPORT void imcb_chat_invite(struct im_connection *ic, const char *name, const char *who, const char *msg); +G_MODULE_EXPORT void bee_chat_list_finish(struct im_connection *ic); + #endif /* __BEE_H__ */ diff --git a/protocols/bee_chat.c b/protocols/bee_chat.c index 2fcb0396..76ed7f85 100644 --- a/protocols/bee_chat.c +++ b/protocols/bee_chat.c @@ -273,3 +273,8 @@ void imcb_chat_invite(struct im_connection *ic, const char *name, const char *wh ic->bee->ui->chat_invite(ic->bee, bu, name, msg); } } + +void bee_chat_list_finish(struct im_connection *ic) +{ + cmd_chat_list_finish(ic); +} diff --git a/protocols/nogaim.h b/protocols/nogaim.h index e5569313..b2ae2cae 100644 --- a/protocols/nogaim.h +++ b/protocols/nogaim.h @@ -95,6 +95,7 @@ struct im_connection { bee_t *bee; GSList *groupchats; + GSList *chatlist; }; struct groupchat { @@ -262,6 +263,13 @@ struct prpl { /* If null, equivalent to handle_cmp( ic->acc->user, who ) */ gboolean (* handle_is_self) (struct im_connection *, const char *who); + /* This sets/updates the im_connection->chatlist field with a + * bee_chat_info_t GSList. This function should ensure the + * bee_chat_list_finish() function gets called at some point + * after the chat list is completely updated. + */ + void (*chat_list) (struct im_connection *, const char *server); + /* Some placeholders so eventually older plugins may cooperate with newer BitlBees. */ void *resv1; void *resv2; -- cgit v1.2.3 From f4e69f5a4779bfa6ec4dada59a49aeeca69bd395 Mon Sep 17 00:00:00 2001 From: jgeboski Date: Fri, 24 Jun 2016 16:21:42 -0400 Subject: purple: added room listing support --- protocols/purple/bpurple.h | 7 +++ protocols/purple/purple.c | 124 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) (limited to 'protocols') diff --git a/protocols/purple/bpurple.h b/protocols/purple/bpurple.h index 39677b86..c7b3a59c 100644 --- a/protocols/purple/bpurple.h +++ b/protocols/purple/bpurple.h @@ -14,4 +14,11 @@ struct purple_data guint next_request_id; }; +struct purple_roomlist_data +{ + GSList *chats; + gint topic; + gint userc; +}; + #endif /* !BPURPLE_H */ diff --git a/protocols/purple/purple.c b/protocols/purple/purple.c index 6ea2d7d8..bee45607 100644 --- a/protocols/purple/purple.c +++ b/protocols/purple/purple.c @@ -353,6 +353,21 @@ static void purple_login(account_t *acc) } } +static void purple_chatlist_free(struct im_connection *ic) +{ + bee_chat_info_t *ci; + GSList *l = ic->chatlist; + + while (l) { + ci = l->data; + l = g_slist_delete_link(l, l); + + g_free(ci->title); + g_free(ci->topic); + g_free(ci); + } +} + static void purple_logout(struct im_connection *ic) { struct purple_data *pd = ic->proto_data; @@ -368,6 +383,7 @@ static void purple_logout(struct im_connection *ic) purple_account_set_enabled(pd->account, "BitlBee", FALSE); purple_connections = g_slist_remove(purple_connections, ic); purple_accounts_remove(pd->account); + purple_chatlist_free(ic); g_hash_table_destroy(pd->input_requests); g_free(pd); } @@ -731,6 +747,13 @@ struct groupchat *purple_chat_join(struct im_connection *ic, const char *room, c return imcb_chat_new(ic, room); } +void purple_chat_list(struct im_connection *ic, const char *server) +{ + struct purple_data *pd = ic->proto_data; + + purple_roomlist_get_list(pd->account->gc); +} + void purple_transfer_request(struct im_connection *ic, file_transfer_t *ft, char *handle); static void purple_ui_init(); @@ -1266,6 +1289,105 @@ static PurplePrivacyUiOps bee_privacy_uiops = prplcb_privacy_deny_removed, /* deny_removed */ }; +static void prplcb_roomlist_create(PurpleRoomlist *list) +{ + struct purple_roomlist_data *rld; + + list->ui_data = rld = g_new0(struct purple_roomlist_data, 1); + rld->topic = -1; + rld->userc = -1; +} + +static void prplcb_roomlist_set_fields(PurpleRoomlist *list, GList *fields) +{ + GList *l; + guint i; + PurpleRoomlistField *field; + struct purple_roomlist_data *rld = list->ui_data; + + for (i = 0, l = fields; l; i++, l = l->next) { + field = l->data; + + if ((g_strcasecmp(field->name, "description") == 0) || + (g_strcasecmp(field->name, "topic") == 0)) { + if (field->type == PURPLE_ROOMLIST_FIELD_STRING) { + rld->topic = i; + } + } else if (g_strcasecmp(field->name, "users") == 0) { + if (field->type == PURPLE_ROOMLIST_FIELD_INT) { + rld->userc = i; + } + } + } +} + +static void prplcb_roomlist_add_room(PurpleRoomlist *list, PurpleRoomlistRoom *room) +{ + bee_chat_info_t *ci; + const char *title; + const char *topic; + GList *fields; + gpointer data; + struct purple_roomlist_data *rld = list->ui_data; + + fields = purple_roomlist_room_get_fields(room); + title = purple_roomlist_room_get_name(room); + + if (rld->topic >= 0) { + topic = g_list_nth_data(fields, rld->topic); + } else { + topic = NULL; + } + + ci = g_new(bee_chat_info_t, 1); + ci->title = g_strdup(title); + ci->topic = g_strdup(topic); + rld->chats = g_slist_prepend(rld->chats, ci); + + if (rld->userc >= 0) { + data = g_list_nth_data(fields, rld->userc); + ci->userc = GPOINTER_TO_INT(data); + } else { + ci->userc = -1; + } +} + +static void prplcb_roomlist_in_progress(PurpleRoomlist *list, gboolean in_progress) +{ + struct im_connection *ic; + struct purple_roomlist_data *rld = list->ui_data; + + if (in_progress) { + return; + } + + ic = purple_ic_by_pa(list->account); + purple_chatlist_free(ic); + + ic->chatlist = g_slist_reverse(rld->chats); + rld->chats = NULL; + + bee_chat_list_finish(ic); + purple_roomlist_unref(list); +} + +static void prplcb_roomlist_destroy(PurpleRoomlist *list) +{ + /* bee_chat_list_finish() frees rld->chats */ + g_free(list->ui_data); + list->ui_data = NULL; +} + +static PurpleRoomlistUiOps bee_roomlist_uiops = +{ + NULL, /* show_with_account */ + prplcb_roomlist_create, /* create */ + prplcb_roomlist_set_fields, /* set_fields */ + prplcb_roomlist_add_room, /* add_room */ + prplcb_roomlist_in_progress, /* in_progress */ + prplcb_roomlist_destroy, /* destroy */ +}; + static void prplcb_debug_print(PurpleDebugLevel level, const char *category, const char *arg_s) { fprintf(stderr, "DEBUG %s: %s", category, arg_s); @@ -1430,6 +1552,7 @@ static void purple_ui_init() purple_conversations_set_ui_ops(&bee_conv_uiops); purple_request_set_ui_ops(&bee_request_uiops); purple_privacy_set_ui_ops(&bee_privacy_uiops); + purple_roomlist_set_ui_ops(&bee_roomlist_uiops); purple_notify_set_ui_ops(&bee_notify_uiops); purple_accounts_set_ui_ops(&bee_account_uiops); purple_xfers_set_ui_ops(&bee_xfer_uiops); @@ -1517,6 +1640,7 @@ void purple_initmodule() funcs.chat_kick = purple_chat_kick; funcs.chat_leave = purple_chat_leave; funcs.chat_join = purple_chat_join; + funcs.chat_list = purple_chat_list; funcs.transfer_request = purple_transfer_request; help = g_string_new("BitlBee libpurple module supports the following IM protocols:\n"); -- cgit v1.2.3 From 405a14e9ea1da8332d1550c7d08bc31ca469272e Mon Sep 17 00:00:00 2001 From: Marius Halden Date: Wed, 6 Jul 2016 18:03:30 +0200 Subject: Use saslprep, more cb stuff --- protocols/jabber/jabber.c | 4 ++ protocols/jabber/jabber.h | 1 + protocols/jabber/sasl.c | 105 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 87 insertions(+), 23 deletions(-) (limited to 'protocols') diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c index 2fa19c0f..fcd90598 100644 --- a/protocols/jabber/jabber.c +++ b/protocols/jabber/jabber.c @@ -116,6 +116,9 @@ static void jabber_init(account_t *acc) s = set_add(&acc->set, "carbons", "true", set_eval_bool, acc); s->flags |= ACC_SET_OFFLINE_ONLY; + s = set_add(&acc->set, "disable_scram", "false", set_eval_bool, acc); + s->flags |= SET_HIDDEN_DEFAULT; + acc->flags |= ACC_FLAG_AWAY_MESSAGE | ACC_FLAG_STATUS_MESSAGE | ACC_FLAG_HANDLE_DOMAINS; } @@ -379,6 +382,7 @@ static void jabber_logout(struct im_connection *ic) g_free(jd->me); g_free(jd->challenge.cnonce); g_free(jd->challenge.server_signature); + g_free(jd->challenge.cb_header); g_free(jd); jabber_connections = g_slist_remove(jabber_connections, ic); diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h index 3445e4d7..c97c5b71 100644 --- a/protocols/jabber/jabber.h +++ b/protocols/jabber/jabber.h @@ -109,6 +109,7 @@ struct jabber_data { int scram_algo; char *cnonce; char *server_signature; + char *cb_header; } challenge; const struct oauth2_service *oauth2_service; diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index 10fbe270..e3467d86 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -23,6 +23,8 @@ #include #include +#include +#include #include "jabber.h" #include "base64.h" @@ -40,7 +42,7 @@ const struct oauth2_service oauth2_service_google = "6C-Zgf7Tr7gEQTPlBhMUgo7R", }; -static int is_ascii(const char *str) +/*static int is_ascii(const char *str) { if (!str) { return 0; @@ -52,7 +54,7 @@ static int is_ascii(const char *str) } return 1; -} +}*/ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) { @@ -60,7 +62,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, sup_scram = 0; + int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0, sup_scram_cb = 0; int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE; GString *mechs; @@ -97,9 +99,15 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) } else if (g_strcasecmp(c->text, "X-OAUTH2") == 0) { sup_gtalk = 1; } else if (g_strcasecmp(c->text, "SCRAM-SHA-1") == 0) { - sup_scram = JSCRAM_SHA1; + sup_scram |= JSCRAM_SHA1; + } else if (g_strcasecmp(c->text, "SCRAM-SHA-1-PLUS") == 0) { + sup_scram |= JSCRAM_SHA1; + sup_scram_cb |= JSCRAM_SHA1; } else if (g_strcasecmp(c->text, "SCRAM-SHA-256") == 0) { - sup_scram = JSCRAM_SHA256; + sup_scram |= JSCRAM_SHA256; + } else if (g_strcasecmp(c->text, "SCRAM-SHA-256-PLUS") == 0) { + sup_scram |= JSCRAM_SHA256; + sup_scram_cb |= JSCRAM_SHA256; } g_string_append_printf(mechs, " %s", c->text); @@ -108,9 +116,14 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) c = c->next; } - if (sup_scram && (!is_ascii(jd->username) || !is_ascii(ic->acc->pass))) { + /*if (sup_scram && (!is_ascii(jd->username) || !is_ascii(ic->acc->pass))) { imcb_log(ic, "Username/password contains non-ascii characters, SCRAM authentication disabled"); sup_scram = 0; + }*/ + + if (sup_scram_cb && !jd->ssl) { + imcb_log(ic, "Not connected over TLS, SCRAM Channel bindings disabled"); + sup_scram_cb = 0; } if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) { @@ -164,20 +177,28 @@ 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) { + } else if (sup_scram && !set_getbool(&ic->acc->set, "disable_scram")) { GString *gs; - int len; + int len, r; unsigned char cnonce_bin[30]; - char *cnonce; + char *cnonce, *puser = NULL; - if (sup_scram & JSCRAM_SHA256) { + if (sup_scram & JSCRAM_SHA256 && (!sup_scram_cb || (sup_scram_cb & JSCRAM_SHA256))) { jd->challenge.type = JCHALLENGE_SCRAM; jd->challenge.scram_algo = GCRY_MD_SHA256; - xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); - } else if (sup_scram & JSCRAM_SHA1) { + if (sup_scram_cb) { + xt_add_attr(reply, "mechanism", "SCRAM-SHA-256-PLUS"); + } else { + xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); + } + } else if (sup_scram & JSCRAM_SHA1 && (!sup_scram_cb || (sup_scram_cb & JSCRAM_SHA1))) { jd->challenge.type = JCHALLENGE_SCRAM; jd->challenge.scram_algo = GCRY_MD_SHA1; - xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); + if (sup_scram_cb) { + xt_add_attr(reply, "mechanism", "SCRAM-SHA-1-PLUS"); + } else { + xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); + } } else { imcb_error(ic, "Unknown scram method"); /* Just in case, but we should not get here */ return XT_ABORT; @@ -190,14 +211,22 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) jd->challenge.cnonce = cnonce; - /* XXX: Saslprep username */ - g_string_append_printf(gs, "n,,n=%s,r=%s", jd->username, cnonce); + r = stringprep_profile(jd->username, &puser, "SASLprep", 0); + if (r != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(r)); + return XT_ABORT; + } + + //jd->challenge.cb_header = g_strdup_printf("%s,,", sup_scram_cb ? "y" : "p=tls-unique"); + jd->challenge.cb_header = g_strdup_printf("%s,,", "n"); + g_string_append_printf(gs, "%sn=%s,r=%s", jd->challenge.cb_header, puser, 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(puser); 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. @@ -468,12 +497,13 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer 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; + *reply = NULL, *client_first_bare = NULL, *server_first = NULL, *cb_header64 = NULL, + *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL, *puser = NULL, *ppass = NULL; + unsigned char *salt = NULL; size_t salt_len, iter_count, i; - int algo = jd->challenge.scram_algo; + int algo = jd->challenge.scram_algo, r; size_t md_len = gcry_md_get_algo_dlen(algo); unsigned char client_key[md_len]; @@ -500,15 +530,24 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) goto error; } - // XXX: Normalize password with saslprep + r = stringprep_profile(jd->username, &puser, "SASLprep", 0); + if (r != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(r)); + goto error; + } + r = stringprep_profile(ic->acc->pass, &ppass, "SASLprep", 0); + if (r != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(r)); + goto error; + } salt_len = base64_decode(salt64, &salt); - if (gcry_kdf_derive(ic->acc->pass, strlen(ic->acc->pass), + if (gcry_kdf_derive(ppass, strlen(ppass), GCRY_KDF_PBKDF2, algo, salt, salt_len, iter_count, sizeof(salted_password), salted_password) != 0) { - imcb_error(ic, "gcrypt went booboo :("); + imcb_error(ic, "PBKDF failed"); goto error; } @@ -516,8 +555,23 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) 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); + /*if (jd->challenge.cb_header[0] == 'p') { + gnutls_datum_t cb; + int rc; + + rc = gnutls_session_channel_binding(jd->ssl, GNUTLS_CB_TLS_UNIQUE, &cb); + if (rc) { + imcb_log(ic, "Chanel binding error: %s", gnutls_strerror(rc)); + return XT_ABORT; + } + + for (size_t i = 0; i < cb.size; i++) + fprintf(stderr, "%02x", cb.data[i]); + fprintf(stderr, "\n"); + }*/ + cb_header64 = tobase64(jd->challenge.cb_header); + client_first_bare = g_strdup_printf("n=%s,r=%s", puser, jd->challenge.cnonce); + client_final_noproof = g_strdup_printf("c=%s,r=%s", cb_header64, random); auth_message = g_strdup_printf("%s,%s,%s", client_first_bare, server_first, client_final_noproof); @@ -551,11 +605,14 @@ error: imc_logout(ic, FALSE); silent_error: + g_free(puser); + g_free(ppass); g_free(random); g_free(salt64); g_free(salt); g_free(iter_str); g_free(encoded_proof); + g_free(cb_header64); g_free(client_first_bare); g_free(client_final_noproof); g_free(client_final); @@ -563,6 +620,8 @@ silent_error: g_free(reply); g_free(jd->challenge.cnonce); jd->challenge.cnonce = NULL; + g_free(jd->challenge.cb_header); + jd->challenge.cb_header = NULL; xt_free_node(reply_pkt); return ret; } -- cgit v1.2.3 From 7e41de51bd6466306f21e1d2bf54b3251c1bf08e Mon Sep 17 00:00:00 2001 From: Marius Halden Date: Wed, 6 Jul 2016 21:02:43 +0200 Subject: Remove cb stuff and some cleanup --- protocols/jabber/sasl.c | 106 +++++++++++------------------------------------- 1 file changed, 23 insertions(+), 83 deletions(-) (limited to 'protocols') diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index e3467d86..f0381ee8 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -42,27 +42,13 @@ const struct oauth2_service oauth2_service_google = "6C-Zgf7Tr7gEQTPlBhMUgo7R", }; -/*static int is_ascii(const char *str) -{ - if (!str) { - return 0; - } - - while (*str) { - if (*str++ & 0x80) - return 0; - } - - return 1; -}*/ - xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) { struct im_connection *ic = 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, sup_scram = 0, sup_scram_cb = 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; @@ -100,14 +86,8 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) sup_gtalk = 1; } else if (g_strcasecmp(c->text, "SCRAM-SHA-1") == 0) { sup_scram |= JSCRAM_SHA1; - } else if (g_strcasecmp(c->text, "SCRAM-SHA-1-PLUS") == 0) { - sup_scram |= JSCRAM_SHA1; - sup_scram_cb |= JSCRAM_SHA1; } else if (g_strcasecmp(c->text, "SCRAM-SHA-256") == 0) { sup_scram |= JSCRAM_SHA256; - } else if (g_strcasecmp(c->text, "SCRAM-SHA-256-PLUS") == 0) { - sup_scram |= JSCRAM_SHA256; - sup_scram_cb |= JSCRAM_SHA256; } g_string_append_printf(mechs, " %s", c->text); @@ -116,16 +96,6 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) c = c->next; } - /*if (sup_scram && (!is_ascii(jd->username) || !is_ascii(ic->acc->pass))) { - imcb_log(ic, "Username/password contains non-ascii characters, SCRAM authentication disabled"); - sup_scram = 0; - }*/ - - if (sup_scram_cb && !jd->ssl) { - imcb_log(ic, "Not connected over TLS, SCRAM Channel bindings disabled"); - sup_scram_cb = 0; - } - if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) { if (sup_gtalk) { imcb_error(ic, "This server requires OAuth " @@ -178,53 +148,37 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data) xt_free_node(reply); return XT_ABORT; } else if (sup_scram && !set_getbool(&ic->acc->set, "disable_scram")) { - GString *gs; - int len, r; + int rc; unsigned char cnonce_bin[30]; - char *cnonce, *puser = NULL; + char *puser = NULL; - if (sup_scram & JSCRAM_SHA256 && (!sup_scram_cb || (sup_scram_cb & JSCRAM_SHA256))) { + if (sup_scram & JSCRAM_SHA256) { jd->challenge.type = JCHALLENGE_SCRAM; jd->challenge.scram_algo = GCRY_MD_SHA256; - if (sup_scram_cb) { - xt_add_attr(reply, "mechanism", "SCRAM-SHA-256-PLUS"); - } else { - xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); - } - } else if (sup_scram & JSCRAM_SHA1 && (!sup_scram_cb || (sup_scram_cb & JSCRAM_SHA1))) { + xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); + } else if (sup_scram & JSCRAM_SHA1) { jd->challenge.type = JCHALLENGE_SCRAM; jd->challenge.scram_algo = GCRY_MD_SHA1; - if (sup_scram_cb) { - xt_add_attr(reply, "mechanism", "SCRAM-SHA-1-PLUS"); - } else { - xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); - } + xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); } else { imcb_error(ic, "Unknown scram method"); /* Just in case, but we should not get here */ return XT_ABORT; } - 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; - - r = stringprep_profile(jd->username, &puser, "SASLprep", 0); - if (r != STRINGPREP_OK) { - imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(r)); + rc = stringprep_profile(jd->username, &puser, "SASLprep", 0); + if (rc != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); return XT_ABORT; } - //jd->challenge.cb_header = g_strdup_printf("%s,,", sup_scram_cb ? "y" : "p=tls-unique"); - jd->challenge.cb_header = g_strdup_printf("%s,,", "n"); - g_string_append_printf(gs, "%sn=%s,r=%s", jd->challenge.cb_header, puser, cnonce); + random_bytes(cnonce_bin, sizeof(cnonce_bin)); + jd->challenge.cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin)); + + jd->challenge.cb_header = g_strdup("n,,"); - len = gs->len; - s = g_string_free(gs, FALSE); + s = g_strdup_printf("%sn=%s,r=%s", jd->challenge.cb_header, puser, jd->challenge.cnonce); - reply->text = base64_encode((unsigned char *)s , len); + reply->text = base64_encode((unsigned char *)s , strlen(s)); reply->text_len = strlen(reply->text); g_free(puser); g_free(s); @@ -503,7 +457,7 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) unsigned char *salt = NULL; size_t salt_len, iter_count, i; - int algo = jd->challenge.scram_algo, r; + int algo = jd->challenge.scram_algo, rc; size_t md_len = gcry_md_get_algo_dlen(algo); unsigned char client_key[md_len]; @@ -530,14 +484,14 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) goto error; } - r = stringprep_profile(jd->username, &puser, "SASLprep", 0); - if (r != STRINGPREP_OK) { - imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(r)); + rc = stringprep_profile(jd->username, &puser, "SASLprep", 0); + if (rc != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); goto error; } - r = stringprep_profile(ic->acc->pass, &ppass, "SASLprep", 0); - if (r != STRINGPREP_OK) { - imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(r)); + rc = stringprep_profile(ic->acc->pass, &ppass, "SASLprep", 0); + if (rc != STRINGPREP_OK) { + imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); goto error; } @@ -555,20 +509,6 @@ static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) gcry_md_hash_buffer(algo, stored_key, client_key, sizeof(client_key)); - /*if (jd->challenge.cb_header[0] == 'p') { - gnutls_datum_t cb; - int rc; - - rc = gnutls_session_channel_binding(jd->ssl, GNUTLS_CB_TLS_UNIQUE, &cb); - if (rc) { - imcb_log(ic, "Chanel binding error: %s", gnutls_strerror(rc)); - return XT_ABORT; - } - - for (size_t i = 0; i < cb.size; i++) - fprintf(stderr, "%02x", cb.data[i]); - fprintf(stderr, "\n"); - }*/ cb_header64 = tobase64(jd->challenge.cb_header); client_first_bare = g_strdup_printf("n=%s,r=%s", puser, jd->challenge.cnonce); client_final_noproof = g_strdup_printf("c=%s,r=%s", cb_header64, random); -- cgit v1.2.3