source: protocols/jabber/sasl.c @ 9ed8175

Last change on this file since 9ed8175 was 40cfbc5, checked in by dequis <dx@…>, at 2015-04-28T13:47:48Z

hipchat: Basic implementation: Auth, profile and mention names

This is enough to log in with their usernames, make 'chat add' based
groupchat joins slightly more smooth, and see mention names as nicks.

All the MUC list stuff is left out intentionally since that's not as
stable as I wish.

  • Property mode set to 100644
File size: 15.4 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Jabber module - SASL authentication                                      *
5*                                                                           *
6*  Copyright 2006-2012 Wilmer van der Gaast <wilmer@gaast.net>              *
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
24#include <ctype.h>
25
26#include "jabber.h"
27#include "base64.h"
28#include "oauth2.h"
29#include "oauth.h"
30
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",
44        "https://www.bitlbee.org/main.php/Facebook/oauth2.html",
45        "offline_access,xmpp_login",
46        "126828914005625",
47        "4b100f0f244d620bf3f15f8b217d4c32",
48};
49
50xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
51{
52        struct im_connection *ic = data;
53        struct jabber_data *jd = ic->proto_data;
54        struct xt_node *c, *reply;
55        char *s;
56        int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_fb = 0;
57        int want_oauth = FALSE, want_hipchat = FALSE;
58        GString *mechs;
59
60        if (!sasl_supported(ic)) {
61                /* Should abort this now, since we should already be doing
62                   IQ authentication. Strange things happen when you try
63                   to do both... */
64                imcb_log(ic,
65                         "XMPP 1.0 non-compliant server seems to support SASL, please report this as a BitlBee bug!");
66                return XT_HANDLED;
67        }
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);
73                return XT_ABORT;
74        }
75
76        want_oauth = set_getbool(&ic->acc->set, "oauth");
77        want_hipchat = (jd->flags & JFLAG_HIPCHAT);
78
79        mechs = g_string_new("");
80        c = node->children;
81        while ((c = xt_find_node(c, "mechanism"))) {
82                if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
83                        sup_plain = 1;
84                } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
85                        sup_digest = 1;
86                } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
87                        sup_gtalk = 1;
88                } else if (c->text && g_strcasecmp(c->text, "X-FACEBOOK-PLATFORM") == 0) {
89                        sup_fb = 1;
90                }
91
92                if (c->text) {
93                        g_string_append_printf(mechs, " %s", c->text);
94                }
95
96                c = c->next;
97        }
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);
109                return XT_ABORT;
110        }
111        g_string_free(mechs, TRUE);
112
113        reply = xt_new_node("auth", NULL, NULL);
114        if (!want_hipchat) {
115                xt_add_attr(reply, "xmlns", XMLNS_SASL);
116        } else {
117                xt_add_attr(reply, "xmlns", XMLNS_HIPCHAT);
118        }
119
120        if (sup_gtalk && want_oauth) {
121                int len;
122
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 . */
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);
130                s[0] = 0;
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");
138                jd->flags |= JFLAG_SASL_FB;
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);
143                return XT_ABORT;
144        } else if (sup_digest) {
145                xt_add_attr(reply, "mechanism", "DIGEST-MD5");
146
147                /* The rest will be done later, when we receive a <challenge/>. */
148        } else if (sup_plain) {
149                int len;
150                GString *gs;
151                char *username;
152
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);
162
163                /* With SASL PLAIN in XMPP, the text should be b64(\0user\0pass) */
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
177                reply->text = base64_encode((unsigned char *) s, len);
178                reply->text_len = strlen(reply->text);
179                g_free(s);
180        }
181
182        if (reply && !jabber_write_packet(ic, reply)) {
183                xt_free_node(reply);
184                return XT_ABORT;
185        }
186        xt_free_node(reply);
187
188        /* To prevent classic authentication from happening. */
189        jd->flags |= JFLAG_STREAM_STARTED;
190
191        return XT_HANDLED;
192}
193
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... */
196char *sasl_get_part(char *data, char *field)
197{
198        int i, len;
199
200        len = strlen(field);
201
202        while (g_ascii_isspace(*data) || *data == ',') {
203                data++;
204        }
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++) {
210                        /* If we have a ", skip until it's closed again. */
211                        if (data[i] == '"') {
212                                i++;
213                                while (data[i] != '"' || data[i - 1] == '\\') {
214                                        i++;
215                                }
216                        }
217
218                        /* If we got a comma, we got a new field. Check it,
219                           find the next key after it. */
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] == '=') {
227                                        i += len + 1;
228                                        break;
229                                }
230                        }
231                }
232        }
233
234        if (data[i] == '"') {
235                int j;
236                char *ret;
237
238                i++;
239                len = 0;
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] == '\\') {
247                                ret[j++] = ret[++i];
248                        } else {
249                                ret[j++] = ret[i];
250                        }
251                }
252                ret[j] = 0;
253
254                return ret;
255        } else if (data[i]) {
256                len = 0;
257                while (data[i + len] && data[i + len] != ',') {
258                        len++;
259                }
260
261                return g_strndup(data + i, len);
262        } else {
263                return NULL;
264        }
265}
266
267xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
268{
269        struct im_connection *ic = data;
270        struct jabber_data *jd = ic->proto_data;
271        struct xt_node *reply_pkt = NULL;
272        char *nonce = NULL, *realm = NULL, *cnonce = NULL;
273        unsigned char cnonce_bin[30];
274        char *digest_uri = NULL;
275        char *dec = NULL;
276        char *s = NULL, *reply = NULL;
277        xt_status ret = XT_ABORT;
278
279        if (node->text_len == 0) {
280                goto error;
281        }
282
283        dec = frombase64(node->text);
284
285        if (jd->flags & JFLAG_SASL_FB) {
286                /* New-style Facebook OAauth2 support. Instead of sending a refresh
287                   token, they just send an access token that should never expire. */
288                GSList *p_in = NULL, *p_out = NULL;
289                char time[33];
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"))) {
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;
311
312                nonce = sasl_get_part(dec, "nonce");
313                realm = sasl_get_part(dec, "realm");
314
315                if (!nonce) {
316                        goto error;
317                }
318
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... */
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
330                /* Generate the MD5 hash of username:realm:password,
331                   I decided to call it H. */
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
338                /* Now generate the hex. MD5 hash of H:nonce:cnonce, called A1. */
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
349                /* A2... */
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
359                /* Final result: A1:nonce:00000001:cnonce:auth:A2. Let's reuse H for it. */
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
369                /* Now build the SASL response string: */
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 {
374                /* We found rspauth, but don't really care... */
375                g_free(s);
376        }
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)) {
383                goto silent_error;
384        }
385
386        ret = XT_HANDLED;
387        goto silent_error;
388
389error:
390        imcb_error(ic, "Incorrect SASL challenge received");
391        imc_logout(ic, FALSE);
392
393silent_error:
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
403        return ret;
404}
405
406xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
407{
408        struct im_connection *ic = data;
409        struct jabber_data *jd = ic->proto_data;
410        char *s;
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);
416                return XT_ABORT;
417        }
418
419        if (strcmp(node->name, "success") == 0) {
420                imcb_log(ic, "Authentication finished");
421                jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
422
423                if (jd->flags & JFLAG_HIPCHAT) {
424                        return hipchat_handle_success(ic, node);
425                }
426        } else if (strcmp(node->name, "failure") == 0) {
427                imcb_error(ic, "Authentication failure");
428                imc_logout(ic, FALSE);
429                return XT_ABORT;
430        }
431
432        return XT_HANDLED;
433}
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... */
438gboolean sasl_supported(struct im_connection *ic)
439{
440        struct jabber_data *jd = ic->proto_data;
441
442        return (jd->xt && jd->xt->root && xt_find_attr(jd->xt->root, "version")) != 0;
443}
444
445void sasl_oauth2_init(struct im_connection *ic)
446{
447        struct jabber_data *jd = ic->proto_data;
448        char *msg, *url;
449
450        imcb_log(ic, "Starting OAuth authentication");
451
452        /* Temporary contact, just used to receive the OAuth response. */
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);
462}
463
464static gboolean sasl_oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond)
465{
466        struct im_connection *ic = data;
467
468        if (g_slist_find(jabber_connections, ic)) {
469                imcb_remove_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
470        }
471        return FALSE;
472}
473
474static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token,
475                                  const char *error);
476
477int sasl_oauth2_get_refresh_token(struct im_connection *ic, const char *msg)
478{
479        struct jabber_data *jd = ic->proto_data;
480        char *code;
481        int ret;
482
483        imcb_log(ic, "Requesting OAuth access token");
484
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. */
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);
495        return ret;
496}
497
498int sasl_oauth2_refresh(struct im_connection *ic, const char *refresh_token)
499{
500        struct jabber_data *jd = ic->proto_data;
501
502        return oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_REFRESH,
503                                   refresh_token, sasl_oauth2_got_token, ic);
504}
505
506static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error)
507{
508        struct im_connection *ic = data;
509        struct jabber_data *jd;
510        GSList *auth = NULL;
511
512        if (g_slist_find(jabber_connections, ic) == NULL) {
513                return;
514        }
515
516        jd = ic->proto_data;
517
518        if (access_token == NULL) {
519                imcb_error(ic, "OAuth failure (%s)", error);
520                imc_logout(ic, TRUE);
521                return;
522        }
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);
540}
Note: See TracBrowser for help on using the repository browser.