source: protocols/jabber/sasl.c @ 71f87ba

Last change on this file since 71f87ba was 71f87ba, checked in by dequis <dx@…>, at 2015-04-05T22:47:15Z

Adapt old subprotocol checks to the last commit

  • Property mode set to 100644
File size: 15.4 KB
RevLine 
[5997488]1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Jabber module - SASL authentication                                      *
5*                                                                           *
[0e788f5]6*  Copyright 2006-2012 Wilmer van der Gaast <wilmer@gaast.net>              *
[5997488]7*                                                                           *
8*  This program is free software; you can redistribute it and/or modify     *
9*  it under the terms of the GNU General Public License as published by     *
10*  the Free Software Foundation; either version 2 of the License, or        *
11*  (at your option) any later version.                                      *
12*                                                                           *
13*  This program is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            *
16*  GNU General Public License for more details.                             *
17*                                                                           *
18*  You should have received a copy of the GNU General Public License along  *
19*  with this program; if not, write to the Free Software Foundation, Inc.,  *
20*  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              *
21*                                                                           *
22\***************************************************************************/
23
[af97b23]24#include <ctype.h>
25
[5997488]26#include "jabber.h"
27#include "base64.h"
[57b4525]28#include "oauth2.h"
[e1c926f]29#include "oauth.h"
[5997488]30
[18c6d36]31const struct oauth2_service oauth2_service_google =
32{
33        "https://accounts.google.com/o/oauth2/auth",
34        "https://accounts.google.com/o/oauth2/token",
35        "urn:ietf:wg:oauth:2.0:oob",
36        "https://www.googleapis.com/auth/googletalk",
37        "783993391592.apps.googleusercontent.com",
38        "6C-Zgf7Tr7gEQTPlBhMUgo7R",
39};
40const struct oauth2_service oauth2_service_facebook =
41{
42        "https://www.facebook.com/dialog/oauth",
43        "https://graph.facebook.com/oauth/access_token",
[8eb2e84]44        "https://www.bitlbee.org/main.php/Facebook/oauth2.html",
[18c6d36]45        "offline_access,xmpp_login",
46        "126828914005625",
47        "4b100f0f244d620bf3f15f8b217d4c32",
48};
49
[5ebff60]50xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
[5997488]51{
[0da65d5]52        struct im_connection *ic = data;
53        struct jabber_data *jd = ic->proto_data;
[5997488]54        struct xt_node *c, *reply;
55        char *s;
[c27a923]56        int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_fb = 0;
[0e4c3dd]57        int want_oauth = FALSE, want_hipchat = FALSE;
[4be0e34]58        GString *mechs;
[5ebff60]59
60        if (!sasl_supported(ic)) {
[8d74291]61                /* Should abort this now, since we should already be doing
62                   IQ authentication. Strange things happen when you try
63                   to do both... */
[5ebff60]64                imcb_log(ic,
65                         "XMPP 1.0 non-compliant server seems to support SASL, please report this as a BitlBee bug!");
[8d74291]66                return XT_HANDLED;
67        }
[5ebff60]68
69        s = xt_find_attr(node, "xmlns");
70        if (!s || strcmp(s, XMLNS_SASL) != 0) {
71                imcb_log(ic, "Stream error while authenticating");
72                imc_logout(ic, FALSE);
[5997488]73                return XT_ABORT;
74        }
[5ebff60]75
76        want_oauth = set_getbool(&ic->acc->set, "oauth");
[71f87ba]77        want_hipchat = (jd->flags & JFLAG_HIPCHAT);
[5ebff60]78
79        mechs = g_string_new("");
[5997488]80        c = node->children;
[5ebff60]81        while ((c = xt_find_node(c, "mechanism"))) {
82                if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
[5997488]83                        sup_plain = 1;
[5ebff60]84                } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
[5997488]85                        sup_digest = 1;
[5ebff60]86                } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
[18c6d36]87                        sup_gtalk = 1;
[5ebff60]88                } else if (c->text && g_strcasecmp(c->text, "X-FACEBOOK-PLATFORM") == 0) {
[e1c926f]89                        sup_fb = 1;
[5ebff60]90                }
91
92                if (c->text) {
93                        g_string_append_printf(mechs, " %s", c->text);
94                }
95
[5997488]96                c = c->next;
97        }
[5ebff60]98
99        if (!want_oauth && !sup_plain && !sup_digest) {
100                if (!sup_gtalk && !sup_fb) {
101                        imcb_error(ic, "This server requires OAuth "
102                                   "(supported schemes:%s)", mechs->str);
103                } else {
104                        imcb_error(ic, "BitlBee does not support any of the offered SASL "
105                                   "authentication schemes:%s", mechs->str);
106                }
107                imc_logout(ic, FALSE);
108                g_string_free(mechs, TRUE);
[5997488]109                return XT_ABORT;
110        }
[5ebff60]111        g_string_free(mechs, TRUE);
112
113        reply = xt_new_node("auth", NULL, NULL);
[0e4c3dd]114        if (!want_hipchat) {
115                xt_add_attr(reply, "xmlns", XMLNS_SASL);
116        } else {
117                xt_add_attr(reply, "xmlns", XMLNS_HIPCHAT);
118        }
[5ebff60]119
120        if (sup_gtalk && want_oauth) {
[4a5d885]121                int len;
[5ebff60]122
[4a5d885]123                /* X-OAUTH2 is, not *the* standard OAuth2 SASL/XMPP implementation.
124                   It's currently used by GTalk and vaguely documented on
125                   http://code.google.com/apis/cloudprint/docs/rawxmpp.html . */
[5ebff60]126                xt_add_attr(reply, "mechanism", "X-OAUTH2");
127
128                len = strlen(jd->username) + strlen(jd->oauth2_access_token) + 2;
129                s = g_malloc(len + 1);
[4a5d885]130                s[0] = 0;
[5ebff60]131                strcpy(s + 1, jd->username);
132                strcpy(s + 2 + strlen(jd->username), jd->oauth2_access_token);
133                reply->text = base64_encode((unsigned char *) s, len);
134                reply->text_len = strlen(reply->text);
135                g_free(s);
136        } else if (sup_fb && want_oauth) {
137                xt_add_attr(reply, "mechanism", "X-FACEBOOK-PLATFORM");
[e1c926f]138                jd->flags |= JFLAG_SASL_FB;
[5ebff60]139        } else if (want_oauth) {
140                imcb_error(ic, "OAuth requested, but not supported by server");
141                imc_logout(ic, FALSE);
142                xt_free_node(reply);
[18c6d36]143                return XT_ABORT;
[5ebff60]144        } else if (sup_digest) {
145                xt_add_attr(reply, "mechanism", "DIGEST-MD5");
146
[fe7a554]147                /* The rest will be done later, when we receive a <challenge/>. */
[5ebff60]148        } else if (sup_plain) {
[5997488]149                int len;
[0e4c3dd]150                GString *gs;
151                char *username;
[5ebff60]152
[0e4c3dd]153                if (!want_hipchat) {
154                        xt_add_attr(reply, "mechanism", "PLAIN");
155                        username = jd->username;
156                } else {
157                        username = jd->me;
158                }
159
160                /* set an arbitrary initial size to avoid reallocations */
161                gs = g_string_sized_new(128);
[5ebff60]162
[5997488]163                /* With SASL PLAIN in XMPP, the text should be b64(\0user\0pass) */
[0e4c3dd]164                g_string_append_c(gs, '\0');
165                g_string_append(gs, username);
166                g_string_append_c(gs, '\0');
167                g_string_append(gs, ic->acc->pass);
168                if (want_hipchat) {
169                        /* Hipchat's variation adds \0resource at the end */
170                        g_string_append_c(gs, '\0');
171                        g_string_append(gs, set_getstr(&ic->acc->set, "resource"));
172                }
173
174                len = gs->len;
175                s = g_string_free(gs, FALSE);
176
[5ebff60]177                reply->text = base64_encode((unsigned char *) s, len);
178                reply->text_len = strlen(reply->text);
179                g_free(s);
[5997488]180        }
[5ebff60]181
182        if (reply && !jabber_write_packet(ic, reply)) {
183                xt_free_node(reply);
[5997488]184                return XT_ABORT;
185        }
[5ebff60]186        xt_free_node(reply);
187
[5997488]188        /* To prevent classic authentication from happening. */
189        jd->flags |= JFLAG_STREAM_STARTED;
[5ebff60]190
[5997488]191        return XT_HANDLED;
192}
193
[af97b23]194/* Non-static function, but not mentioned in jabber.h because it's for internal
195   use, just that the unittest should be able to reach it... */
[5ebff60]196char *sasl_get_part(char *data, char *field)
[d8e0484]197{
198        int i, len;
[5ebff60]199
200        len = strlen(field);
201
202        while (g_ascii_isspace(*data) || *data == ',') {
203                data++;
[d8e0484]204        }
[5ebff60]205
206        if (g_strncasecmp(data, field, len) == 0 && data[len] == '=') {
207                i = strlen(field) + 1;
208        } else {
209                for (i = 0; data[i]; i++) {
[d8e0484]210                        /* If we have a ", skip until it's closed again. */
[5ebff60]211                        if (data[i] == '"') {
212                                i++;
213                                while (data[i] != '"' || data[i - 1] == '\\') {
214                                        i++;
215                                }
[d8e0484]216                        }
[5ebff60]217
[af97b23]218                        /* If we got a comma, we got a new field. Check it,
219                           find the next key after it. */
[5ebff60]220                        if (data[i] == ',') {
221                                while (g_ascii_isspace(data[i]) || data[i] == ',') {
222                                        i++;
223                                }
224
225                                if (g_strncasecmp(data + i, field, len) == 0 &&
226                                    data[i + len] == '=') {
[af97b23]227                                        i += len + 1;
228                                        break;
229                                }
[d8e0484]230                        }
231                }
232        }
[5ebff60]233
234        if (data[i] == '"') {
[d8e0484]235                int j;
236                char *ret;
[5ebff60]237
238                i++;
[d8e0484]239                len = 0;
[5ebff60]240                while (data[i + len] != '"' || data[i + len - 1] == '\\') {
241                        len++;
242                }
243
244                ret = g_strndup(data + i, len);
245                for (i = j = 0; ret[i]; i++) {
246                        if (ret[i] == '\\') {
[d8e0484]247                                ret[j++] = ret[++i];
[5ebff60]248                        } else {
[d8e0484]249                                ret[j++] = ret[i];
250                        }
251                }
252                ret[j] = 0;
[5ebff60]253
[d8e0484]254                return ret;
[5ebff60]255        } else if (data[i]) {
[d8e0484]256                len = 0;
[5ebff60]257                while (data[i + len] && data[i + len] != ',') {
258                        len++;
259                }
260
261                return g_strndup(data + i, len);
262        } else {
[d8e0484]263                return NULL;
264        }
265}
266
[5ebff60]267xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
[5997488]268{
[0da65d5]269        struct im_connection *ic = data;
270        struct jabber_data *jd = ic->proto_data;
[f138bd2]271        struct xt_node *reply_pkt = NULL;
[3b6eadc]272        char *nonce = NULL, *realm = NULL, *cnonce = NULL;
273        unsigned char cnonce_bin[30];
[d8e0484]274        char *digest_uri = NULL;
275        char *dec = NULL;
[f138bd2]276        char *s = NULL, *reply = NULL;
[d8e0484]277        xt_status ret = XT_ABORT;
[5ebff60]278
279        if (node->text_len == 0) {
[d8e0484]280                goto error;
[5ebff60]281        }
282
283        dec = frombase64(node->text);
284
285        if (jd->flags & JFLAG_SASL_FB) {
[644b808]286                /* New-style Facebook OAauth2 support. Instead of sending a refresh
287                   token, they just send an access token that should never expire. */
[64b6635]288                GSList *p_in = NULL, *p_out = NULL;
289                char time[33];
[5ebff60]290
291                oauth_params_parse(&p_in, dec);
292                oauth_params_add(&p_out, "nonce", oauth_params_get(&p_in, "nonce"));
293                oauth_params_add(&p_out, "method", oauth_params_get(&p_in, "method"));
294                oauth_params_free(&p_in);
295
296                g_snprintf(time, sizeof(time), "%lld", (long long) (gettime() * 1000));
297                oauth_params_add(&p_out, "call_id", time);
298                oauth_params_add(&p_out, "api_key", oauth2_service_facebook.consumer_key);
299                oauth_params_add(&p_out, "v", "1.0");
300                oauth_params_add(&p_out, "format", "XML");
301                oauth_params_add(&p_out, "access_token", jd->oauth2_access_token);
302
303                reply = oauth_params_string(p_out);
304                oauth_params_free(&p_out);
305        } else if (!(s = sasl_get_part(dec, "rspauth"))) {
[d8e0484]306                /* See RFC 2831 for for information. */
307                md5_state_t A1, A2, H;
308                md5_byte_t A1r[16], A2r[16], Hr[16];
309                char A1h[33], A2h[33], Hh[33];
310                int i;
[5ebff60]311
312                nonce = sasl_get_part(dec, "nonce");
313                realm = sasl_get_part(dec, "realm");
314
315                if (!nonce) {
[d8e0484]316                        goto error;
[5ebff60]317                }
318
[d9282b4]319                /* Jabber.Org considers the realm part optional and doesn't
320                   specify one. Oh well, actually they're right, but still,
321                   don't know if this is right... */
[5ebff60]322                if (!realm) {
323                        realm = g_strdup(jd->server);
324                }
325
326                random_bytes(cnonce_bin, sizeof(cnonce_bin));
327                cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
328                digest_uri = g_strdup_printf("%s/%s", "xmpp", jd->server);
329
[d8e0484]330                /* Generate the MD5 hash of username:realm:password,
331                   I decided to call it H. */
[5ebff60]332                md5_init(&H);
333                s = g_strdup_printf("%s:%s:%s", jd->username, realm, ic->acc->pass);
334                md5_append(&H, (unsigned char *) s, strlen(s));
335                g_free(s);
336                md5_finish(&H, Hr);
337
[d8e0484]338                /* Now generate the hex. MD5 hash of H:nonce:cnonce, called A1. */
[5ebff60]339                md5_init(&A1);
340                s = g_strdup_printf(":%s:%s", nonce, cnonce);
341                md5_append(&A1, Hr, 16);
342                md5_append(&A1, (unsigned char *) s, strlen(s));
343                g_free(s);
344                md5_finish(&A1, A1r);
345                for (i = 0; i < 16; i++) {
346                        sprintf(A1h + i * 2, "%02x", A1r[i]);
347                }
348
[d8e0484]349                /* A2... */
[5ebff60]350                md5_init(&A2);
351                s = g_strdup_printf("%s:%s", "AUTHENTICATE", digest_uri);
352                md5_append(&A2, (unsigned char *) s, strlen(s));
353                g_free(s);
354                md5_finish(&A2, A2r);
355                for (i = 0; i < 16; i++) {
356                        sprintf(A2h + i * 2, "%02x", A2r[i]);
357                }
358
[d8e0484]359                /* Final result: A1:nonce:00000001:cnonce:auth:A2. Let's reuse H for it. */
[5ebff60]360                md5_init(&H);
361                s = g_strdup_printf("%s:%s:%s:%s:%s:%s", A1h, nonce, "00000001", cnonce, "auth", A2h);
362                md5_append(&H, (unsigned char *) s, strlen(s));
363                g_free(s);
364                md5_finish(&H, Hr);
365                for (i = 0; i < 16; i++) {
366                        sprintf(Hh + i * 2, "%02x", Hr[i]);
367                }
368
[d8e0484]369                /* Now build the SASL response string: */
[5ebff60]370                reply = g_strdup_printf("username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\","
371                                        "nc=%08x,qop=auth,digest-uri=\"%s\",response=%s,charset=%s",
372                                        jd->username, realm, nonce, cnonce, 1, digest_uri, Hh, "utf-8");
373        } else {
[d8e0484]374                /* We found rspauth, but don't really care... */
[5ebff60]375                g_free(s);
[d8e0484]376        }
[5ebff60]377
378        s = reply ? tobase64(reply) : NULL;
379        reply_pkt = xt_new_node("response", s, NULL);
380        xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL);
381
382        if (!jabber_write_packet(ic, reply_pkt)) {
[d8e0484]383                goto silent_error;
[5ebff60]384        }
385
[d8e0484]386        ret = XT_HANDLED;
387        goto silent_error;
388
389error:
[5ebff60]390        imcb_error(ic, "Incorrect SASL challenge received");
391        imc_logout(ic, FALSE);
[d8e0484]392
393silent_error:
[5ebff60]394        g_free(digest_uri);
395        g_free(cnonce);
396        g_free(nonce);
397        g_free(reply);
398        g_free(realm);
399        g_free(dec);
400        g_free(s);
401        xt_free_node(reply_pkt);
402
[d8e0484]403        return ret;
[5997488]404}
405
[5ebff60]406xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
[5997488]407{
[0da65d5]408        struct im_connection *ic = data;
409        struct jabber_data *jd = ic->proto_data;
[5997488]410        char *s;
[5ebff60]411
412        s = xt_find_attr(node, "xmlns");
413        if (!s || strcmp(s, XMLNS_SASL) != 0) {
414                imcb_log(ic, "Stream error while authenticating");
415                imc_logout(ic, FALSE);
[5997488]416                return XT_ABORT;
417        }
[5ebff60]418
419        if (strcmp(node->name, "success") == 0) {
420                imcb_log(ic, "Authentication finished");
[5997488]421                jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
[0e4c3dd]422
[71f87ba]423                if (jd->flags & JFLAG_HIPCHAT) {
[0e4c3dd]424                        return hipchat_handle_success(ic, node);
425                }
[5ebff60]426        } else if (strcmp(node->name, "failure") == 0) {
427                imcb_error(ic, "Authentication failure");
428                imc_logout(ic, FALSE);
[5997488]429                return XT_ABORT;
430        }
[5ebff60]431
[5997488]432        return XT_HANDLED;
433}
[8d74291]434
435/* This one is needed to judge if we'll do authentication using IQ or SASL.
436   It's done by checking if the <stream:stream> from the server has a
437   version attribute. I don't know if this is the right way though... */
[5ebff60]438gboolean sasl_supported(struct im_connection *ic)
[8d74291]439{
[0da65d5]440        struct jabber_data *jd = ic->proto_data;
[5ebff60]441
442        return (jd->xt && jd->xt->root && xt_find_attr(jd->xt->root, "version")) != 0;
[8d74291]443}
[4a5d885]444
[5ebff60]445void sasl_oauth2_init(struct im_connection *ic)
[4a5d885]446{
[18c6d36]447        struct jabber_data *jd = ic->proto_data;
[4a5d885]448        char *msg, *url;
[5ebff60]449
450        imcb_log(ic, "Starting OAuth authentication");
451
[4a5d885]452        /* Temporary contact, just used to receive the OAuth response. */
[5ebff60]453        imcb_add_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
454        url = oauth2_url(jd->oauth2_service);
455        msg = g_strdup_printf("Open this URL in your browser to authenticate: %s", url);
456        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, msg, 0, 0);
457        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, "Respond to this message with the returned "
458                       "authorization token.", 0, 0);
459
460        g_free(msg);
461        g_free(url);
[4a5d885]462}
463
[5ebff60]464static gboolean sasl_oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond)
[4a5d885]465{
466        struct im_connection *ic = data;
[5ebff60]467
468        if (g_slist_find(jabber_connections, ic)) {
469                imcb_remove_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
470        }
[4a5d885]471        return FALSE;
472}
473
[5ebff60]474static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token,
475                                  const char *error);
[4a5d885]476
[5ebff60]477int sasl_oauth2_get_refresh_token(struct im_connection *ic, const char *msg)
[4a5d885]478{
[18c6d36]479        struct jabber_data *jd = ic->proto_data;
[4a5d885]480        char *code;
481        int ret;
[5ebff60]482
483        imcb_log(ic, "Requesting OAuth access token");
484
[4a5d885]485        /* Don't do it here because the caller may get confused if the contact
486           we're currently sending a message to is deleted. */
[5ebff60]487        b_timeout_add(1, sasl_oauth2_remove_contact, ic);
488
489        code = g_strdup(msg);
490        g_strstrip(code);
491        ret = oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_CODE,
492                                  code, sasl_oauth2_got_token, ic);
493
494        g_free(code);
[4a5d885]495        return ret;
496}
497
[5ebff60]498int sasl_oauth2_refresh(struct im_connection *ic, const char *refresh_token)
[4a5d885]499{
[18c6d36]500        struct jabber_data *jd = ic->proto_data;
[5ebff60]501
502        return oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_REFRESH,
503                                   refresh_token, sasl_oauth2_got_token, ic);
[4a5d885]504}
505
[5ebff60]506static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error)
[4a5d885]507{
508        struct im_connection *ic = data;
509        struct jabber_data *jd;
[36533bf]510        GSList *auth = NULL;
[5ebff60]511
512        if (g_slist_find(jabber_connections, ic) == NULL) {
[4a5d885]513                return;
[5ebff60]514        }
515
[4a5d885]516        jd = ic->proto_data;
[5ebff60]517
518        if (access_token == NULL) {
519                imcb_error(ic, "OAuth failure (%s)", error);
520                imc_logout(ic, TRUE);
[e1c926f]521                return;
[4a5d885]522        }
[5ebff60]523
524        oauth_params_parse(&auth, ic->acc->pass);
525        if (refresh_token) {
526                oauth_params_set(&auth, "refresh_token", refresh_token);
527        }
528        if (access_token) {
529                oauth_params_set(&auth, "access_token", access_token);
530        }
531
532        g_free(ic->acc->pass);
533        ic->acc->pass = oauth_params_string(auth);
534        oauth_params_free(&auth);
535
536        g_free(jd->oauth2_access_token);
537        jd->oauth2_access_token = g_strdup(access_token);
538
539        jabber_connect(ic);
[4a5d885]540}
Note: See TracBrowser for help on using the repository browser.