/* * 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 * */ #include "camel-sasl-xoauth.h" #include #include #define GOA_API_IS_SUBJECT_TO_CHANGE #include #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 #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; gint 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 gchar * 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 gchar * 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); }