source: protocols/jabber/sasl.c @ 8407e25

Last change on this file since 8407e25 was 0e788f5, checked in by Wilmer van der Gaast <wilmer@…>, at 2013-02-21T19:15:59Z

I'm still bored on a long flight. Wrote a script to automatically update
my copyright mentions since some were getting pretty stale. Left files not
touched since before 2012 alone so that this change doesn't touch almost
EVERY source file.

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