source: protocols/jabber/sasl.c @ 235b51a

Last change on this file since 235b51a was 235b51a, checked in by dx <dx@…>, at 2016-06-11T20:40:33Z

Use correct error when no schemes are supported (#78)

This will make bitlbee tell the user about the requirement for oauth
when the server actually announces support for oauth. If the server does
not announce oauth support bitlbee will tell the user it doesn't support
any of the schemes provided by the server.

These messages were reversed before.

  • Property mode set to 100644
File size: 14.8 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};
40
41xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
42{
43        struct im_connection *ic = data;
44        struct jabber_data *jd = ic->proto_data;
45        struct xt_node *c, *reply;
46        char *s;
47        int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0;
48        int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE;
49        GString *mechs;
50
51        if (!sasl_supported(ic)) {
52                /* Should abort this now, since we should already be doing
53                   IQ authentication. Strange things happen when you try
54                   to do both... */
55                imcb_log(ic,
56                         "XMPP 1.0 non-compliant server seems to support SASL, please report this as a BitlBee bug!");
57                return XT_HANDLED;
58        }
59
60        s = xt_find_attr(node, "xmlns");
61        if (!s || strcmp(s, XMLNS_SASL) != 0) {
62                imcb_log(ic, "Stream error while authenticating");
63                imc_logout(ic, FALSE);
64                return XT_ABORT;
65        }
66
67        want_anonymous = set_getbool(&ic->acc->set, "anonymous");
68        want_oauth = set_getbool(&ic->acc->set, "oauth");
69        want_hipchat = (jd->flags & JFLAG_HIPCHAT);
70
71        mechs = g_string_new("");
72        c = node->children;
73        while ((c = xt_find_node(c, "mechanism"))) {
74                if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
75                        sup_plain = 1;
76                } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
77                        sup_digest = 1;
78                } else if (c->text && g_strcasecmp(c->text, "ANONYMOUS") == 0) {
79                        sup_anonymous = 1;
80                } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
81                        sup_gtalk = 1;
82                }
83
84                if (c->text) {
85                        g_string_append_printf(mechs, " %s", c->text);
86                }
87
88                c = c->next;
89        }
90
91        if (!want_oauth && !sup_plain && !sup_digest) {
92                if (sup_gtalk) {
93                        imcb_error(ic, "This server requires OAuth "
94                                   "(supported schemes:%s)", mechs->str);
95                } else {
96                        imcb_error(ic, "BitlBee does not support any of the offered SASL "
97                                   "authentication schemes:%s", mechs->str);
98                }
99                imc_logout(ic, FALSE);
100                g_string_free(mechs, TRUE);
101                return XT_ABORT;
102        }
103        g_string_free(mechs, TRUE);
104
105        reply = xt_new_node("auth", NULL, NULL);
106        if (!want_hipchat) {
107                xt_add_attr(reply, "xmlns", XMLNS_SASL);
108        } else {
109                xt_add_attr(reply, "xmlns", XMLNS_HIPCHAT);
110        }
111
112        if (sup_gtalk && want_oauth) {
113                int len;
114
115                /* X-OAUTH2 is, not *the* standard OAuth2 SASL/XMPP implementation.
116                   It's currently used by GTalk and vaguely documented on
117                   http://code.google.com/apis/cloudprint/docs/rawxmpp.html . */
118                xt_add_attr(reply, "mechanism", "X-OAUTH2");
119
120                len = strlen(jd->username) + strlen(jd->oauth2_access_token) + 2;
121                s = g_malloc(len + 1);
122                s[0] = 0;
123                strcpy(s + 1, jd->username);
124                strcpy(s + 2 + strlen(jd->username), jd->oauth2_access_token);
125                reply->text = base64_encode((unsigned char *) s, len);
126                reply->text_len = strlen(reply->text);
127                g_free(s);
128        } else if (want_oauth) {
129                imcb_error(ic, "OAuth requested, but not supported by server");
130                imc_logout(ic, FALSE);
131                xt_free_node(reply);
132                return XT_ABORT;
133        } else if (want_anonymous && sup_anonymous) {
134                xt_add_attr(reply, "mechanism", "ANONYMOUS");
135
136                /* Well, that was easy. */
137        } else if (want_anonymous) {
138                imcb_error(ic, "Anonymous login requested, but not supported by server");
139                imc_logout(ic, FALSE);
140                xt_free_node(reply);
141                return XT_ABORT;
142        } else if (sup_digest && !(jd->ssl && sup_plain)) {
143                /* Only try DIGEST-MD5 if there's no SSL/TLS or if PLAIN isn't supported.
144                 * Which in practice means "don't bother with DIGEST-MD5 most of the time".
145                 * It's weak, pointless over TLS, and often breaks with some servers (hi openfire) */
146
147                xt_add_attr(reply, "mechanism", "DIGEST-MD5");
148
149                /* The rest will be done later, when we receive a <challenge/>. */
150        } else if (sup_plain) {
151                int len;
152                GString *gs;
153                char *username;
154
155                if (!want_hipchat) {
156                        xt_add_attr(reply, "mechanism", "PLAIN");
157                        username = jd->username;
158                } else {
159                        username = jd->me;
160                }
161
162                /* set an arbitrary initial size to avoid reallocations */
163                gs = g_string_sized_new(128);
164
165                /* With SASL PLAIN in XMPP, the text should be b64(\0user\0pass) */
166                g_string_append_c(gs, '\0');
167                g_string_append(gs, username);
168                g_string_append_c(gs, '\0');
169                g_string_append(gs, ic->acc->pass);
170                if (want_hipchat) {
171                        /* Hipchat's variation adds \0resource at the end */
172                        g_string_append_c(gs, '\0');
173                        g_string_append(gs, set_getstr(&ic->acc->set, "resource"));
174                }
175
176                len = gs->len;
177                s = g_string_free(gs, FALSE);
178
179                reply->text = base64_encode((unsigned char *) s, len);
180                reply->text_len = strlen(reply->text);
181                g_free(s);
182        }
183
184        if (reply && !jabber_write_packet(ic, reply)) {
185                xt_free_node(reply);
186                return XT_ABORT;
187        }
188        xt_free_node(reply);
189
190        /* To prevent classic authentication from happening. */
191        jd->flags |= JFLAG_STREAM_STARTED;
192
193        return XT_HANDLED;
194}
195
196/* Non-static function, but not mentioned in jabber.h because it's for internal
197   use, just that the unittest should be able to reach it... */
198char *sasl_get_part(char *data, char *field)
199{
200        int i, len;
201
202        len = strlen(field);
203
204        while (g_ascii_isspace(*data) || *data == ',') {
205                data++;
206        }
207
208        if (g_strncasecmp(data, field, len) == 0 && data[len] == '=') {
209                i = strlen(field) + 1;
210        } else {
211                for (i = 0; data[i]; i++) {
212                        /* If we have a ", skip until it's closed again. */
213                        if (data[i] == '"') {
214                                i++;
215                                while (data[i] != '"' || data[i - 1] == '\\') {
216                                        i++;
217                                }
218                        }
219
220                        /* If we got a comma, we got a new field. Check it,
221                           find the next key after it. */
222                        if (data[i] == ',') {
223                                while (g_ascii_isspace(data[i]) || data[i] == ',') {
224                                        i++;
225                                }
226
227                                if (g_strncasecmp(data + i, field, len) == 0 &&
228                                    data[i + len] == '=') {
229                                        i += len + 1;
230                                        break;
231                                }
232                        }
233                }
234        }
235
236        if (data[i] == '"') {
237                int j;
238                char *ret;
239
240                i++;
241                len = 0;
242                while (data[i + len] != '"' || data[i + len - 1] == '\\') {
243                        len++;
244                }
245
246                ret = g_strndup(data + i, len);
247                for (i = j = 0; ret[i]; i++) {
248                        if (ret[i] == '\\') {
249                                ret[j++] = ret[++i];
250                        } else {
251                                ret[j++] = ret[i];
252                        }
253                }
254                ret[j] = 0;
255
256                return ret;
257        } else if (data[i]) {
258                len = 0;
259                while (data[i + len] && data[i + len] != ',') {
260                        len++;
261                }
262
263                return g_strndup(data + i, len);
264        } else {
265                return NULL;
266        }
267}
268
269xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
270{
271        struct im_connection *ic = data;
272        struct jabber_data *jd = ic->proto_data;
273        struct xt_node *reply_pkt = NULL;
274        char *nonce = NULL, *realm = NULL, *cnonce = NULL;
275        unsigned char cnonce_bin[30];
276        char *digest_uri = NULL;
277        char *dec = NULL;
278        char *s = NULL, *reply = NULL;
279        xt_status ret = XT_ABORT;
280
281        if (node->text_len == 0) {
282                goto error;
283        }
284
285        dec = frombase64(node->text);
286
287        if (!(s = sasl_get_part(dec, "rspauth"))) {
288                /* See RFC 2831 for for information. */
289                md5_state_t A1, A2, H;
290                md5_byte_t A1r[16], A2r[16], Hr[16];
291                char A1h[33], A2h[33], Hh[33];
292                int i;
293
294                nonce = sasl_get_part(dec, "nonce");
295                realm = sasl_get_part(dec, "realm");
296
297                if (!nonce) {
298                        goto error;
299                }
300
301                /* Jabber.Org considers the realm part optional and doesn't
302                   specify one. Oh well, actually they're right, but still,
303                   don't know if this is right... */
304                if (!realm) {
305                        realm = g_strdup(jd->server);
306                }
307
308                random_bytes(cnonce_bin, sizeof(cnonce_bin));
309                cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
310                digest_uri = g_strdup_printf("%s/%s", "xmpp", jd->server);
311
312                /* Generate the MD5 hash of username:realm:password,
313                   I decided to call it H. */
314                md5_init(&H);
315                s = g_strdup_printf("%s:%s:%s", jd->username, realm, ic->acc->pass);
316                md5_append(&H, (unsigned char *) s, strlen(s));
317                g_free(s);
318                md5_finish(&H, Hr);
319
320                /* Now generate the hex. MD5 hash of H:nonce:cnonce, called A1. */
321                md5_init(&A1);
322                s = g_strdup_printf(":%s:%s", nonce, cnonce);
323                md5_append(&A1, Hr, 16);
324                md5_append(&A1, (unsigned char *) s, strlen(s));
325                g_free(s);
326                md5_finish(&A1, A1r);
327                for (i = 0; i < 16; i++) {
328                        sprintf(A1h + i * 2, "%02x", A1r[i]);
329                }
330
331                /* A2... */
332                md5_init(&A2);
333                s = g_strdup_printf("%s:%s", "AUTHENTICATE", digest_uri);
334                md5_append(&A2, (unsigned char *) s, strlen(s));
335                g_free(s);
336                md5_finish(&A2, A2r);
337                for (i = 0; i < 16; i++) {
338                        sprintf(A2h + i * 2, "%02x", A2r[i]);
339                }
340
341                /* Final result: A1:nonce:00000001:cnonce:auth:A2. Let's reuse H for it. */
342                md5_init(&H);
343                s = g_strdup_printf("%s:%s:%s:%s:%s:%s", A1h, nonce, "00000001", cnonce, "auth", A2h);
344                md5_append(&H, (unsigned char *) s, strlen(s));
345                g_free(s);
346                md5_finish(&H, Hr);
347                for (i = 0; i < 16; i++) {
348                        sprintf(Hh + i * 2, "%02x", Hr[i]);
349                }
350
351                /* Now build the SASL response string: */
352                reply = g_strdup_printf("username=\"%s\",realm=\"%s\",nonce=\"%s\",cnonce=\"%s\","
353                                        "nc=%08x,qop=auth,digest-uri=\"%s\",response=%s,charset=%s",
354                                        jd->username, realm, nonce, cnonce, 1, digest_uri, Hh, "utf-8");
355        } else {
356                /* We found rspauth, but don't really care... */
357                g_free(s);
358        }
359
360        s = reply ? tobase64(reply) : NULL;
361        reply_pkt = xt_new_node("response", s, NULL);
362        xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL);
363
364        if (!jabber_write_packet(ic, reply_pkt)) {
365                goto silent_error;
366        }
367
368        ret = XT_HANDLED;
369        goto silent_error;
370
371error:
372        imcb_error(ic, "Incorrect SASL challenge received");
373        imc_logout(ic, FALSE);
374
375silent_error:
376        g_free(digest_uri);
377        g_free(cnonce);
378        g_free(nonce);
379        g_free(reply);
380        g_free(realm);
381        g_free(dec);
382        g_free(s);
383        xt_free_node(reply_pkt);
384
385        return ret;
386}
387
388xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
389{
390        struct im_connection *ic = data;
391        struct jabber_data *jd = ic->proto_data;
392        char *s;
393
394        s = xt_find_attr(node, "xmlns");
395        if (!s || strcmp(s, XMLNS_SASL) != 0) {
396                imcb_log(ic, "Stream error while authenticating");
397                imc_logout(ic, FALSE);
398                return XT_ABORT;
399        }
400
401        if (strcmp(node->name, "success") == 0) {
402                imcb_log(ic, "Authentication finished");
403                jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
404
405                if (jd->flags & JFLAG_HIPCHAT) {
406                        return hipchat_handle_success(ic, node);
407                }
408        } else if (strcmp(node->name, "failure") == 0) {
409                imcb_error(ic, "Authentication failure");
410                imc_logout(ic, FALSE);
411                return XT_ABORT;
412        }
413
414        return XT_HANDLED;
415}
416
417/* This one is needed to judge if we'll do authentication using IQ or SASL.
418   It's done by checking if the <stream:stream> from the server has a
419   version attribute. I don't know if this is the right way though... */
420gboolean sasl_supported(struct im_connection *ic)
421{
422        struct jabber_data *jd = ic->proto_data;
423
424        return (jd->xt && jd->xt->root && xt_find_attr(jd->xt->root, "version")) != 0;
425}
426
427void sasl_oauth2_init(struct im_connection *ic)
428{
429        struct jabber_data *jd = ic->proto_data;
430        char *msg, *url;
431
432        imcb_log(ic, "Starting OAuth authentication");
433
434        /* Temporary contact, just used to receive the OAuth response. */
435        imcb_add_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
436        url = oauth2_url(jd->oauth2_service);
437        msg = g_strdup_printf("Open this URL in your browser to authenticate: %s", url);
438        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, msg, 0, 0);
439        imcb_buddy_msg(ic, JABBER_OAUTH_HANDLE, "Respond to this message with the returned "
440                       "authorization token.", 0, 0);
441
442        g_free(msg);
443        g_free(url);
444}
445
446static gboolean sasl_oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond)
447{
448        struct im_connection *ic = data;
449
450        if (g_slist_find(jabber_connections, ic)) {
451                imcb_remove_buddy(ic, JABBER_OAUTH_HANDLE, NULL);
452        }
453        return FALSE;
454}
455
456static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token,
457                                  const char *error);
458
459int sasl_oauth2_get_refresh_token(struct im_connection *ic, const char *msg)
460{
461        struct jabber_data *jd = ic->proto_data;
462        char *code;
463        int ret;
464
465        imcb_log(ic, "Requesting OAuth access token");
466
467        /* Don't do it here because the caller may get confused if the contact
468           we're currently sending a message to is deleted. */
469        b_timeout_add(1, sasl_oauth2_remove_contact, ic);
470
471        code = g_strdup(msg);
472        g_strstrip(code);
473        ret = oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_CODE,
474                                  code, sasl_oauth2_got_token, ic);
475
476        g_free(code);
477        return ret;
478}
479
480int sasl_oauth2_refresh(struct im_connection *ic, const char *refresh_token)
481{
482        struct jabber_data *jd = ic->proto_data;
483
484        return oauth2_access_token(jd->oauth2_service, OAUTH2_AUTH_REFRESH,
485                                   refresh_token, sasl_oauth2_got_token, ic);
486}
487
488static void sasl_oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error)
489{
490        struct im_connection *ic = data;
491        struct jabber_data *jd;
492        GSList *auth = NULL;
493
494        if (g_slist_find(jabber_connections, ic) == NULL) {
495                return;
496        }
497
498        jd = ic->proto_data;
499
500        if (access_token == NULL) {
501                imcb_error(ic, "OAuth failure (%s)", error);
502                imc_logout(ic, TRUE);
503                return;
504        }
505
506        oauth_params_parse(&auth, ic->acc->pass);
507        if (refresh_token) {
508                oauth_params_set(&auth, "refresh_token", refresh_token);
509        }
510        if (access_token) {
511                oauth_params_set(&auth, "access_token", access_token);
512        }
513
514        g_free(ic->acc->pass);
515        ic->acc->pass = oauth_params_string(auth);
516        oauth_params_free(&auth);
517
518        g_free(jd->oauth2_access_token);
519        jd->oauth2_access_token = g_strdup(access_token);
520
521        jabber_connect(ic);
522}
Note: See TracBrowser for help on using the repository browser.