/*
* 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 <http://www.gnu.org/licenses/>.
*
* Author: Stef Walter <stefw@collabora.co.uk>
*/
#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 <sys/wait.h>
#include <string.h>
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;
}