# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info> # # SPDX-License-Identifier: Zlib """Foreign configuration exporter for derivepassphrase.""" from __future__ import annotations import importlib import os from typing import TYPE_CHECKING, Protocol import derivepassphrase as dpp if TYPE_CHECKING: from collections.abc import Callable from typing import Any from typing_extensions import Buffer __author__ = dpp.__author__ __version__ = dpp.__version__ __all__ = () INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT = ( 'Invalid vault native configuration format: {fmt!r}' ) class NotAVaultConfigError(ValueError): """The `path` does not hold a `format`-type vault configuration.""" def __init__( self, path: str | bytes, format: str | None = None, # noqa: A002 ) -> None: self.path = path self.format = format def __str__(self) -> str: # pragma: no cover formatted_format = ( f'vault {self.format} configuration' if self.format else 'vault configuration' ) return f'Not a {formatted_format}: {self.path!r}' def get_vault_key() -> bytes: """Automatically determine the vault(1) master key/password. Query the `VAULT_KEY`, `LOGNAME`, `USER` and `USERNAME` environment variables, in that order. This is the same algorithm that vault uses. Returns: The master key/password. This is generally used as input to a key-derivation function to determine the *actual* encryption and signing keys for the vault configuration. Raises: KeyError: We cannot find any of the named environment variables. Please set `VAULT_KEY` manually to the desired value. """ username = ( os.environb.get(b'VAULT_KEY') or os.environb.get(b'LOGNAME') or os.environb.get(b'USER') or os.environb.get(b'USERNAME') ) if not username: env_var = 'VAULT_KEY' raise KeyError(env_var) return username def get_vault_path() -> str | bytes | os.PathLike: """Automatically determine the vault(1) configuration path. Query the `VAULT_PATH` environment variable, or default to `~/.vault`. This is the same algorithm that vault uses. If not absolute, then `VAULT_PATH` is relative to the home directory. Returns: The vault configuration path. Depending on the vault version, this may be a file or a directory. Raises: RuntimeError: We cannot determine the home directory. Please set `HOME` manually to the correct value. """ result = os.path.join( os.path.expanduser('~'), os.environ.get('VAULT_PATH', '.vault') ) if result.startswith('~'): msg = 'Cannot determine home directory' raise RuntimeError(msg) return result class ExportVaultConfigDataFunction(Protocol): # pragma: no cover def __call__( self, path: str | bytes | os.PathLike | None = None, key: str | Buffer | None = None, *, format: str, # noqa: A002 ) -> Any: ... # noqa: ANN401 _export_vault_config_data_registry: dict[ str, ExportVaultConfigDataFunction, ] = {} def register_export_vault_config_data_handler( *names: str, ) -> Callable[[ExportVaultConfigDataFunction], ExportVaultConfigDataFunction]: if not names: msg = 'No names given to export_data handler registry' raise ValueError(msg) if '' in names: msg = 'Cannot register export_data handler under an empty name' raise ValueError(msg) def wrapper( f: ExportVaultConfigDataFunction, ) -> ExportVaultConfigDataFunction: for name in names: if name in _export_vault_config_data_registry: msg = f'export_data handler already registered: {name!r}' raise ValueError(msg) _export_vault_config_data_registry[name] = f return f return wrapper def find_vault_config_data_handlers() -> None: """Find all export handlers for vault config data. (This function is idempotent.) Raises: ModuleNotFoundError: A required module was not found. """ # Defer imports (and handler registrations) to avoid circular # imports. The modules themselves contain function definitions that # register themselves automatically with # `_export_vault_config_data_registry`. importlib.import_module('derivepassphrase.exporter.storeroom') importlib.import_module('derivepassphrase.exporter.vault_native') def export_vault_config_data( path: str | bytes | os.PathLike | None = None, key: str | Buffer | None = None, *, format: str, # noqa: A002 ) -> Any: # noqa: ANN401 """Export the full vault-native configuration stored in `path`. Args: path: The path to the vault configuration file or directory. If not given, then query [`get_vault_path`][] for the correct value. key: Encryption key/password for the configuration file or directory, usually the username, or passed via the `VAULT_KEY` environment variable. If not given, then query [`exporter.get_vault_key`][] for the value. format: The format to attempt parsing as. Must be `v0.2`, `v0.3` or `storeroom`. Returns: The vault configuration, as recorded in the configuration file. This may or may not be a valid configuration according to `vault` or `derivepassphrase`. Raises: IsADirectoryError: The requested format requires a configuration file, but `path` points to a directory instead. NotADirectoryError: The requested format requires a configuration directory, but `path` points to something else instead. OSError: There was an OS error while accessing the configuration file/directory. RuntimeError: Something went wrong during data collection, e.g. we encountered unsupported or corrupted data in the configuration file/directory. json.JSONDecodeError: An internal JSON data structure failed to parse from disk. The configuration file/directory is probably corrupted. exporter.NotAVaultConfigError: The file/directory contents are not in the claimed configuration format. ValueError: The requested format is invalid. ModuleNotFoundError: The requested format requires support code, which failed to load because of missing Python libraries. """ find_vault_config_data_handlers() handler = _export_vault_config_data_registry.get(format) if handler is None: msg = INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format(fmt=format) raise ValueError(msg) return handler(path, key, format=format)