aboutsummaryrefslogtreecommitdiffstats
path: root/modules/online-accounts/camel-sasl-xoauth.c
diff options
context:
space:
mode:
Diffstat (limited to 'modules/online-accounts/camel-sasl-xoauth.c')
-rw-r--r--modules/online-accounts/camel-sasl-xoauth.c504
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);
+}