diff options
Diffstat (limited to 'modules/online-accounts/camel-sasl-xoauth.c')
-rw-r--r-- | modules/online-accounts/camel-sasl-xoauth.c | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/modules/online-accounts/camel-sasl-xoauth.c b/modules/online-accounts/camel-sasl-xoauth.c new file mode 100644 index 0000000000..9ce391214d --- /dev/null +++ b/modules/online-accounts/camel-sasl-xoauth.c @@ -0,0 +1,504 @@ +/* + * camel-sasl-xoauth.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see <http://www.gnu.org/licenses/> + * + */ + +#include "camel-sasl-xoauth.h" + +#include <config.h> +#include <glib/gi18n-lib.h> + +#define GOA_API_IS_SUBJECT_TO_CHANGE +#include <goa/goa.h> + +#define CAMEL_SASL_XOAUTH_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuthPrivate)) + +/* This is the property name or URL parameter under which we + * embed the GoaAccount ID into an EAccount or ESource object. */ +#define GOA_KEY "goa-account-id" + +struct _CamelSaslXOAuthPrivate { + gint placeholder; +}; + +G_DEFINE_DYNAMIC_TYPE (CamelSaslXOAuth, camel_sasl_xoauth, CAMEL_TYPE_SASL) + +/***************************************************************************** + * This is based on an old revision of gnome-online-accounts + * which demonstrated OAuth authentication with an IMAP server. + * + * See commit 5bcbe2a3eac4821892680e0655b27ab8c128ab15 + *****************************************************************************/ + +#include <libsoup/soup.h> + +#define OAUTH_ENCODE_STRING(str) \ + (str ? soup_uri_encode ((str), "!$&'()*+,;=@") : g_strdup ("")) + +#define SHA1_BLOCK_SIZE 64 +#define SHA1_LENGTH 20 + +/* + * hmac_sha1: + * @key: The key + * @message: The message + * + * Given the key and message, compute the HMAC-SHA1 hash and return the base-64 + * encoding of it. This is very geared towards OAuth, and as such both key and + * message must be NULL-terminated strings, and the result is base-64 encoded. + */ +static gchar * +hmac_sha1 (const gchar *key, + const gchar *message) +{ + GChecksum *checksum; + gchar *real_key; + guchar ipad[SHA1_BLOCK_SIZE]; + guchar opad[SHA1_BLOCK_SIZE]; + guchar inner[SHA1_LENGTH]; + guchar digest[SHA1_LENGTH]; + gsize key_length, inner_length, digest_length; + int i; + + g_return_val_if_fail (key, NULL); + g_return_val_if_fail (message, NULL); + + checksum = g_checksum_new (G_CHECKSUM_SHA1); + + /* If the key is longer than the block size, hash it first */ + if (strlen (key) > SHA1_BLOCK_SIZE) { + guchar new_key[SHA1_LENGTH]; + + key_length = sizeof (new_key); + + g_checksum_update (checksum, (guchar*)key, strlen (key)); + g_checksum_get_digest (checksum, new_key, &key_length); + g_checksum_reset (checksum); + + real_key = g_memdup (new_key, key_length); + } else { + real_key = g_strdup (key); + key_length = strlen (key); + } + + /* Sanity check the length */ + g_assert (key_length <= SHA1_BLOCK_SIZE); + + /* Protect against use of the provided key by NULLing it */ + key = NULL; + + /* Stage 1 */ + memset (ipad, 0, sizeof (ipad)); + memset (opad, 0, sizeof (opad)); + + memcpy (ipad, real_key, key_length); + memcpy (opad, real_key, key_length); + + /* Stage 2 and 5 */ + for (i = 0; i < sizeof (ipad); i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5C; + } + + /* Stage 3 and 4 */ + g_checksum_update (checksum, ipad, sizeof (ipad)); + g_checksum_update (checksum, (guchar*)message, strlen (message)); + inner_length = sizeof (inner); + g_checksum_get_digest (checksum, inner, &inner_length); + g_checksum_reset (checksum); + + /* Stage 6 and 7 */ + g_checksum_update (checksum, opad, sizeof (opad)); + g_checksum_update (checksum, inner, inner_length); + + digest_length = sizeof (digest); + g_checksum_get_digest (checksum, digest, &digest_length); + + g_checksum_free (checksum); + g_free (real_key); + + return g_base64_encode (digest, digest_length); +} + +static char * +sign_plaintext (const gchar *consumer_secret, + const gchar *token_secret) +{ + gchar *cs; + gchar *ts; + gchar *rv; + + cs = OAUTH_ENCODE_STRING (consumer_secret); + ts = OAUTH_ENCODE_STRING (token_secret); + rv = g_strconcat (cs, "&", ts, NULL); + + g_free (cs); + g_free (ts); + + return rv; +} + +static char * +sign_hmac (const gchar *consumer_secret, + const gchar *token_secret, + const gchar *http_method, + const gchar *request_uri, + const gchar *encoded_params) +{ + GString *text; + gchar *signature; + gchar *key; + + text = g_string_new (NULL); + g_string_append (text, http_method); + g_string_append_c (text, '&'); + g_string_append_uri_escaped (text, request_uri, NULL, FALSE); + g_string_append_c (text, '&'); + g_string_append_uri_escaped (text, encoded_params, NULL, FALSE); + + /* PLAINTEXT signature value is the HMAC-SHA1 key value */ + key = sign_plaintext (consumer_secret, token_secret); + signature = hmac_sha1 (key, text->str); + g_free (key); + + g_string_free (text, TRUE); + + return signature; +} + +static GHashTable * +calculate_xoauth_params (const gchar *request_uri, + const gchar *consumer_key, + const gchar *consumer_secret, + const gchar *access_token, + const gchar *access_token_secret) +{ + gchar *signature; + GHashTable *params; + gchar *nonce; + gchar *timestamp; + GList *keys; + GList *iter; + GString *normalized; + gpointer key; + + nonce = g_strdup_printf ("%u", g_random_int ()); + timestamp = g_strdup_printf ( + "%" G_GINT64_FORMAT, (gint64) time (NULL)); + + params = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) NULL, + (GDestroyNotify) g_free); + + key = (gpointer) "oauth_consumer_key"; + g_hash_table_insert (params, key, g_strdup (consumer_key)); + + key = (gpointer) "oauth_nonce"; + g_hash_table_insert (params, key, nonce); /* takes ownership */ + + key = (gpointer) "oauth_timestamp"; + g_hash_table_insert (params, key, timestamp); /* takes ownership */ + + key = (gpointer) "oauth_version"; + g_hash_table_insert (params, key, g_strdup ("1.0")); + + key = (gpointer) "oauth_signature_method"; + g_hash_table_insert (params, key, g_strdup ("HMAC-SHA1")); + + key = (gpointer) "oauth_token"; + g_hash_table_insert (params, key, g_strdup (access_token)); + + normalized = g_string_new (NULL); + keys = g_hash_table_get_keys (params); + keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); + for (iter = keys; iter != NULL; iter = iter->next) { + const gchar *key = iter->data; + const gchar *value; + gchar *k; + gchar *v; + + value = g_hash_table_lookup (params, key); + if (normalized->len > 0) + g_string_append_c (normalized, '&'); + + k = OAUTH_ENCODE_STRING (key); + v = OAUTH_ENCODE_STRING (value); + + g_string_append_printf (normalized, "%s=%s", k, v); + + g_free (k); + g_free (v); + } + g_list_free (keys); + + signature = sign_hmac ( + consumer_secret, access_token_secret, + "GET", request_uri, normalized->str); + + key = (gpointer) "oauth_signature"; + g_hash_table_insert (params, key, signature); /* takes ownership */ + + g_string_free (normalized, TRUE); + + return params; +} + +static gchar * +calculate_xoauth_param (const gchar *request_uri, + const gchar *consumer_key, + const gchar *consumer_secret, + const gchar *access_token, + const gchar *access_token_secret) +{ + GString *str; + GHashTable *params; + GList *keys; + GList *iter; + + params = calculate_xoauth_params ( + request_uri, + consumer_key, + consumer_secret, + access_token, + access_token_secret); + + str = g_string_new ("GET "); + g_string_append (str, request_uri); + g_string_append_c (str, ' '); + keys = g_hash_table_get_keys (params); + keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); + for (iter = keys; iter != NULL; iter = iter->next) { + const gchar *key = iter->data; + const gchar *value; + gchar *k; + gchar *v; + + value = g_hash_table_lookup (params, key); + if (iter != keys) + g_string_append_c (str, ','); + + k = OAUTH_ENCODE_STRING (key); + v = OAUTH_ENCODE_STRING (value); + g_string_append_printf (str, "%s=\"%s\"", k, v); + g_free (k); + g_free (v); + } + g_list_free (keys); + + g_hash_table_unref (params); + + return g_string_free (str, FALSE); +} + +/****************************************************************************/ + +static GoaObject * +sasl_xoauth_get_account_by_id (GoaClient *client, + const gchar *account_id) +{ + GoaObject *match = NULL; + GList *list, *iter; + + list = goa_client_get_accounts (client); + + for (iter = list; iter != NULL; iter = g_list_next (iter)) { + GoaObject *goa_object; + GoaAccount *goa_account; + const gchar *candidate_id; + + goa_object = GOA_OBJECT (iter->data); + goa_account = goa_object_get_account (goa_object); + candidate_id = goa_account_get_id (goa_account); + + if (g_strcmp0 (account_id, candidate_id) == 0) + match = g_object_ref (goa_object); + + g_object_unref (goa_account); + + if (match != NULL) + break; + } + + g_list_free_full (list, (GDestroyNotify) g_object_unref); + + return match; +} + +static GByteArray * +sasl_xoauth_challenge_sync (CamelSasl *sasl, + GByteArray *token, + GCancellable *cancellable, + GError **error) +{ + GoaClient *goa_client; + GoaObject *goa_object; + GoaAccount *goa_account; + GByteArray *parameters = NULL; + CamelService *service; + CamelURL *url; + const gchar *account_id; + gchar *xoauth_param = NULL; + gboolean success; + + service = camel_sasl_get_service (sasl); + url = camel_service_get_camel_url (service); + account_id = camel_url_get_param (url, GOA_KEY); + g_return_val_if_fail (account_id != NULL, NULL); + + goa_client = goa_client_new_sync (cancellable, error); + if (goa_client == NULL) + return NULL; + + goa_object = sasl_xoauth_get_account_by_id (goa_client, account_id); + if (goa_object == NULL) { + g_set_error_literal ( + error, CAMEL_SERVICE_ERROR, + CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, + _("Cannot find a corresponding account in " + "the org.gnome.OnlineAccounts service from " + "which to obtain an authentication token.")); + g_object_unref (goa_client); + return NULL; + } + + goa_account = goa_object_get_account (goa_object); + + success = goa_account_call_ensure_credentials_sync ( + goa_account, NULL, cancellable, error); + + if (success) { + GoaOAuthBased *goa_oauth_based; + const gchar *identity; + const gchar *consumer_key; + const gchar *consumer_secret; + const gchar *service_type; + gchar *access_token = NULL; + gchar *access_token_secret = NULL; + gchar *request_uri; + + goa_oauth_based = goa_object_get_oauth_based (goa_object); + + identity = goa_account_get_identity (goa_account); + service_type = CAMEL_IS_STORE (service) ? "imap" : "smtp"; + + /* FIXME This should probably be generalized. */ + request_uri = g_strdup_printf ( + "https://mail.google.com/mail/b/%s/%s/", + identity, service_type); + + consumer_key = + goa_oauth_based_get_consumer_key (goa_oauth_based); + consumer_secret = + goa_oauth_based_get_consumer_secret (goa_oauth_based); + + success = goa_oauth_based_call_get_access_token_sync ( + goa_oauth_based, + &access_token, + &access_token_secret, + NULL, + cancellable, + error); + + if (success) + xoauth_param = calculate_xoauth_param ( + request_uri, + consumer_key, + consumer_secret, + access_token, + access_token_secret); + + g_free (access_token); + g_free (access_token_secret); + g_free (request_uri); + + g_object_unref (goa_oauth_based); + } + + g_object_unref (goa_account); + g_object_unref (goa_object); + g_object_unref (goa_client); + + if (success) { + /* Sanity check. */ + g_return_val_if_fail (xoauth_param != NULL, NULL); + + parameters = g_byte_array_new (); + g_byte_array_append ( + parameters, (guint8 *) xoauth_param, + strlen (xoauth_param) + 1); + g_free (xoauth_param); + } + + /* IMAP and SMTP services will Base64-encode the XOAUTH parameters. */ + + return parameters; +} + +static gpointer +camel_sasl_xoauth_auth_type_init (gpointer unused) +{ + CamelServiceAuthType *auth_type; + + /* This is a one-time allocation, never freed. */ + auth_type = g_malloc0 (sizeof (CamelServiceAuthType)); + auth_type->name = _("OAuth"); + auth_type->description = + _("This option will connect to the server by " + "way of the GNOME Online Accounts service"); + auth_type->authproto = "XOAUTH"; + auth_type->need_password = FALSE; + + return auth_type; +} + +static void +camel_sasl_xoauth_class_init (CamelSaslXOAuthClass *class) +{ + static GOnce auth_type_once = G_ONCE_INIT; + CamelSaslClass *sasl_class; + + g_once (&auth_type_once, camel_sasl_xoauth_auth_type_init, NULL); + + g_type_class_add_private (class, sizeof (CamelSaslXOAuthPrivate)); + + sasl_class = CAMEL_SASL_CLASS (class); + sasl_class->auth_type = auth_type_once.retval; + sasl_class->challenge_sync = sasl_xoauth_challenge_sync; +} + +static void +camel_sasl_xoauth_class_finalize (CamelSaslXOAuthClass *class) +{ +} + +static void +camel_sasl_xoauth_init (CamelSaslXOAuth *sasl) +{ + sasl->priv = CAMEL_SASL_XOAUTH_GET_PRIVATE (sasl); +} + +void +camel_sasl_xoauth_type_register (GTypeModule *type_module) +{ + /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration + * function, so we have to wrap it with a public function in + * order to register types from a separate compilation unit. */ + camel_sasl_xoauth_register_type (type_module); +} |