source: protocols/jabber/sasl.c @ b38d399

Last change on this file since b38d399 was c27a923, checked in by dequis <dx@…>, at 2014-10-11T02:20:52Z

Remove MSNXMPP specific code

The MSN XMPP gateway was shutdown december 2013 and isn't coming back.

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