evolution-3.6.4/modules/online-accounts/camel-sasl-xoauth.c

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 }