summaryrefslogtreecommitdiffstats
path: root/agent/cache.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 09:59:15 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 09:59:15 +0000
commit8de1ee1b2b676b0d07586f0752750dd6b0fb7511 (patch)
treedd46fd7dc3863045696cd0e48032d8a36fa0daf5 /agent/cache.c
parentInitial commit. (diff)
downloadgnupg2-8de1ee1b2b676b0d07586f0752750dd6b0fb7511.tar.xz
gnupg2-8de1ee1b2b676b0d07586f0752750dd6b0fb7511.zip
Adding upstream version 2.2.27.upstream/2.2.27upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--agent/cache.c530
1 files changed, 530 insertions, 0 deletions
diff --git a/agent/cache.c b/agent/cache.c
new file mode 100644
index 0000000..238b6e2
--- /dev/null
+++ b/agent/cache.c
@@ -0,0 +1,530 @@
+/* cache.c - keep a cache of passphrases
+ * Copyright (C) 2002, 2010 Free Software Foundation, Inc.
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <assert.h>
+#include <npth.h>
+
+#include "agent.h"
+
+/* The size of the encryption key in bytes. */
+#define ENCRYPTION_KEYSIZE (128/8)
+
+/* A mutex used to serialize access to the cache. */
+static npth_mutex_t cache_lock;
+/* The encryption context. This is the only place where the
+ encryption key for all cached entries is available. It would be nice
+ to keep this (or just the key) in some hardware device, for example
+ a TPM. Libgcrypt could be extended to provide such a service.
+ With the current scheme it is easy to retrieve the cached entries
+ if access to Libgcrypt's memory is available. The encryption
+ merely avoids grepping for clear texts in the memory. Nevertheless
+ the encryption provides the necessary infrastructure to make it
+ more secure. */
+static gcry_cipher_hd_t encryption_handle;
+
+
+struct secret_data_s {
+ int totallen; /* This includes the padding and space for AESWRAP. */
+ char data[1]; /* A string. */
+};
+
+typedef struct cache_item_s *ITEM;
+struct cache_item_s {
+ ITEM next;
+ time_t created;
+ time_t accessed;
+ int ttl; /* max. lifetime given in seconds, -1 one means infinite */
+ struct secret_data_s *pw;
+ cache_mode_t cache_mode;
+ int restricted; /* The value of ctrl->restricted is part of the key. */
+ char key[1];
+};
+
+/* The cache himself. */
+static ITEM thecache;
+
+/* NULL or the last cache key stored by agent_store_cache_hit. */
+static char *last_stored_cache_key;
+
+
+/* This function must be called once to initialize this module. It
+ has to be done before a second thread is spawned. */
+void
+initialize_module_cache (void)
+{
+ int err;
+
+ err = npth_mutex_init (&cache_lock, NULL);
+
+ if (err)
+ log_fatal ("error initializing cache module: %s\n", strerror (err));
+}
+
+
+void
+deinitialize_module_cache (void)
+{
+ gcry_cipher_close (encryption_handle);
+ encryption_handle = NULL;
+}
+
+
+/* We do the encryption init on the fly. We can't do it in the module
+ init code because that is run before we listen for connections and
+ in case we are started on demand by gpg etc. it will only wait for
+ a few seconds to decide whether the agent may now accept
+ connections. Thus we should get into listen state as soon as
+ possible. */
+static gpg_error_t
+init_encryption (void)
+{
+ gpg_error_t err;
+ void *key;
+
+ if (encryption_handle)
+ return 0; /* Shortcut - Already initialized. */
+
+ err = gcry_cipher_open (&encryption_handle, GCRY_CIPHER_AES128,
+ GCRY_CIPHER_MODE_AESWRAP, GCRY_CIPHER_SECURE);
+ if (!err)
+ {
+ key = gcry_random_bytes (ENCRYPTION_KEYSIZE, GCRY_STRONG_RANDOM);
+ if (!key)
+ err = gpg_error_from_syserror ();
+ else
+ {
+ err = gcry_cipher_setkey (encryption_handle, key, ENCRYPTION_KEYSIZE);
+ xfree (key);
+ }
+ if (err)
+ {
+ gcry_cipher_close (encryption_handle);
+ encryption_handle = NULL;
+ }
+ }
+ if (err)
+ log_error ("error initializing cache encryption context: %s\n",
+ gpg_strerror (err));
+
+ return err? gpg_error (GPG_ERR_NOT_INITIALIZED) : 0;
+}
+
+
+
+static void
+release_data (struct secret_data_s *data)
+{
+ xfree (data);
+}
+
+static gpg_error_t
+new_data (const char *string, struct secret_data_s **r_data)
+{
+ gpg_error_t err;
+ struct secret_data_s *d, *d_enc;
+ size_t length;
+ int total;
+
+ *r_data = NULL;
+
+ err = init_encryption ();
+ if (err)
+ return err;
+
+ length = strlen (string) + 1;
+
+ /* We pad the data to 32 bytes so that it get more complicated
+ finding something out by watching allocation patterns. This is
+ usually not possible but we better assume nothing about our secure
+ storage provider. To support the AESWRAP mode we need to add 8
+ extra bytes as well. */
+ total = (length + 8) + 32 - ((length+8) % 32);
+
+ d = xtrymalloc_secure (sizeof *d + total - 1);
+ if (!d)
+ return gpg_error_from_syserror ();
+ memcpy (d->data, string, length);
+
+ d_enc = xtrymalloc (sizeof *d_enc + total - 1);
+ if (!d_enc)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (d);
+ return err;
+ }
+
+ d_enc->totallen = total;
+ err = gcry_cipher_encrypt (encryption_handle, d_enc->data, total,
+ d->data, total - 8);
+ xfree (d);
+ if (err)
+ {
+ xfree (d_enc);
+ return err;
+ }
+ *r_data = d_enc;
+ return 0;
+}
+
+
+
+/* Check whether there are items to expire. */
+static void
+housekeeping (void)
+{
+ ITEM r, rprev;
+ time_t current = gnupg_get_time ();
+
+ /* First expire the actual data */
+ for (r=thecache; r; r = r->next)
+ {
+ if (r->pw && r->ttl >= 0 && r->accessed + r->ttl < current)
+ {
+ if (DBG_CACHE)
+ log_debug (" expired '%s'.%d (%ds after last access)\n",
+ r->key, r->restricted, r->ttl);
+ release_data (r->pw);
+ r->pw = NULL;
+ r->accessed = current;
+ }
+ }
+
+ /* Second, make sure that we also remove them based on the created stamp so
+ that the user has to enter it from time to time. */
+ for (r=thecache; r; r = r->next)
+ {
+ unsigned long maxttl;
+
+ switch (r->cache_mode)
+ {
+ case CACHE_MODE_SSH: maxttl = opt.max_cache_ttl_ssh; break;
+ default: maxttl = opt.max_cache_ttl; break;
+ }
+ if (r->pw && r->created + maxttl < current)
+ {
+ if (DBG_CACHE)
+ log_debug (" expired '%s'.%d (%lus after creation)\n",
+ r->key, r->restricted, opt.max_cache_ttl);
+ release_data (r->pw);
+ r->pw = NULL;
+ r->accessed = current;
+ }
+ }
+
+ /* Third, make sure that we don't have too many items in the list.
+ * Expire old and unused entries after 30 minutes. */
+ for (rprev=NULL, r=thecache; r; )
+ {
+ if (!r->pw && r->ttl >= 0 && r->accessed + 60*30 < current)
+ {
+ ITEM r2 = r->next;
+ if (DBG_CACHE)
+ log_debug (" removed '%s'.%d (mode %d) (slot not used for 30m)\n",
+ r->key, r->restricted, r->cache_mode);
+ xfree (r);
+ if (!rprev)
+ thecache = r2;
+ else
+ rprev->next = r2;
+ r = r2;
+ }
+ else
+ {
+ rprev = r;
+ r = r->next;
+ }
+ }
+}
+
+
+void
+agent_cache_housekeeping (void)
+{
+ int res;
+
+ if (DBG_CACHE)
+ log_debug ("agent_cache_housekeeping\n");
+
+ res = npth_mutex_lock (&cache_lock);
+ if (res)
+ log_fatal ("failed to acquire cache mutex: %s\n", strerror (res));
+
+ housekeeping ();
+
+ res = npth_mutex_unlock (&cache_lock);
+ if (res)
+ log_fatal ("failed to release cache mutex: %s\n", strerror (res));
+}
+
+
+void
+agent_flush_cache (void)
+{
+ ITEM r;
+ int res;
+
+ if (DBG_CACHE)
+ log_debug ("agent_flush_cache\n");
+
+ res = npth_mutex_lock (&cache_lock);
+ if (res)
+ log_fatal ("failed to acquire cache mutex: %s\n", strerror (res));
+
+ for (r=thecache; r; r = r->next)
+ {
+ if (r->pw)
+ {
+ if (DBG_CACHE)
+ log_debug (" flushing '%s'.%d\n", r->key, r->restricted);
+ release_data (r->pw);
+ r->pw = NULL;
+ r->accessed = 0;
+ }
+ }
+
+ res = npth_mutex_unlock (&cache_lock);
+ if (res)
+ log_fatal ("failed to release cache mutex: %s\n", strerror (res));
+}
+
+
+/* Compare two cache modes. */
+static int
+cache_mode_equal (cache_mode_t a, cache_mode_t b)
+{
+ /* CACHE_MODE_ANY matches any mode other than CACHE_MODE_IGNORE. */
+ return ((a == CACHE_MODE_ANY && b != CACHE_MODE_IGNORE)
+ || (b == CACHE_MODE_ANY && a != CACHE_MODE_IGNORE) || a == b);
+}
+
+
+/* Store the string DATA in the cache under KEY and mark it with a
+ maximum lifetime of TTL seconds. If there is already data under
+ this key, it will be replaced. Using a DATA of NULL deletes the
+ entry. A TTL of 0 is replaced by the default TTL and a TTL of -1
+ set infinite timeout. CACHE_MODE is stored with the cache entry
+ and used to select different timeouts. */
+int
+agent_put_cache (ctrl_t ctrl, const char *key, cache_mode_t cache_mode,
+ const char *data, int ttl)
+{
+ gpg_error_t err = 0;
+ ITEM r;
+ int res;
+ int restricted = ctrl? ctrl->restricted : -1;
+
+ res = npth_mutex_lock (&cache_lock);
+ if (res)
+ log_fatal ("failed to acquire cache mutex: %s\n", strerror (res));
+
+ if (DBG_CACHE)
+ log_debug ("agent_put_cache '%s'.%d (mode %d) requested ttl=%d\n",
+ key, restricted, cache_mode, ttl);
+ housekeeping ();
+
+ if (!ttl)
+ {
+ switch(cache_mode)
+ {
+ case CACHE_MODE_SSH: ttl = opt.def_cache_ttl_ssh; break;
+ default: ttl = opt.def_cache_ttl; break;
+ }
+ }
+ if ((!ttl && data) || cache_mode == CACHE_MODE_IGNORE)
+ goto out;
+
+ for (r=thecache; r; r = r->next)
+ {
+ if (((cache_mode != CACHE_MODE_USER
+ && cache_mode != CACHE_MODE_NONCE)
+ || cache_mode_equal (r->cache_mode, cache_mode))
+ && r->restricted == restricted
+ && !strcmp (r->key, key))
+ break;
+ }
+ if (r) /* Replace. */
+ {
+ if (r->pw)
+ {
+ release_data (r->pw);
+ r->pw = NULL;
+ }
+ if (data)
+ {
+ r->created = r->accessed = gnupg_get_time ();
+ r->ttl = ttl;
+ r->cache_mode = cache_mode;
+ err = new_data (data, &r->pw);
+ if (err)
+ log_error ("error replacing cache item: %s\n", gpg_strerror (err));
+ }
+ }
+ else if (data) /* Insert. */
+ {
+ r = xtrycalloc (1, sizeof *r + strlen (key));
+ if (!r)
+ err = gpg_error_from_syserror ();
+ else
+ {
+ strcpy (r->key, key);
+ r->restricted = restricted;
+ r->created = r->accessed = gnupg_get_time ();
+ r->ttl = ttl;
+ r->cache_mode = cache_mode;
+ err = new_data (data, &r->pw);
+ if (err)
+ xfree (r);
+ else
+ {
+ r->next = thecache;
+ thecache = r;
+ }
+ }
+ if (err)
+ log_error ("error inserting cache item: %s\n", gpg_strerror (err));
+ }
+
+ out:
+ res = npth_mutex_unlock (&cache_lock);
+ if (res)
+ log_fatal ("failed to release cache mutex: %s\n", strerror (res));
+
+ return err;
+}
+
+
+/* Try to find an item in the cache. Note that we currently don't
+ make use of CACHE_MODE except for CACHE_MODE_NONCE and
+ CACHE_MODE_USER. */
+char *
+agent_get_cache (ctrl_t ctrl, const char *key, cache_mode_t cache_mode)
+{
+ gpg_error_t err;
+ ITEM r;
+ char *value = NULL;
+ int res;
+ int last_stored = 0;
+ int restricted = ctrl? ctrl->restricted : -1;
+
+ if (cache_mode == CACHE_MODE_IGNORE)
+ return NULL;
+
+ res = npth_mutex_lock (&cache_lock);
+ if (res)
+ log_fatal ("failed to acquire cache mutex: %s\n", strerror (res));
+
+ if (!key)
+ {
+ key = last_stored_cache_key;
+ if (!key)
+ goto out;
+ last_stored = 1;
+ }
+
+ if (DBG_CACHE)
+ log_debug ("agent_get_cache '%s'.%d (mode %d)%s ...\n",
+ key, ctrl->restricted, cache_mode,
+ last_stored? " (stored cache key)":"");
+ housekeeping ();
+
+ for (r=thecache; r; r = r->next)
+ {
+ if (r->pw
+ && ((cache_mode != CACHE_MODE_USER
+ && cache_mode != CACHE_MODE_NONCE)
+ || cache_mode_equal (r->cache_mode, cache_mode))
+ && r->restricted == restricted
+ && !strcmp (r->key, key))
+ {
+ /* Note: To avoid races KEY may not be accessed anymore below. */
+ r->accessed = gnupg_get_time ();
+ if (DBG_CACHE)
+ log_debug ("... hit\n");
+ if (r->pw->totallen < 32)
+ err = gpg_error (GPG_ERR_INV_LENGTH);
+ else if ((err = init_encryption ()))
+ ;
+ else if (!(value = xtrymalloc_secure (r->pw->totallen - 8)))
+ err = gpg_error_from_syserror ();
+ else
+ {
+ err = gcry_cipher_decrypt (encryption_handle,
+ value, r->pw->totallen - 8,
+ r->pw->data, r->pw->totallen);
+ }
+ if (err)
+ {
+ xfree (value);
+ value = NULL;
+ log_error ("retrieving cache entry '%s'.%d failed: %s\n",
+ key, restricted, gpg_strerror (err));
+ }
+ break;
+ }
+ }
+ if (DBG_CACHE && value == NULL)
+ log_debug ("... miss\n");
+
+ out:
+ res = npth_mutex_unlock (&cache_lock);
+ if (res)
+ log_fatal ("failed to release cache mutex: %s\n", strerror (res));
+
+ return value;
+}
+
+
+/* Store the key for the last successful cache hit. That value is
+ used by agent_get_cache if the requested KEY is given as NULL.
+ NULL may be used to remove that key. */
+void
+agent_store_cache_hit (const char *key)
+{
+ char *new;
+ char *old;
+
+ /* To make sure the update is atomic under the non-preemptive thread
+ * model, we must make sure not to surrender control to a different
+ * thread. Therefore, we avoid calling the allocator during the
+ * update.
+ *
+ * Background: xtrystrdup uses gcry_strdup which may use the secure
+ * memory allocator of Libgcrypt. That allocator takes locks and
+ * since version 1.14 libgpg-error is nPth aware and thus taking a
+ * lock may now lead to thread switch. Note that this only happens
+ * when secure memory is _allocated_ (the standard allocator uses
+ * malloc which is not nPth aware) but not when calling _xfree_
+ * because gcry_free needs to check whether the pointer is in secure
+ * memory and thus needs to take a lock.
+ */
+ new = key ? xtrystrdup (key) : NULL;
+
+ /* Atomic update. */
+ old = last_stored_cache_key;
+ last_stored_cache_key = new;
+ /* Done. */
+
+ xfree (old);
+}