Source code for fairdm.backups.keys

"""The public key a portal's backups are encrypted to."""

import os
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager

import gnupg
from dbbackup import settings as dbbackup_settings


[docs] class BackupKey: """An armoured OpenPGP public key that backups are encrypted to. One class serves the production checks and ``run_backup``, so what the checks accept is exactly what a backup is encrypted with. Args: armored: The key as armoured text, such as the value of ``BACKUP_PUBLIC_KEY``. """ PRIVATE_ARMOUR = "PRIVATE KEY BLOCK" def __init__(self, armored: str): self.armored = armored
[docs] def home(self) -> tempfile.TemporaryDirectory[str]: """Return a new, empty keyring directory that is removed when its block ends. Returns: A context manager yielding the path of the directory, under django-dbbackup's temporary directory. """ return tempfile.TemporaryDirectory( dir=dbbackup_settings.TMP_DIR, ignore_cleanup_errors=True )
[docs] def import_into(self, home: str) -> str | None: """Import the key into the keyring at ``home``. Args: home: The keyring directory. Returns: The fingerprint of a key that can encrypt, or ``None`` when the text holds no such key, or holds secret key material. Raises: OSError: gpg is not installed. """ if self.PRIVATE_ARMOUR in self.armored: return None gpg = gnupg.GPG(gnupghome=home) gpg.import_keys(self.armored) for key in gpg.list_keys(): if "E" in key["cap"]: return str(key["fingerprint"]) return None
@property def fingerprint(self) -> str | None: """The fingerprint of a key that can encrypt, or ``None``. It is ``None`` when the text is not an armoured public key, the key cannot encrypt, or the text holds a secret key, so the portal never holds the key that decrypts its backups. Raises: OSError: gpg is not installed. """ with self.home() as home: return self.import_into(home)
[docs] @contextmanager def keyring(self) -> Iterator[None]: """Make django-dbbackup encrypt to this key for the length of the block. The key is imported into a new keyring that is removed on exit. django-dbbackup overwrites its trust setting when it is imported, and gpg refuses a recipient whose trust is unknown, so trust is set here. The keyring, recipient and trust are restored on exit, also after an exception. Yields: Nothing. Raises: ValueError: The text is not a public key that can encrypt. OSError: gpg is not installed. """ with self.home() as home: fingerprint = self.import_into(home) if fingerprint is None: msg = "BACKUP_PUBLIC_KEY is not a public key that can encrypt." raise ValueError(msg) previous = ( dbbackup_settings.GPG_RECIPIENT, dbbackup_settings.GPG_ALWAYS_TRUST, os.environ.get("GNUPGHOME"), ) dbbackup_settings.GPG_RECIPIENT = fingerprint dbbackup_settings.GPG_ALWAYS_TRUST = True os.environ["GNUPGHOME"] = home try: yield finally: dbbackup_settings.GPG_RECIPIENT, dbbackup_settings.GPG_ALWAYS_TRUST = ( previous[:2] ) if previous[2] is None: os.environ.pop("GNUPGHOME", None) else: os.environ["GNUPGHOME"] = previous[2]