/* * 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 * */ /* XXX Yeah, yeah... */ #define GOA_API_IS_SUBJECT_TO_CHANGE #include #include #include #include #include "camel-sasl-xoauth.h" #define CAMEL_SASL_XOAUTH_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuthPrivate)) #define HMAC_SHA1_LEN 20 /* bytes, raw */ struct _CamelSaslXOAuthPrivate { gint placeholder; }; G_DEFINE_DYNAMIC_TYPE (CamelSaslXOAuth, camel_sasl_xoauth, CAMEL_TYPE_SASL) static void sasl_xoauth_append_request (GByteArray *byte_array, const gchar *request_uri, const gchar *consumer_key, const gchar *consumer_secret, const gchar *access_token, const gchar *access_token_secret) { GString *query; GString *base_string; GString *signing_key; GString *request; GHashTable *parameters; GList *keys; GList *iter; GHmac *signature_hmac; guchar signature_digest[HMAC_SHA1_LEN]; gsize signature_digest_len; gchar *string; gpointer key, val; guint ii; const gchar *oauth_keys[] = { "oauth_version", "oauth_nonce", "oauth_timestamp", "oauth_consumer_key", "oauth_token", "oauth_signature_method", "oauth_signature" }; parameters = g_hash_table_new_full ( (GHashFunc) g_str_hash, (GEqualFunc) g_str_equal, (GDestroyNotify) NULL, (GDestroyNotify) g_free); /* Add OAuth parameters. */ key = (gpointer) "oauth_version"; g_hash_table_insert (parameters, key, g_strdup ("1.0")); key = (gpointer) "oauth_nonce"; string = g_strdup_printf ("%u", g_random_int ()); g_hash_table_insert (parameters, key, string); /* takes ownership */ key = (gpointer) "oauth_timestamp"; string = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) time (NULL)); g_hash_table_insert (parameters, key, string); /* takes ownership */ key = (gpointer) "oauth_consumer_key"; g_hash_table_insert (parameters, key, g_strdup (consumer_key)); key = (gpointer) "oauth_token"; g_hash_table_insert (parameters, key, g_strdup (access_token)); key = (gpointer) "oauth_signature_method"; g_hash_table_insert (parameters, key, g_strdup ("HMAC-SHA1")); /* Build the query part of the signature base string. * Parameters in the query part must be sorted by name. */ query = g_string_sized_new (512); keys = g_hash_table_get_keys (parameters); keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); for (iter = keys; iter != NULL; iter = iter->next) { key = iter->data; val = g_hash_table_lookup (parameters, key); if (iter != keys) g_string_append_c (query, '&'); g_string_append_uri_escaped (query, key, NULL, FALSE); g_string_append_c (query, '='); g_string_append_uri_escaped (query, val, NULL, FALSE); } g_list_free (keys); /* Build the signature base string. */ base_string = g_string_new (NULL); g_string_append (base_string, "GET"); g_string_append_c (base_string, '&'); g_string_append_uri_escaped (base_string, request_uri, NULL, FALSE); g_string_append_c (base_string, '&'); g_string_append_uri_escaped (base_string, query->str, NULL, FALSE); /* Build the HMAC-SHA1 signing key. */ signing_key = g_string_sized_new (512); g_string_append_uri_escaped ( signing_key, consumer_secret, NULL, FALSE); g_string_append_c (signing_key, '&'); g_string_append_uri_escaped ( signing_key, access_token_secret, NULL, FALSE); /* Sign the request. */ signature_digest_len = sizeof (signature_digest); signature_hmac = g_hmac_new ( G_CHECKSUM_SHA1, (guchar *) signing_key->str, signing_key->len); g_hmac_update ( signature_hmac, (guchar *) base_string->str, base_string->len); g_hmac_get_digest ( signature_hmac, signature_digest, &signature_digest_len); g_hmac_unref (signature_hmac); key = (gpointer) "oauth_signature"; string = g_base64_encode (signature_digest, signature_digest_len); g_hash_table_insert (parameters, key, string); /* takes ownership */ /* Build the formal request string. */ /* The request is easier to assemble with a GString. */ request = g_string_sized_new (512); g_string_append_printf (request, "GET %s ", request_uri); for (ii = 0; ii < G_N_ELEMENTS (oauth_keys); ii++) { key = (gpointer) oauth_keys[ii]; val = g_hash_table_lookup (parameters, key); if (ii > 0) g_string_append_c (request, ','); g_string_append (request, key); g_string_append_c (request, '='); g_string_append_c (request, '"'); g_string_append_uri_escaped (request, val, NULL, FALSE); g_string_append_c (request, '"'); } /* Copy the GString content to the GByteArray. */ g_byte_array_append ( byte_array, (guint8 *) request->str, request->len + 1); g_string_free (request, TRUE); /* Clean up. */ g_string_free (query, TRUE); g_string_free (base_string, TRUE); g_string_free (signing_key, TRUE); g_hash_table_unref (parameters); } /****************************************************************************/ static gchar * sasl_xoauth_find_account_id (ESourceRegistry *registry, const gchar *uid) { ESource *source; ESource *ancestor; const gchar *extension_name; gchar *account_id = NULL; extension_name = E_SOURCE_EXTENSION_GOA; source = e_source_registry_ref_source (registry, uid); g_return_val_if_fail (source != NULL, NULL); ancestor = e_source_registry_find_extension ( registry, source, extension_name); if (ancestor != NULL) { ESourceGoa *extension; extension = e_source_get_extension (ancestor, extension_name); account_id = e_source_goa_dup_account_id (extension); g_object_unref (ancestor); } g_object_unref (source); return account_id; } 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 *byte_array = NULL; CamelService *service; CamelSession *session; ESourceRegistry *registry; const gchar *uid; gchar *account_id; gboolean success; service = camel_sasl_get_service (sasl); session = camel_service_ref_session (service); registry = e_mail_session_get_registry (E_MAIL_SESSION (session)); goa_client = goa_client_new_sync (cancellable, error); if (goa_client == NULL) goto exit; uid = camel_service_get_uid (service); account_id = sasl_xoauth_find_account_id (registry, uid); goa_object = sasl_xoauth_get_account_by_id (goa_client, account_id); g_free (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); goto exit; } 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) { byte_array = g_byte_array_new (); sasl_xoauth_append_request ( byte_array, 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); /* IMAP and SMTP services will Base64-encode the request. */ exit: g_object_unref (session); return byte_array; } 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); }