diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:59:15 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:59:15 +0000 |
commit | 8de1ee1b2b676b0d07586f0752750dd6b0fb7511 (patch) | |
tree | dd46fd7dc3863045696cd0e48032d8a36fa0daf5 /agent/cache.c | |
parent | Initial commit. (diff) | |
download | gnupg2-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.c | 530 |
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); +} |