2022-10-16 22:58:38 +02:00
|
|
|
"""Data structures and utilities to make keyring access easier.
|
2022-10-15 13:47:45 +02:00
|
|
|
|
|
|
|
IMPORTANT: This module has to be loaded _after_ initialisation of the logging
|
|
|
|
module.
|
|
|
|
"""
|
2022-09-30 22:40:42 +02:00
|
|
|
|
|
|
|
import lacre.text as text
|
2022-10-05 22:11:26 +02:00
|
|
|
import logging
|
2022-10-14 22:42:30 +02:00
|
|
|
from os import stat
|
2022-10-15 19:53:55 +02:00
|
|
|
from watchdog.events import FileSystemEventHandler
|
2022-10-17 23:25:51 +02:00
|
|
|
from asyncio import Semaphore, run
|
2022-10-17 20:13:37 +02:00
|
|
|
import copy
|
2022-09-30 22:40:42 +02:00
|
|
|
|
|
|
|
import GnuPG
|
|
|
|
|
2022-10-05 22:11:26 +02:00
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
2022-09-30 22:40:42 +02:00
|
|
|
|
2022-10-17 20:13:37 +02:00
|
|
|
def _sanitize(keys):
|
2022-10-22 11:19:47 +02:00
|
|
|
return {fingerprint: text.sanitize_case_sense(keys[fingerprint]) for fingerprint in keys}
|
2022-10-17 20:13:37 +02:00
|
|
|
|
|
|
|
|
2022-10-11 21:50:51 +02:00
|
|
|
class KeyCacheMisconfiguration(Exception):
|
|
|
|
"""Exception used to signal that KeyCache is misconfigured."""
|
|
|
|
|
|
|
|
|
2022-09-30 22:40:42 +02:00
|
|
|
class KeyCache:
|
|
|
|
"""A store for OpenPGP keys.
|
|
|
|
|
|
|
|
Key case is sanitised while loading from GnuPG if so
|
|
|
|
configured. See mail_case_insensitive parameter in section
|
|
|
|
[default].
|
|
|
|
"""
|
|
|
|
|
2022-10-17 20:13:37 +02:00
|
|
|
def __init__(self, keys: dict = None):
|
2022-10-05 22:11:26 +02:00
|
|
|
"""Initialise an empty cache.
|
|
|
|
|
|
|
|
With keyring_dir given, set location of the directory from which keys should be loaded.
|
|
|
|
"""
|
2022-10-17 20:13:37 +02:00
|
|
|
self._keys = keys
|
2022-09-30 22:40:42 +02:00
|
|
|
|
2022-10-05 22:11:26 +02:00
|
|
|
def __getitem__(self, fingerpring):
|
|
|
|
"""Look up email assigned to the given fingerprint."""
|
|
|
|
return self._keys[fingerpring]
|
2022-09-30 22:40:42 +02:00
|
|
|
|
2022-10-05 22:11:26 +02:00
|
|
|
def __setitem__(self, fingerprint, email):
|
|
|
|
"""Assign an email to a fingerpring, overwriting it if it was already present."""
|
|
|
|
self._keys[fingerprint] = email
|
2022-09-30 22:40:42 +02:00
|
|
|
|
|
|
|
def __contains__(self, fingerprint):
|
|
|
|
"""Check if the given fingerprint is assigned to an email."""
|
|
|
|
# This method has to be present for KeyCache to be a dict substitute.
|
|
|
|
# See mailgate, function _identify_gpg_recipients.
|
|
|
|
return fingerprint in self._keys
|
|
|
|
|
|
|
|
def has_email(self, email):
|
2022-10-05 22:11:26 +02:00
|
|
|
"""Check if cache contains a key assigned to the given email."""
|
2022-09-30 22:40:42 +02:00
|
|
|
return email in self._keys.values()
|
|
|
|
|
2022-10-20 22:27:34 +02:00
|
|
|
def __repr__(self):
|
|
|
|
"""Return text representation of this object."""
|
|
|
|
details = ' '.join(self._keys.keys())
|
|
|
|
return f'<KeyCache {details}>'
|
|
|
|
|
2022-10-17 20:13:37 +02:00
|
|
|
|
|
|
|
class KeyRing:
|
|
|
|
"""A high-level adapter for GnuPG-maintained keyring directory.
|
|
|
|
|
|
|
|
Its role is to keep a cache of keys present in the keyring,
|
|
|
|
reload it when necessary and produce static copies of
|
|
|
|
fingerprint=>email maps.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, path: str):
|
|
|
|
"""Initialise the adapter."""
|
|
|
|
self._path = path
|
|
|
|
self._keys = self._load_and_sanitize()
|
|
|
|
self._sema = Semaphore()
|
|
|
|
self._last_mod = None
|
|
|
|
|
|
|
|
def _load_and_sanitize(self):
|
|
|
|
keys = self._load_keyring_from(self._path)
|
|
|
|
return _sanitize(keys)
|
|
|
|
|
|
|
|
def _load_keyring_from(self, keyring_dir):
|
|
|
|
return GnuPG.public_keys(keyring_dir)
|
|
|
|
|
|
|
|
async def freeze_identities(self) -> KeyCache:
|
|
|
|
"""Return a static, async-safe copy of the identity map."""
|
|
|
|
async with self._sema:
|
|
|
|
keys = copy.deepcopy(self._keys)
|
|
|
|
return KeyCache(keys)
|
|
|
|
|
2022-10-05 22:11:26 +02:00
|
|
|
def load(self):
|
|
|
|
"""Load keyring, replacing any previous contents of the cache."""
|
2022-10-11 21:50:51 +02:00
|
|
|
LOG.debug('Reloading keys...')
|
2022-10-17 23:25:51 +02:00
|
|
|
run(self._load())
|
2022-10-14 22:42:30 +02:00
|
|
|
|
2022-10-17 23:25:51 +02:00
|
|
|
async def _load(self):
|
2022-10-14 22:42:30 +02:00
|
|
|
last_mod = self._read_mod_time()
|
|
|
|
if self._is_modified(last_mod):
|
2022-10-17 20:13:37 +02:00
|
|
|
async with self._sema:
|
|
|
|
self.replace_keyring(self._load_keyring_from(self._path))
|
2022-10-14 22:42:30 +02:00
|
|
|
|
|
|
|
self._last_mod = self._read_mod_time()
|
2022-10-05 22:11:26 +02:00
|
|
|
|
|
|
|
reload = load
|
|
|
|
|
2022-09-30 22:40:42 +02:00
|
|
|
def replace_keyring(self, keys: dict):
|
|
|
|
"""Overwrite previously stored key cache with KEYS."""
|
2022-10-17 20:13:37 +02:00
|
|
|
keys = _sanitize(keys)
|
2022-09-30 22:40:42 +02:00
|
|
|
|
2022-10-11 21:50:51 +02:00
|
|
|
LOG.info(f'Storing {len(keys)} keys')
|
2022-09-30 22:40:42 +02:00
|
|
|
self._keys = keys
|
|
|
|
|
2022-10-17 20:13:37 +02:00
|
|
|
def _read_mod_time(self):
|
|
|
|
# (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime)
|
2022-10-22 11:19:47 +02:00
|
|
|
# 0 1 2 3 4 5 6 7 8 9
|
2022-10-17 20:13:37 +02:00
|
|
|
MTIME = 8
|
|
|
|
st = stat(self._path)
|
|
|
|
return st[MTIME]
|
2022-10-14 22:42:30 +02:00
|
|
|
|
|
|
|
def _is_modified(self, last_mod):
|
|
|
|
if self._last_mod is None:
|
|
|
|
LOG.debug('Keyring not loaded before')
|
|
|
|
return True
|
|
|
|
elif self._last_mod != last_mod:
|
|
|
|
LOG.debug('Keyring directory mtime changed')
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
LOG.debug('Keyring not modified ')
|
|
|
|
return False
|
|
|
|
|
2022-10-15 19:53:55 +02:00
|
|
|
|
|
|
|
class KeyringModificationListener(FileSystemEventHandler):
|
|
|
|
"""A filesystem event listener that triggers key cache reload."""
|
|
|
|
|
2022-10-17 20:13:37 +02:00
|
|
|
def __init__(self, keyring: KeyRing):
|
2022-10-15 19:53:55 +02:00
|
|
|
"""Initialise a listener with a callback to be executed upon each change."""
|
2022-10-17 20:13:37 +02:00
|
|
|
self._keyring = keyring
|
2022-10-15 19:53:55 +02:00
|
|
|
|
|
|
|
def handle(self, event):
|
|
|
|
"""Reload keys upon FS event."""
|
|
|
|
LOG.debug(f'Reloading on event {event!r}')
|
2022-10-17 20:13:37 +02:00
|
|
|
self._keyring.reload()
|
2022-10-15 19:53:55 +02:00
|
|
|
|
|
|
|
# All methods should do the same: reload the key cache.
|
|
|
|
# on_created = handle
|
|
|
|
# on_deleted = handle
|
|
|
|
on_modified = handle
|