/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * * This library 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. * * This library 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 library. If not, see . * * Authors: Jeffrey Stedfast */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include "camel-certdb.h" #include "camel-file-utils.h" #include "camel-win32.h" #define CAMEL_CERTDB_VERSION 0x100 #define CAMEL_CERTDB_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), CAMEL_TYPE_CERTDB, CamelCertDBPrivate)) struct _CamelCertDBPrivate { gchar *filename; guint32 version; guint32 saved_certs; gboolean dirty; GPtrArray *certs; GHashTable *cert_hash; GMutex db_lock; /* for the db hashtable/array */ GMutex io_lock; /* load/save lock, for access to saved_count, etc */ }; G_DEFINE_TYPE (CamelCertDB, camel_certdb, G_TYPE_OBJECT) typedef struct { gchar *hostname; gchar *fingerprint; } CamelCertDBKey; static CamelCertDBKey * certdb_key_new (const gchar *hostname, const gchar *fingerprint) { CamelCertDBKey *key; key = g_new0 (CamelCertDBKey, 1); key->hostname = g_strdup (hostname); key->fingerprint = g_strdup (fingerprint); return key; } static void certdb_key_free (gpointer ptr) { CamelCertDBKey *key = ptr; if (!key) return; g_free (key->hostname); g_free (key->fingerprint); g_free (key); } static guint certdb_key_hash (gconstpointer ptr) { const CamelCertDBKey *key = ptr; if (!key) return 0; /* hash by fingerprint only, but compare by both hostname and fingerprint */ return g_str_hash (key->fingerprint); } static gboolean certdb_key_equal (gconstpointer ptr1, gconstpointer ptr2) { const CamelCertDBKey *key1 = ptr1, *key2 = ptr2; gboolean same_hostname; if (!key1 || !key2) return key1 == key2; if (!key1->hostname || !key2->hostname) same_hostname = key1->hostname == key2->hostname; else same_hostname = g_ascii_strcasecmp (key1->hostname, key2->hostname) == 0; if (same_hostname) { if (!key1->fingerprint || !key2->fingerprint) return key1->fingerprint == key2->fingerprint; return g_ascii_strcasecmp (key1->fingerprint, key2->fingerprint) == 0; } return same_hostname; } static void certdb_finalize (GObject *object) { CamelCertDBPrivate *priv; priv = CAMEL_CERTDB_GET_PRIVATE (object); if (priv->dirty) camel_certdb_save (CAMEL_CERTDB (object)); camel_certdb_clear (CAMEL_CERTDB (object)); g_ptr_array_free (priv->certs, TRUE); g_hash_table_destroy (priv->cert_hash); g_free (priv->filename); g_mutex_clear (&priv->db_lock); g_mutex_clear (&priv->io_lock); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (camel_certdb_parent_class)->finalize (object); } static gint certdb_header_load (CamelCertDB *certdb, FILE *istream) { if (camel_file_util_decode_uint32 ( istream, &certdb->priv->version) == -1) return -1; if (camel_file_util_decode_uint32 ( istream, &certdb->priv->saved_certs) == -1) return -1; return 0; } static gint certdb_header_save (CamelCertDB *certdb, FILE *ostream) { if (camel_file_util_encode_uint32 ( ostream, certdb->priv->version) == -1) return -1; if (camel_file_util_encode_uint32 ( ostream, certdb->priv->saved_certs) == -1) return -1; return 0; } static CamelCert * certdb_cert_load (CamelCertDB *certdb, FILE *istream) { CamelCert *cert; cert = camel_cert_new (); if (camel_file_util_decode_string (istream, &cert->issuer) == -1) goto error; if (camel_file_util_decode_string (istream, &cert->subject) == -1) goto error; if (camel_file_util_decode_string (istream, &cert->hostname) == -1) goto error; if (camel_file_util_decode_string (istream, &cert->fingerprint) == -1) goto error; if (camel_file_util_decode_uint32 (istream, &cert->trust) == -1) goto error; /* unset temporary trusts on load */ if (cert->trust == CAMEL_CERT_TRUST_TEMPORARY) cert->trust = CAMEL_CERT_TRUST_UNKNOWN; return cert; error: camel_cert_unref (cert); return NULL; } static gint certdb_cert_save (CamelCertDB *certdb, CamelCert *cert, FILE *ostream) { if (camel_file_util_encode_string (ostream, cert->issuer) == -1) return -1; if (camel_file_util_encode_string (ostream, cert->subject) == -1) return -1; if (camel_file_util_encode_string (ostream, cert->hostname) == -1) return -1; if (camel_file_util_encode_string (ostream, cert->fingerprint) == -1) return -1; if (camel_file_util_encode_uint32 (ostream, cert->trust) == -1) return -1; return 0; } static void camel_certdb_class_init (CamelCertDBClass *class) { GObjectClass *object_class; g_type_class_add_private (class, sizeof (CamelCertDBPrivate)); object_class = G_OBJECT_CLASS (class); object_class->finalize = certdb_finalize; class->header_load = certdb_header_load; class->header_save = certdb_header_save; class->cert_load = certdb_cert_load; class->cert_save = certdb_cert_save; } static void camel_certdb_init (CamelCertDB *certdb) { certdb->priv = CAMEL_CERTDB_GET_PRIVATE (certdb); certdb->priv->version = CAMEL_CERTDB_VERSION; certdb->priv->certs = g_ptr_array_new (); certdb->priv->cert_hash = g_hash_table_new_full ( (GHashFunc) certdb_key_hash, (GEqualFunc) certdb_key_equal, (GDestroyNotify) certdb_key_free, (GDestroyNotify) NULL); g_mutex_init (&certdb->priv->db_lock); g_mutex_init (&certdb->priv->io_lock); } CamelCert * camel_cert_new (void) { CamelCert *cert; cert = g_slice_new0 (CamelCert); cert->refcount = 1; return cert; } void camel_cert_ref (CamelCert *cert) { g_return_if_fail (cert != NULL); g_return_if_fail (cert->refcount > 0); g_atomic_int_inc (&cert->refcount); } void camel_cert_unref (CamelCert *cert) { g_return_if_fail (cert != NULL); g_return_if_fail (cert->refcount > 0); if (g_atomic_int_dec_and_test (&cert->refcount)) { g_free (cert->issuer); g_free (cert->subject); g_free (cert->hostname); g_free (cert->fingerprint); if (cert->rawcert != NULL) g_bytes_unref (cert->rawcert); g_slice_free (CamelCert, cert); } } static const gchar * certdb_get_cert_dir (void) { static gchar *cert_dir = NULL; if (G_UNLIKELY (cert_dir == NULL)) { const gchar *data_dir; const gchar *home_dir; gchar *old_dir; home_dir = g_get_home_dir (); data_dir = g_get_user_data_dir (); cert_dir = g_build_filename (data_dir, "camel_certs", NULL); /* Move the old certificate directory if present. */ old_dir = g_build_filename (home_dir, ".camel_certs", NULL); if (g_file_test (old_dir, G_FILE_TEST_IS_DIR)) { if (g_rename (old_dir, cert_dir) == -1) { g_warning ("%s: Failed to rename '%s' to '%s': %s", G_STRFUNC, old_dir, cert_dir, g_strerror (errno)); } } g_free (old_dir); g_mkdir_with_parents (cert_dir, 0700); } return cert_dir; } gboolean camel_cert_load_cert_file (CamelCert *cert, GError **error) { gchar *contents = NULL; gchar *filename; gsize length; const gchar *cert_dir; g_return_val_if_fail (cert != NULL, FALSE); if (cert->rawcert) { g_bytes_unref (cert->rawcert); cert->rawcert = NULL; } cert_dir = certdb_get_cert_dir (); filename = g_build_filename (cert_dir, cert->fingerprint, NULL); if (g_file_get_contents (filename, &contents, &length, error)) cert->rawcert = g_bytes_new_take (contents, length); g_free (filename); return cert->rawcert != NULL; } gboolean camel_cert_save_cert_file (CamelCert *cert, const GByteArray *der_data, GError **error) { GFile *file; GFileOutputStream *output_stream; gchar *filename; const gchar *cert_dir; g_return_val_if_fail (cert != NULL, FALSE); g_return_val_if_fail (der_data != NULL, FALSE); if (cert->rawcert) { g_bytes_unref (cert->rawcert); cert->rawcert = NULL; } cert_dir = certdb_get_cert_dir (); filename = g_build_filename (cert_dir, cert->fingerprint, NULL); file = g_file_new_for_path (filename); output_stream = g_file_replace ( file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, error); g_object_unref (file); g_free (filename); if (output_stream != NULL) { gssize n_written; GBytes *bytes; /* XXX Treat GByteArray as though its data is owned by * GTlsCertificate. That means avoiding functions * like g_byte_array_free_to_bytes() that alter or * reset the GByteArray. */ bytes = g_bytes_new (der_data->data, der_data->len); /* XXX Not handling partial writes, but GIO does not make * it easy. Need a g_output_stream_write_all_bytes(). * (see: https://bugzilla.gnome.org/708838) */ n_written = g_output_stream_write_bytes ( G_OUTPUT_STREAM (output_stream), bytes, NULL, error); if (n_written < 0) { g_bytes_unref (bytes); bytes = NULL; } cert->rawcert = bytes; g_object_unref (output_stream); } return cert->rawcert != NULL; } CamelCertDB * camel_certdb_new (void) { return g_object_new (CAMEL_TYPE_CERTDB, NULL); } static CamelCertDB *default_certdb = NULL; static GMutex default_certdb_lock; void camel_certdb_set_default (CamelCertDB *certdb) { g_mutex_lock (&default_certdb_lock); if (default_certdb) g_object_unref (default_certdb); if (certdb) g_object_ref (certdb); default_certdb = certdb; g_mutex_unlock (&default_certdb_lock); } CamelCertDB * camel_certdb_get_default (void) { CamelCertDB *certdb; g_mutex_lock (&default_certdb_lock); if (default_certdb) g_object_ref (default_certdb); certdb = default_certdb; g_mutex_unlock (&default_certdb_lock); return certdb; } void camel_certdb_set_filename (CamelCertDB *certdb, const gchar *filename) { g_return_if_fail (CAMEL_IS_CERTDB (certdb)); g_return_if_fail (filename != NULL); g_mutex_lock (&certdb->priv->db_lock); g_free (certdb->priv->filename); certdb->priv->filename = g_strdup (filename); g_mutex_unlock (&certdb->priv->db_lock); } gint camel_certdb_load (CamelCertDB *certdb) { CamelCertDBClass *class; CamelCert *cert; FILE *in; gint i; g_return_val_if_fail (CAMEL_IS_CERTDB (certdb), -1); g_return_val_if_fail (certdb->priv->filename != NULL, -1); in = g_fopen (certdb->priv->filename, "rb"); if (in == NULL) return -1; class = CAMEL_CERTDB_GET_CLASS (certdb); if (!class->header_load || !class->cert_load) { fclose (in); in = NULL; g_warn_if_reached (); return -1; } g_mutex_lock (&certdb->priv->io_lock); if (class->header_load (certdb, in) == -1) goto error; for (i = 0; i < certdb->priv->saved_certs; i++) { cert = class->cert_load (certdb, in); if (cert == NULL) goto error; /* NOTE: If we are upgrading from an evolution-data-server version * prior to the change to look up certs by hostname (bug 606181), * this "put" will result in duplicate entries for the same * hostname being dropped. The change will become permanent on * disk the next time the certdb is dirtied for some reason and * has to be saved. */ camel_certdb_put (certdb, cert); } g_mutex_unlock (&certdb->priv->io_lock); if (fclose (in) != 0) return -1; certdb->priv->dirty = FALSE; return 0; error: g_warning ("Cannot load certificate database: %s", g_strerror (ferror (in))); g_mutex_unlock (&certdb->priv->io_lock); fclose (in); return -1; } gint camel_certdb_save (CamelCertDB *certdb) { CamelCertDBClass *class; CamelCert *cert; gchar *filename; gsize filename_len; gint fd, i; FILE *out; g_return_val_if_fail (CAMEL_IS_CERTDB (certdb), -1); g_return_val_if_fail (certdb->priv->filename != NULL, -1); /* no change, nothing new to save, simply return success */ if (!certdb->priv->dirty) return 0; filename_len = strlen (certdb->priv->filename) + 4; filename = alloca (filename_len); g_snprintf (filename, filename_len, "%s~", certdb->priv->filename); fd = g_open (filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0600); if (fd == -1) return -1; out = fdopen (fd, "wb"); if (out == NULL) { i = errno; close (fd); g_unlink (filename); errno = i; return -1; } class = CAMEL_CERTDB_GET_CLASS (certdb); if (!class->header_save || !class->cert_save) { fclose (out); out = NULL; g_warn_if_reached (); return -1; } g_mutex_lock (&certdb->priv->io_lock); certdb->priv->saved_certs = certdb->priv->certs->len; if (class->header_save (certdb, out) == -1) goto error; for (i = 0; i < certdb->priv->saved_certs; i++) { cert = (CamelCert *) certdb->priv->certs->pdata[i]; if (class->cert_save (certdb, cert, out) == -1) goto error; } g_mutex_unlock (&certdb->priv->io_lock); if (fflush (out) != 0 || fsync (fileno (out)) == -1) { i = errno; fclose (out); g_unlink (filename); errno = i; return -1; } if (fclose (out) != 0) { i = errno; g_unlink (filename); errno = i; return -1; } if (g_rename (filename, certdb->priv->filename) == -1) { i = errno; g_unlink (filename); errno = i; return -1; } certdb->priv->dirty = FALSE; return 0; error: g_warning ("Cannot save certificate database: %s", g_strerror (ferror (out))); g_mutex_unlock (&certdb->priv->io_lock); i = errno; fclose (out); g_unlink (filename); errno = i; return -1; } void camel_certdb_touch (CamelCertDB *certdb) { g_return_if_fail (CAMEL_IS_CERTDB (certdb)); certdb->priv->dirty = TRUE; } /** * camel_certdb_get_host: * * FIXME Document me! * * Since: 3.6 **/ CamelCert * camel_certdb_get_host (CamelCertDB *certdb, const gchar *hostname, const gchar *fingerprint) { CamelCert *cert; CamelCertDBKey *key; g_return_val_if_fail (CAMEL_IS_CERTDB (certdb), NULL); g_mutex_lock (&certdb->priv->db_lock); key = certdb_key_new (hostname, fingerprint); cert = g_hash_table_lookup (certdb->priv->cert_hash, key); if (cert != NULL) camel_cert_ref (cert); certdb_key_free (key); g_mutex_unlock (&certdb->priv->db_lock); return cert; } /** * camel_certdb_put: * * FIXME Document me! * * Since: 3.6 **/ void camel_certdb_put (CamelCertDB *certdb, CamelCert *cert) { CamelCert *old_cert; CamelCertDBKey *key; g_return_if_fail (CAMEL_IS_CERTDB (certdb)); g_mutex_lock (&certdb->priv->db_lock); key = certdb_key_new (cert->hostname, cert->fingerprint); /* Replace an existing entry with the same hostname. */ old_cert = g_hash_table_lookup (certdb->priv->cert_hash, key); if (old_cert != NULL) { g_hash_table_remove (certdb->priv->cert_hash, key); g_ptr_array_remove (certdb->priv->certs, old_cert); camel_cert_unref (old_cert); } camel_cert_ref (cert); g_ptr_array_add (certdb->priv->certs, cert); /* takes ownership of 'key' */ g_hash_table_insert (certdb->priv->cert_hash, key, cert); certdb->priv->dirty = TRUE; g_mutex_unlock (&certdb->priv->db_lock); } /** * camel_certdb_remove_host: * * FIXME Document me! * * Since: 3.6 **/ void camel_certdb_remove_host (CamelCertDB *certdb, const gchar *hostname, const gchar *fingerprint) { CamelCert *cert; CamelCertDBKey *key; g_return_if_fail (CAMEL_IS_CERTDB (certdb)); g_mutex_lock (&certdb->priv->db_lock); key = certdb_key_new (hostname, fingerprint); cert = g_hash_table_lookup (certdb->priv->cert_hash, key); if (cert != NULL) { g_hash_table_remove (certdb->priv->cert_hash, key); g_ptr_array_remove (certdb->priv->certs, cert); camel_cert_unref (cert); certdb->priv->dirty = TRUE; } certdb_key_free (key); g_mutex_unlock (&certdb->priv->db_lock); } static gboolean cert_remove (gpointer key, gpointer value, gpointer user_data) { return TRUE; } void camel_certdb_clear (CamelCertDB *certdb) { CamelCert *cert; gint i; g_return_if_fail (CAMEL_IS_CERTDB (certdb)); g_mutex_lock (&certdb->priv->db_lock); g_hash_table_foreach_remove (certdb->priv->cert_hash, cert_remove, NULL); for (i = 0; i < certdb->priv->certs->len; i++) { cert = (CamelCert *) certdb->priv->certs->pdata[i]; camel_cert_unref (cert); } certdb->priv->saved_certs = 0; g_ptr_array_set_size (certdb->priv->certs, 0); certdb->priv->dirty = TRUE; g_mutex_unlock (&certdb->priv->db_lock); } /** * camel_certdb_list_certs: * @certdb: a #CamelCertDB * * Gathers a list of known certificates. Each certificate in the returned #GSList * is referenced, thus unref it with camel_cert_unref() when done with it, the same * as free the list itself. * * Returns: (transfer full): (element-type CamelCert): Newly allocated list of * referenced CamelCert-s, which are stored in the @certdb. * * Since: 3.16 **/ GSList * camel_certdb_list_certs (CamelCertDB *certdb) { GSList *certs = NULL; gint ii; g_return_val_if_fail (CAMEL_IS_CERTDB (certdb), NULL); g_mutex_lock (&certdb->priv->db_lock); for (ii = 0; ii < certdb->priv->certs->len; ii++) { CamelCert *cert = (CamelCert *) certdb->priv->certs->pdata[ii]; camel_cert_ref (cert); certs = g_slist_prepend (certs, cert); } g_mutex_unlock (&certdb->priv->db_lock); return g_slist_reverse (certs); }