No issues found
1 /*
2 * camel-sasl-xoauth.c
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with the program; if not, see <http://www.gnu.org/licenses/>
16 *
17 */
18
19 /* XXX Yeah, yeah... */
20 #define GOA_API_IS_SUBJECT_TO_CHANGE
21
22 #include <config.h>
23 #include <glib/gi18n-lib.h>
24
25 #include <goa/goa.h>
26
27 #include <libemail-engine/e-mail-session.h>
28
29 #include "camel-sasl-xoauth.h"
30
31 #define CAMEL_SASL_XOAUTH_GET_PRIVATE(obj) \
32 (G_TYPE_INSTANCE_GET_PRIVATE \
33 ((obj), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuthPrivate))
34
35 /* This is the property name or URL parameter under which we
36 * embed the GoaAccount ID into an EAccount or ESource object. */
37 #define GOA_KEY "goa-account-id"
38
39 struct _CamelSaslXOAuthPrivate {
40 gint placeholder;
41 };
42
43 G_DEFINE_DYNAMIC_TYPE (CamelSaslXOAuth, camel_sasl_xoauth, CAMEL_TYPE_SASL)
44
45 /*****************************************************************************
46 * This is based on an old revision of gnome-online-accounts
47 * which demonstrated OAuth authentication with an IMAP server.
48 *
49 * See commit 5bcbe2a3eac4821892680e0655b27ab8c128ab15
50 *****************************************************************************/
51
52 #include <libsoup/soup.h>
53
54 #define OAUTH_ENCODE_STRING(str) \
55 (str ? soup_uri_encode ((str), "!$&'()*+,;=@") : g_strdup (""))
56
57 #define SHA1_BLOCK_SIZE 64
58 #define SHA1_LENGTH 20
59
60 /*
61 * hmac_sha1:
62 * @key: The key
63 * @message: The message
64 *
65 * Given the key and message, compute the HMAC-SHA1 hash and return the base-64
66 * encoding of it. This is very geared towards OAuth, and as such both key and
67 * message must be NULL-terminated strings, and the result is base-64 encoded.
68 */
69 static gchar *
70 hmac_sha1 (const gchar *key,
71 const gchar *message)
72 {
73 GChecksum *checksum;
74 gchar *real_key;
75 guchar ipad[SHA1_BLOCK_SIZE];
76 guchar opad[SHA1_BLOCK_SIZE];
77 guchar inner[SHA1_LENGTH];
78 guchar digest[SHA1_LENGTH];
79 gsize key_length, inner_length, digest_length;
80 gint i;
81
82 g_return_val_if_fail (key, NULL);
83 g_return_val_if_fail (message, NULL);
84
85 checksum = g_checksum_new (G_CHECKSUM_SHA1);
86
87 /* If the key is longer than the block size, hash it first */
88 if (strlen (key) > SHA1_BLOCK_SIZE) {
89 guchar new_key[SHA1_LENGTH];
90
91 key_length = sizeof (new_key);
92
93 g_checksum_update (checksum, (guchar *) key, strlen (key));
94 g_checksum_get_digest (checksum, new_key, &key_length);
95 g_checksum_reset (checksum);
96
97 real_key = g_memdup (new_key, key_length);
98 } else {
99 real_key = g_strdup (key);
100 key_length = strlen (key);
101 }
102
103 /* Sanity check the length */
104 g_assert (key_length <= SHA1_BLOCK_SIZE);
105
106 /* Protect against use of the provided key by NULLing it */
107 key = NULL;
108
109 /* Stage 1 */
110 memset (ipad, 0, sizeof (ipad));
111 memset (opad, 0, sizeof (opad));
112
113 memcpy (ipad, real_key, key_length);
114 memcpy (opad, real_key, key_length);
115
116 /* Stage 2 and 5 */
117 for (i = 0; i < sizeof (ipad); i++) {
118 ipad[i] ^= 0x36;
119 opad[i] ^= 0x5C;
120 }
121
122 /* Stage 3 and 4 */
123 g_checksum_update (checksum, ipad, sizeof (ipad));
124 g_checksum_update (checksum, (guchar *) message, strlen (message));
125 inner_length = sizeof (inner);
126 g_checksum_get_digest (checksum, inner, &inner_length);
127 g_checksum_reset (checksum);
128
129 /* Stage 6 and 7 */
130 g_checksum_update (checksum, opad, sizeof (opad));
131 g_checksum_update (checksum, inner, inner_length);
132
133 digest_length = sizeof (digest);
134 g_checksum_get_digest (checksum, digest, &digest_length);
135
136 g_checksum_free (checksum);
137 g_free (real_key);
138
139 return g_base64_encode (digest, digest_length);
140 }
141
142 static gchar *
143 sign_plaintext (const gchar *consumer_secret,
144 const gchar *token_secret)
145 {
146 gchar *cs;
147 gchar *ts;
148 gchar *rv;
149
150 cs = OAUTH_ENCODE_STRING (consumer_secret);
151 ts = OAUTH_ENCODE_STRING (token_secret);
152 rv = g_strconcat (cs, "&", ts, NULL);
153
154 g_free (cs);
155 g_free (ts);
156
157 return rv;
158 }
159
160 static gchar *
161 sign_hmac (const gchar *consumer_secret,
162 const gchar *token_secret,
163 const gchar *http_method,
164 const gchar *request_uri,
165 const gchar *encoded_params)
166 {
167 GString *text;
168 gchar *signature;
169 gchar *key;
170
171 text = g_string_new (NULL);
172 g_string_append (text, http_method);
173 g_string_append_c (text, '&');
174 g_string_append_uri_escaped (text, request_uri, NULL, FALSE);
175 g_string_append_c (text, '&');
176 g_string_append_uri_escaped (text, encoded_params, NULL, FALSE);
177
178 /* PLAINTEXT signature value is the HMAC-SHA1 key value */
179 key = sign_plaintext (consumer_secret, token_secret);
180 signature = hmac_sha1 (key, text->str);
181 g_free (key);
182
183 g_string_free (text, TRUE);
184
185 return signature;
186 }
187
188 static GHashTable *
189 calculate_xoauth_params (const gchar *request_uri,
190 const gchar *consumer_key,
191 const gchar *consumer_secret,
192 const gchar *access_token,
193 const gchar *access_token_secret)
194 {
195 gchar *signature;
196 GHashTable *params;
197 gchar *nonce;
198 gchar *timestamp;
199 GList *keys;
200 GList *iter;
201 GString *normalized;
202 gpointer key;
203
204 nonce = g_strdup_printf ("%u", g_random_int ());
205 timestamp = g_strdup_printf (
206 "%" G_GINT64_FORMAT, (gint64) time (NULL));
207
208 params = g_hash_table_new_full (
209 (GHashFunc) g_str_hash,
210 (GEqualFunc) g_str_equal,
211 (GDestroyNotify) NULL,
212 (GDestroyNotify) g_free);
213
214 key = (gpointer) "oauth_consumer_key";
215 g_hash_table_insert (params, key, g_strdup (consumer_key));
216
217 key = (gpointer) "oauth_nonce";
218 g_hash_table_insert (params, key, nonce); /* takes ownership */
219
220 key = (gpointer) "oauth_timestamp";
221 g_hash_table_insert (params, key, timestamp); /* takes ownership */
222
223 key = (gpointer) "oauth_version";
224 g_hash_table_insert (params, key, g_strdup ("1.0"));
225
226 key = (gpointer) "oauth_signature_method";
227 g_hash_table_insert (params, key, g_strdup ("HMAC-SHA1"));
228
229 key = (gpointer) "oauth_token";
230 g_hash_table_insert (params, key, g_strdup (access_token));
231
232 normalized = g_string_new (NULL);
233 keys = g_hash_table_get_keys (params);
234 keys = g_list_sort (keys, (GCompareFunc) g_strcmp0);
235 for (iter = keys; iter != NULL; iter = iter->next) {
236 const gchar *key = iter->data;
237 const gchar *value;
238 gchar *k;
239 gchar *v;
240
241 value = g_hash_table_lookup (params, key);
242 if (normalized->len > 0)
243 g_string_append_c (normalized, '&');
244
245 k = OAUTH_ENCODE_STRING (key);
246 v = OAUTH_ENCODE_STRING (value);
247
248 g_string_append_printf (normalized, "%s=%s", k, v);
249
250 g_free (k);
251 g_free (v);
252 }
253 g_list_free (keys);
254
255 signature = sign_hmac (
256 consumer_secret, access_token_secret,
257 "GET", request_uri, normalized->str);
258
259 key = (gpointer) "oauth_signature";
260 g_hash_table_insert (params, key, signature); /* takes ownership */
261
262 g_string_free (normalized, TRUE);
263
264 return params;
265 }
266
267 static gchar *
268 calculate_xoauth_param (const gchar *request_uri,
269 const gchar *consumer_key,
270 const gchar *consumer_secret,
271 const gchar *access_token,
272 const gchar *access_token_secret)
273 {
274 GString *str;
275 GHashTable *params;
276 GList *keys;
277 GList *iter;
278
279 params = calculate_xoauth_params (
280 request_uri,
281 consumer_key,
282 consumer_secret,
283 access_token,
284 access_token_secret);
285
286 str = g_string_new ("GET ");
287 g_string_append (str, request_uri);
288 g_string_append_c (str, ' ');
289 keys = g_hash_table_get_keys (params);
290 keys = g_list_sort (keys, (GCompareFunc) g_strcmp0);
291 for (iter = keys; iter != NULL; iter = iter->next) {
292 const gchar *key = iter->data;
293 const gchar *value;
294 gchar *k;
295 gchar *v;
296
297 value = g_hash_table_lookup (params, key);
298 if (iter != keys)
299 g_string_append_c (str, ',');
300
301 k = OAUTH_ENCODE_STRING (key);
302 v = OAUTH_ENCODE_STRING (value);
303 g_string_append_printf (str, "%s=\"%s\"", k, v);
304 g_free (k);
305 g_free (v);
306 }
307 g_list_free (keys);
308
309 g_hash_table_unref (params);
310
311 return g_string_free (str, FALSE);
312 }
313
314 /****************************************************************************/
315
316 static gchar *
317 sasl_xoauth_find_account_id (ESourceRegistry *registry,
318 const gchar *uid)
319 {
320 ESource *source;
321 const gchar *extension_name;
322
323 extension_name = E_SOURCE_EXTENSION_GOA;
324
325 while (uid != NULL) {
326 ESourceGoa *extension;
327 gchar *account_id;
328
329 source = e_source_registry_ref_source (registry, uid);
330 g_return_val_if_fail (source != NULL, NULL);
331
332 if (!e_source_has_extension (source, extension_name)) {
333 uid = e_source_get_parent (source);
334 g_object_unref (source);
335 continue;
336 }
337
338 extension = e_source_get_extension (source, extension_name);
339 account_id = e_source_goa_dup_account_id (extension);
340
341 g_object_unref (source);
342
343 return account_id;
344 }
345
346 return NULL;
347 }
348
349 static GoaObject *
350 sasl_xoauth_get_account_by_id (GoaClient *client,
351 const gchar *account_id)
352 {
353 GoaObject *match = NULL;
354 GList *list, *iter;
355
356 list = goa_client_get_accounts (client);
357
358 for (iter = list; iter != NULL; iter = g_list_next (iter)) {
359 GoaObject *goa_object;
360 GoaAccount *goa_account;
361 const gchar *candidate_id;
362
363 goa_object = GOA_OBJECT (iter->data);
364 goa_account = goa_object_get_account (goa_object);
365 candidate_id = goa_account_get_id (goa_account);
366
367 if (g_strcmp0 (account_id, candidate_id) == 0)
368 match = g_object_ref (goa_object);
369
370 g_object_unref (goa_account);
371
372 if (match != NULL)
373 break;
374 }
375
376 g_list_free_full (list, (GDestroyNotify) g_object_unref);
377
378 return match;
379 }
380
381 static GByteArray *
382 sasl_xoauth_challenge_sync (CamelSasl *sasl,
383 GByteArray *token,
384 GCancellable *cancellable,
385 GError **error)
386 {
387 GoaClient *goa_client;
388 GoaObject *goa_object;
389 GoaAccount *goa_account;
390 GByteArray *parameters = NULL;
391 CamelService *service;
392 CamelSession *session;
393 ESourceRegistry *registry;
394 const gchar *uid;
395 gchar *account_id;
396 gchar *xoauth_param = NULL;
397 gboolean success;
398
399 service = camel_sasl_get_service (sasl);
400 session = camel_service_get_session (service);
401 registry = e_mail_session_get_registry (E_MAIL_SESSION (session));
402
403 goa_client = goa_client_new_sync (cancellable, error);
404 if (goa_client == NULL)
405 return NULL;
406
407 uid = camel_service_get_uid (service);
408 account_id = sasl_xoauth_find_account_id (registry, uid);
409 goa_object = sasl_xoauth_get_account_by_id (goa_client, account_id);
410
411 g_free (account_id);
412
413 if (goa_object == NULL) {
414 g_set_error_literal (
415 error, CAMEL_SERVICE_ERROR,
416 CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE,
417 _("Cannot find a corresponding account in "
418 "the org.gnome.OnlineAccounts service from "
419 "which to obtain an authentication token."));
420 g_object_unref (goa_client);
421 return NULL;
422 }
423
424 goa_account = goa_object_get_account (goa_object);
425
426 success = goa_account_call_ensure_credentials_sync (
427 goa_account, NULL, cancellable, error);
428
429 if (success) {
430 GoaOAuthBased *goa_oauth_based;
431 const gchar *identity;
432 const gchar *consumer_key;
433 const gchar *consumer_secret;
434 const gchar *service_type;
435 gchar *access_token = NULL;
436 gchar *access_token_secret = NULL;
437 gchar *request_uri;
438
439 goa_oauth_based = goa_object_get_oauth_based (goa_object);
440
441 identity = goa_account_get_identity (goa_account);
442 service_type = CAMEL_IS_STORE (service) ? "imap" : "smtp";
443
444 /* FIXME This should probably be generalized. */
445 request_uri = g_strdup_printf (
446 "https://mail.google.com/mail/b/%s/%s/",
447 identity, service_type);
448
449 consumer_key =
450 goa_oauth_based_get_consumer_key (goa_oauth_based);
451 consumer_secret =
452 goa_oauth_based_get_consumer_secret (goa_oauth_based);
453
454 success = goa_oauth_based_call_get_access_token_sync (
455 goa_oauth_based,
456 &access_token,
457 &access_token_secret,
458 NULL,
459 cancellable,
460 error);
461
462 if (success)
463 xoauth_param = calculate_xoauth_param (
464 request_uri,
465 consumer_key,
466 consumer_secret,
467 access_token,
468 access_token_secret);
469
470 g_free (access_token);
471 g_free (access_token_secret);
472 g_free (request_uri);
473
474 g_object_unref (goa_oauth_based);
475 }
476
477 g_object_unref (goa_account);
478 g_object_unref (goa_object);
479 g_object_unref (goa_client);
480
481 if (success) {
482 /* Sanity check. */
483 g_return_val_if_fail (xoauth_param != NULL, NULL);
484
485 parameters = g_byte_array_new ();
486 g_byte_array_append (
487 parameters, (guint8 *) xoauth_param,
488 strlen (xoauth_param) + 1);
489 g_free (xoauth_param);
490 }
491
492 /* IMAP and SMTP services will Base64-encode the XOAUTH parameters. */
493
494 return parameters;
495 }
496
497 static gpointer
498 camel_sasl_xoauth_auth_type_init (gpointer unused)
499 {
500 CamelServiceAuthType *auth_type;
501
502 /* This is a one-time allocation, never freed. */
503 auth_type = g_malloc0 (sizeof (CamelServiceAuthType));
504 auth_type->name = _("OAuth");
505 auth_type->description =
506 _("This option will connect to the server by "
507 "way of the GNOME Online Accounts service");
508 auth_type->authproto = "XOAUTH";
509 auth_type->need_password = FALSE;
510
511 return auth_type;
512 }
513
514 static void
515 camel_sasl_xoauth_class_init (CamelSaslXOAuthClass *class)
516 {
517 static GOnce auth_type_once = G_ONCE_INIT;
518 CamelSaslClass *sasl_class;
519
520 g_once (&auth_type_once, camel_sasl_xoauth_auth_type_init, NULL);
521
522 g_type_class_add_private (class, sizeof (CamelSaslXOAuthPrivate));
523
524 sasl_class = CAMEL_SASL_CLASS (class);
525 sasl_class->auth_type = auth_type_once.retval;
526 sasl_class->challenge_sync = sasl_xoauth_challenge_sync;
527 }
528
529 static void
530 camel_sasl_xoauth_class_finalize (CamelSaslXOAuthClass *class)
531 {
532 }
533
534 static void
535 camel_sasl_xoauth_init (CamelSaslXOAuth *sasl)
536 {
537 sasl->priv = CAMEL_SASL_XOAUTH_GET_PRIVATE (sasl);
538 }
539
540 void
541 camel_sasl_xoauth_type_register (GTypeModule *type_module)
542 {
543 /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
544 * function, so we have to wrap it with a public function in
545 * order to register types from a separate compilation unit. */
546 camel_sasl_xoauth_register_type (type_module);
547 }