/* * gnome-keyring * * Copyright (C) 2011 Collabora Ltd. * * 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.1 of * the License, or (at your option) any later version. * * 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 this program; if not, see . * * Author: Stef Walter */ #include "config.h" #include "gcr-callback-output-stream.h" #include "gcr-collection.h" #include "gcr-gnupg-collection.h" #include "gcr-gnupg-key.h" #include "gcr-gnupg-process.h" #include "gcr-gnupg-records.h" #include "gcr-gnupg-util.h" #include "gcr-internal.h" #include "gcr-record.h" #include "gcr-util.h" #include #include enum { PROP_0, PROP_DIRECTORY }; struct _GcrGnupgCollectionPrivate { GHashTable *items; /* char *keyid -> GcrGnupgKey* */ gchar *directory; }; /* Forward declarations */ static void _gcr_collection_iface (GcrCollectionIface *iface); G_DEFINE_TYPE_WITH_CODE (GcrGnupgCollection, _gcr_gnupg_collection, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GCR_TYPE_COLLECTION, _gcr_collection_iface) ); static void _gcr_gnupg_collection_init (GcrGnupgCollection *self) { self->pv = G_TYPE_INSTANCE_GET_PRIVATE (self, GCR_TYPE_GNUPG_COLLECTION, GcrGnupgCollectionPrivate); self->pv->items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); } static void _gcr_gnupg_collection_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (obj); switch (prop_id) { case PROP_DIRECTORY: g_return_if_fail (!self->pv->directory); self->pv->directory = g_value_dup_string (value); if (self->pv->directory && !g_path_is_absolute (self->pv->directory)) { g_warning ("gnupg collection directory path should be absolute: %s", self->pv->directory); } break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); break; } } static void _gcr_gnupg_collection_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (obj); switch (prop_id) { case PROP_DIRECTORY: g_value_set_string (value, self->pv->directory); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); break; } } static void _gcr_gnupg_collection_dispose (GObject *obj) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (obj); g_hash_table_remove_all (self->pv->items); G_OBJECT_CLASS (_gcr_gnupg_collection_parent_class)->dispose (obj); } static void _gcr_gnupg_collection_finalize (GObject *obj) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (obj); g_assert (self->pv->items); g_assert (g_hash_table_size (self->pv->items) == 0); g_hash_table_destroy (self->pv->items); self->pv->items = NULL; g_free (self->pv->directory); self->pv->directory = NULL; G_OBJECT_CLASS (_gcr_gnupg_collection_parent_class)->finalize (obj); } static void _gcr_gnupg_collection_class_init (GcrGnupgCollectionClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->get_property = _gcr_gnupg_collection_get_property; gobject_class->set_property = _gcr_gnupg_collection_set_property; gobject_class->dispose = _gcr_gnupg_collection_dispose; gobject_class->finalize = _gcr_gnupg_collection_finalize; /** * GcrGnupgCollection:directory: * * Directory to load the gnupg keys from, or %NULL for default * ~/.gnupg/ directory. */ g_object_class_install_property (gobject_class, PROP_DIRECTORY, g_param_spec_string ("directory", "Directory", "Gnupg Directory", NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); g_type_class_add_private (gobject_class, sizeof (GcrGnupgCollectionPrivate)); _gcr_initialize_library (); } static guint gcr_gnupg_collection_real_get_length (GcrCollection *coll) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (coll); return g_hash_table_size (self->pv->items); } static GList* gcr_gnupg_collection_real_get_objects (GcrCollection *coll) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (coll); return g_hash_table_get_values (self->pv->items); } static gboolean gcr_gnupg_collection_real_contains (GcrCollection *collection, GObject *object) { GcrGnupgCollection *self = GCR_GNUPG_COLLECTION (collection); GcrGnupgKey *key; if (!GCR_IS_GNUPG_KEY (object)) return FALSE; key = g_hash_table_lookup (self->pv->items, _gcr_gnupg_key_get_keyid (GCR_GNUPG_KEY (object))); if (key != NULL && G_OBJECT (key) == object) return TRUE; return FALSE; } static void _gcr_collection_iface (GcrCollectionIface *iface) { iface->get_length = gcr_gnupg_collection_real_get_length; iface->get_objects = gcr_gnupg_collection_real_get_objects; iface->contains = gcr_gnupg_collection_real_contains; } /** * _gcr_gnupg_collection_new: * @directory: (allow-none): The gnupg home directory. * * Create a new GcrGnupgCollection. * * The gnupg home directory is where the keyring files live. If directory is * %NULL then the default gnupg home directory is used. * * Returns: (transfer full) (type Gcr.GnupgCollection): A newly allocated collection. */ GcrCollection* _gcr_gnupg_collection_new (const gchar *directory) { return g_object_new (GCR_TYPE_GNUPG_COLLECTION, "directory", directory, NULL); } /* * We have to run the gnupg process twice to list the public and then the * secret keys. These phases are tracked by GcrLoadingPhase. If the first * phase completes successfully (using gpg --list-keys) then we move on to * the second phase where the secret keys are loaded (using gpg --list-secret-keys) * * If a key is loaded as a public key by the public phase, it can be updated by * the secret phase. A key discovered in the secret phase must have a public * counterpart already loaded by the public phase. */ typedef enum { GCR_LOADING_PHASE_PUBLIC = 1, GCR_LOADING_PHASE_SECRET = 2, } GcrLoadingPhase; /* * We use @difference to track the keys that were in the collection before * the load process, and then remove any not found, at the end of the load * process. Strings are directly used from collection->pv->items keys. */ typedef struct { GcrGnupgCollection *collection; /* reffed pointer back to collection */ GcrLoadingPhase loading_phase; /* Whether loading public or private */ GPtrArray *records; /* GcrRecord* not yet made into a key */ GcrGnupgProcess *process; /* The gnupg process itself */ GCancellable *cancel; /* Cancellation for process */ GString *out_data; /* Pending output not yet parsed into colons */ GHashTable *difference; /* Hashset gchar *keyid -> gchar *keyid */ guint error_sig; guint status_sig; GOutputStream *output; GOutputStream *outattr; GQueue *attribute_queue; /* Queue of unprocessed GcrRecord* status records */ GByteArray *attribute_buf; /* Buffer of unprocessed attribute data received */ GHashTable *attributes; /* Processed attributes waiting for a matching key */ } GcrGnupgCollectionLoad; /* Forward declarations */ static void spawn_gnupg_list_process (GcrGnupgCollectionLoad *load, GSimpleAsyncResult *res); static void _gcr_gnupg_collection_load_free (gpointer data) { GcrGnupgCollectionLoad *load = data; g_assert (load); g_ptr_array_unref (load->records); g_string_free (load->out_data, TRUE); g_hash_table_destroy (load->difference); g_object_unref (load->collection); if (load->process) { if (load->error_sig) g_signal_handler_disconnect (load->process, load->error_sig); if (load->status_sig) g_signal_handler_disconnect (load->process, load->status_sig); g_object_unref (load->process); } g_output_stream_close (load->output, NULL, NULL); g_object_unref (load->output); g_output_stream_close (load->outattr, NULL, NULL); g_object_unref (load->outattr); if (load->cancel) g_object_unref (load->cancel); if (load->attribute_queue) { while (!g_queue_is_empty (load->attribute_queue)) _gcr_record_free (g_queue_pop_head (load->attribute_queue)); g_queue_free (load->attribute_queue); } if (load->attribute_buf) g_byte_array_unref (load->attribute_buf); if (load->attributes) g_hash_table_destroy (load->attributes); g_slice_free (GcrGnupgCollectionLoad, load); } static void process_records_as_public_key (GcrGnupgCollectionLoad *load, GPtrArray *records, const gchar *keyid) { GPtrArray *attr_records = NULL; const gchar *fingerprint; gchar *orig_fingerprint; GcrGnupgKey *key; guint i; /* Add in any attributes we have loaded */ fingerprint = _gcr_gnupg_records_get_fingerprint (records); if (fingerprint && load->attributes) attr_records = g_hash_table_lookup (load->attributes, fingerprint); if (attr_records) { g_debug ("adding %d user id attribute(s) to key/fingerprint: %s/%s", (gint)attr_records->len, keyid, fingerprint); if (!g_hash_table_lookup_extended (load->attributes, fingerprint, (gpointer*)&orig_fingerprint, NULL)) g_assert_not_reached (); if (!g_hash_table_steal (load->attributes, fingerprint)) g_assert_not_reached (); g_free (orig_fingerprint); /* Move all the attribute records over to main records set */ for (i = 0; i < attr_records->len; i++) g_ptr_array_add (records, attr_records->pdata[i]); /* Shallow free of attr_records array */ g_free (g_ptr_array_free (attr_records, FALSE)); } /* Note that we've seen this keyid */ g_hash_table_remove (load->difference, keyid); key = g_hash_table_lookup (load->collection->pv->items, keyid); /* Already have this key, just update */ if (key) { g_debug ("updating public key: %s", keyid); _gcr_gnupg_key_set_public_records (key, records); /* Add a new key */ } else { key = _gcr_gnupg_key_new (records, NULL); g_debug ("creating public key: %s", keyid); g_hash_table_insert (load->collection->pv->items, g_strdup (keyid), key); gcr_collection_emit_added (GCR_COLLECTION (load->collection), G_OBJECT (key)); } } static void process_records_as_secret_key (GcrGnupgCollectionLoad *load, GPtrArray *records, const gchar *keyid) { GcrGnupgKey *key; key = g_hash_table_lookup (load->collection->pv->items, keyid); /* Don't have this key */ if (key == NULL) { g_message ("Secret key seen but no public key for: %s", keyid); /* Tell the private key that it's a secret one */ } else { g_debug ("adding secret records to key: %s", keyid); _gcr_gnupg_key_set_secret_records (key, records); } } static void process_records_as_key (GcrGnupgCollectionLoad *load) { GPtrArray *records; const gchar *keyid; GQuark schema; g_assert (load->records->len); records = load->records; load->records = g_ptr_array_new_with_free_func (_gcr_record_free); keyid = _gcr_gnupg_records_get_keyid (records); if (keyid) { schema = _gcr_record_get_schema (records->pdata[0]); /* A public key */ if (schema == GCR_RECORD_SCHEMA_PUB) process_records_as_public_key (load, records, keyid); /* A secret key */ else if (schema == GCR_RECORD_SCHEMA_SEC) process_records_as_secret_key (load, records, keyid); else g_assert_not_reached (); } else { g_warning ("parsed gnupg data had no keyid"); } g_ptr_array_unref (records); } static gboolean process_outstanding_attribute (GcrGnupgCollectionLoad *load, GcrRecord *record) { const gchar *fingerprint; GPtrArray *records; GcrRecord *xa1; guint length; if (!_gcr_record_get_uint (record, GCR_RECORD_ATTRIBUTE_LENGTH, &length)) g_return_val_if_reached (FALSE); fingerprint = _gcr_record_get_raw (record, GCR_RECORD_ATTRIBUTE_KEY_FINGERPRINT); g_return_val_if_fail (fingerprint != NULL, FALSE); /* Do we have enough data for this attribute? */ if (!load->attribute_buf || load->attribute_buf->len < length) { g_debug ("not enough attribute data in buffer: %u", length); return FALSE; } if (!load->attributes) load->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_ptr_array_unref); records = g_hash_table_lookup (load->attributes, fingerprint); if (!records) { records = g_ptr_array_new_with_free_func (_gcr_record_free); g_hash_table_insert (load->attributes, g_strdup (fingerprint), records); } g_debug ("new attribute of length %d for key with fingerprint %s", length, fingerprint); xa1 = _gcr_gnupg_build_xa1_record (record, load->attribute_buf->data, length); g_ptr_array_add (records, xa1); /* Did we use up all the attribute data? Get rid of the buffer */ if (length == load->attribute_buf->len) { g_byte_array_unref (load->attribute_buf); load->attribute_buf = NULL; /* Otherwise clear out the used data from buffer */ } else { g_byte_array_remove_range (load->attribute_buf, 0, length); } return TRUE; } static void process_outstanding_attributes (GcrGnupgCollectionLoad *load) { GcrRecord *record; if (load->attribute_queue == NULL) return; g_debug ("%d outstanding attribute records", (gint)g_queue_get_length (load->attribute_queue)); for (;;) { record = g_queue_peek_head (load->attribute_queue); if (record == NULL) break; if (!process_outstanding_attribute (load, record)) break; g_queue_pop_head (load->attribute_queue); _gcr_record_free (record); } } static void on_line_parse_output (const gchar *line, gpointer user_data) { GcrGnupgCollectionLoad *load = user_data; GcrRecord *record; GQuark schema; g_debug ("output: %s", line); record = _gcr_record_parse_colons (line, -1); if (!record) { g_warning ("invalid gnupg output line: %s", line); return; } schema = _gcr_record_get_schema (record); /* * Each time we see a line with 'pub' or 'sec' schema we assume that * it's a new key being listed. */ if (schema == GCR_RECORD_SCHEMA_PUB || schema == GCR_RECORD_SCHEMA_SEC) { g_debug ("start of new key"); if (load->records->len) process_records_as_key (load); g_assert (!load->records->len); g_ptr_array_add (load->records, record); record = NULL; /* * 'uid' and 'fpr' schema lines get added to the key that came before. */ } else if (schema == GCR_RECORD_SCHEMA_UID || schema == GCR_RECORD_SCHEMA_FPR) { if (load->records->len) { g_ptr_array_add (load->records, record); record = NULL; } } if (record != NULL) _gcr_record_free (record); } static gssize on_gnupg_process_output_data (gconstpointer buffer, gsize count, GCancellable *cancellable, gpointer user_data, GError **error) { GSimpleAsyncResult *res = G_SIMPLE_ASYNC_RESULT (user_data); GcrGnupgCollectionLoad *load = g_simple_async_result_get_op_res_gpointer (res); g_string_append_len (load->out_data, buffer, count); _gcr_util_parse_lines (load->out_data, FALSE, on_line_parse_output, load); return count; } static void on_gnupg_process_error_line (GcrGnupgProcess *process, const gchar *line, gpointer user_data) { g_printerr ("%s\n", line); } static void on_gnupg_process_status_record (GcrGnupgProcess *process, GcrRecord *record, gpointer user_data) { GSimpleAsyncResult *res = G_SIMPLE_ASYNC_RESULT (user_data); GcrGnupgCollectionLoad *load = g_simple_async_result_get_op_res_gpointer (res); if (GCR_RECORD_SCHEMA_ATTRIBUTE != _gcr_record_get_schema (record)) return; if (!load->attribute_queue) load->attribute_queue = g_queue_new (); g_queue_push_tail (load->attribute_queue, _gcr_record_copy (record)); process_outstanding_attributes (load); } static gssize on_gnupg_process_attribute_data (gconstpointer buffer, gsize count, GCancellable *cancellable, gpointer user_data, GError **error) { GSimpleAsyncResult *res = G_SIMPLE_ASYNC_RESULT (user_data); GcrGnupgCollectionLoad *load = g_simple_async_result_get_op_res_gpointer (res); /* If we don't have a buffer, just claim this one */ if (!load->attribute_buf) load->attribute_buf = g_byte_array_new (); g_byte_array_append (load->attribute_buf, buffer, count); process_outstanding_attributes (load); return count; } static void on_gnupg_process_completed (GObject *source, GAsyncResult *result, gpointer user_data) { GSimpleAsyncResult *res = G_SIMPLE_ASYNC_RESULT (user_data); GcrGnupgCollectionLoad *load = g_simple_async_result_get_op_res_gpointer (res); GHashTableIter iter; GError *error = NULL; GObject *object; gpointer keyid; if (!_gcr_gnupg_process_run_finish (GCR_GNUPG_PROCESS (source), result, &error)) { g_simple_async_result_set_from_error (res, error); g_simple_async_result_complete (res); g_object_unref (res); g_clear_error (&error); return; } /* Process any remaining output */ _gcr_util_parse_lines (load->out_data, TRUE, on_line_parse_output, load); /* Process last bit as a key, if any */ if (load->records->len) process_records_as_key (load); /* If we completed loading public keys, then go and load secret */ switch (load->loading_phase) { case GCR_LOADING_PHASE_PUBLIC: g_debug ("public load phase completed"); load->loading_phase = GCR_LOADING_PHASE_SECRET; spawn_gnupg_list_process (load, res); g_object_unref (res); return; case GCR_LOADING_PHASE_SECRET: g_debug ("secret load phase completed"); /* continue below */ break; default: g_assert_not_reached (); } /* Remove any keys that we still have in the difference */ g_hash_table_iter_init (&iter, load->difference); while (g_hash_table_iter_next (&iter, &keyid, NULL)) { object = g_hash_table_lookup (load->collection->pv->items, keyid); if (object != NULL) { g_object_ref (object); g_debug ("removing key no longer present in keyring: %s", (gchar*)keyid); g_hash_table_remove (load->collection->pv->items, keyid); gcr_collection_emit_removed (GCR_COLLECTION (load->collection), object); g_object_unref (object); } } g_simple_async_result_complete (res); g_object_unref (res); } static void spawn_gnupg_list_process (GcrGnupgCollectionLoad *load, GSimpleAsyncResult *res) { GcrGnupgProcessFlags flags = 0; GPtrArray *argv; argv = g_ptr_array_new (); switch (load->loading_phase) { case GCR_LOADING_PHASE_PUBLIC: g_debug ("starting public load phase"); g_ptr_array_add (argv, (gpointer)"--list-keys"); /* Load photos in public phase */ flags = GCR_GNUPG_PROCESS_WITH_ATTRIBUTES | GCR_GNUPG_PROCESS_WITH_STATUS; break; case GCR_LOADING_PHASE_SECRET: g_debug ("starting secret load phase"); g_ptr_array_add (argv, (gpointer)"--list-secret-keys"); break; default: g_assert_not_reached (); } g_ptr_array_add (argv, (gpointer)"--fixed-list-mode"); g_ptr_array_add (argv, (gpointer)"--with-colons"); g_ptr_array_add (argv, (gpointer)"--with-fingerprint"); g_ptr_array_add (argv, NULL); /* res is unreffed in on_gnupg_process_completed */ _gcr_gnupg_process_run_async (load->process, (const gchar**)argv->pdata, NULL, flags, load->cancel, on_gnupg_process_completed, g_object_ref (res)); g_ptr_array_unref (argv); } /** * _gcr_gnupg_collection_load_async: * @self: The collection * @cancellable: Cancellation object or %NULL * @callback: Callback to call when result is ready * @user_data: Data for callback * * Start an operation to load or reload the list of gnupg keys in this * collection. */ void _gcr_gnupg_collection_load_async (GcrGnupgCollection *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *res; GcrGnupgCollectionLoad *load; GHashTableIter iter; gpointer keyid; g_return_if_fail (GCR_IS_GNUPG_COLLECTION (self)); /* TODO: Cancellation not yet implemented */ res = g_simple_async_result_new (G_OBJECT (self), callback, user_data, _gcr_gnupg_collection_load_async); load = g_slice_new0 (GcrGnupgCollectionLoad); load->records = g_ptr_array_new_with_free_func (_gcr_record_free); load->out_data = g_string_sized_new (1024); load->collection = g_object_ref (self); load->cancel = cancellable ? g_object_ref (cancellable) : cancellable; load->output = _gcr_callback_output_stream_new (on_gnupg_process_output_data, res, NULL); load->outattr = _gcr_callback_output_stream_new (on_gnupg_process_attribute_data, res, NULL); load->process = _gcr_gnupg_process_new (self->pv->directory, NULL); _gcr_gnupg_process_set_output_stream (load->process, load->output); _gcr_gnupg_process_set_attribute_stream (load->process, load->outattr); load->error_sig = g_signal_connect (load->process, "error-line", G_CALLBACK (on_gnupg_process_error_line), res); load->status_sig = g_signal_connect (load->process, "status-record", G_CALLBACK (on_gnupg_process_status_record), res); /* * Track all the keys we currently have, at end remove those that * didn't get listed by the gpg process. */ load->difference = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_iter_init (&iter, self->pv->items); while (g_hash_table_iter_next (&iter, &keyid, NULL)) g_hash_table_insert (load->difference, keyid, keyid); g_simple_async_result_set_op_res_gpointer (res, load, _gcr_gnupg_collection_load_free); load->loading_phase = GCR_LOADING_PHASE_PUBLIC; spawn_gnupg_list_process (load, res); g_object_unref (res); } /** * _gcr_gnupg_collection_load_finish: * @self: The collection * @result: The result passed to the callback * @error: Location to raise an error on failure. * * Get the result of an operation to load or reload the list of gnupg keys * in this collection. */ gboolean _gcr_gnupg_collection_load_finish (GcrGnupgCollection *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (GCR_IS_GNUPG_COLLECTION (self), FALSE); g_return_val_if_fail (!error || !*error, FALSE); g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self), _gcr_gnupg_collection_load_async), FALSE); if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error)) return FALSE; return TRUE; }