Coverage for src\derivepassphrase\exporter\__init__.py: 100.000%
46 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
5"""Foreign configuration exporter for derivepassphrase."""
7from __future__ import annotations
9import importlib
10import os
11import pathlib
12from typing import TYPE_CHECKING, Protocol
14if TYPE_CHECKING:
15 from collections.abc import Callable
16 from typing import Any
18 from typing_extensions import Buffer
20__all__ = ()
23INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT = (
24 'Invalid vault native configuration format: {fmt!r}'
25)
28class NotAVaultConfigError(ValueError):
29 """The `path` does not hold a `format`-type vault configuration."""
31 def __init__(
32 self,
33 path: str | bytes | os.PathLike,
34 format: str | None = None, # noqa: A002
35 ) -> None:
36 self.path = os.fspath(path) 1cdb
37 self.format = format 1cdb
39 def __str__(self) -> str: # pragma: no cover
40 """""" # noqa: D419
41 # Defensive programming, so no coverage.
42 formatted_format = (
43 f'vault {self.format} configuration'
44 if self.format
45 else 'vault configuration'
46 )
47 return f'Not a {formatted_format}: {self.path!r}'
50def get_vault_key() -> bytes:
51 """Automatically determine the vault(1) master key/password.
53 Query the `VAULT_KEY`, `LOGNAME`, `USER` and `USERNAME` environment
54 variables, in that order. This is the same algorithm that vault
55 uses.
57 Returns:
58 The master key/password. This is generally used as input to
59 a key-derivation function to determine the *actual* encryption
60 and signing keys for the vault configuration.
62 Raises:
63 KeyError:
64 We cannot find any of the named environment variables.
65 Please set `VAULT_KEY` manually to the desired value.
67 """
69 def getenv_environb(env_var: str) -> bytes: # pragma: no cover 1efcdghibjln
70 # We cannot statically predict which implementation will be
71 # chosen, so exclude both from coverage.
72 return os.environb.get(env_var.encode('UTF-8'), b'') # type: ignore[attr-defined]
74 def getenv_environ(env_var: str) -> bytes: # pragma: no cover 1efcdghibjln
75 # We cannot statically predict which implementation will be
76 # chosen, so exclude both from coverage.
77 return os.environ.get(env_var, '').encode('UTF-8') 1efcdghibjln
79 getenv: Callable[[str], bytes] = ( 1efcdghibjln
80 getenv_environb if os.supports_bytes_environ else getenv_environ
81 )
82 username = ( 1efcdghibjln
83 getenv('VAULT_KEY')
84 or getenv('LOGNAME')
85 or getenv('USER')
86 or getenv('USERNAME')
87 )
88 if not username: 1efcdghibjln
89 env_var = 'VAULT_KEY' 1l
90 raise KeyError(env_var) 1l
91 return username 1efcdghibjn
94def get_vault_path() -> pathlib.Path:
95 """Automatically determine the vault(1) configuration path.
97 Query the `VAULT_PATH` environment variable, or default to
98 `~/.vault`. This is the same algorithm that vault uses. If not
99 absolute, then `VAULT_PATH` is relative to the home directory.
101 Returns:
102 The vault configuration path. Depending on the vault version,
103 this may be a file or a directory.
105 Raises:
106 RuntimeError:
107 We cannot determine the home directory. Please set `HOME`
108 manually to the correct value.
110 """
111 return pathlib.Path( 1efoghibjuv
112 '~', os.environ.get('VAULT_PATH', '.vault')
113 ).expanduser()
116class ExportVaultConfigDataFunction(Protocol):
117 """Typing protocol for vault config data export handlers."""
119 def __call__(
120 self,
121 path: str | bytes | os.PathLike | None = None,
122 key: str | Buffer | None = None,
123 *,
124 format: str, # noqa: A002
125 ) -> Any: # noqa: ANN401
126 """Export the full vault-native configuration stored in `path`.
128 Args:
129 path:
130 The path to the vault configuration file or directory.
131 If not given, then query [`get_vault_path`][] for the
132 correct value.
133 key:
134 Encryption key/password for the configuration file or
135 directory, usually the username, or passed via the
136 `VAULT_KEY` environment variable. If not given, then
137 query [`get_vault_key`][] for the value.
138 format:
139 The format to attempt parsing as. Must be `v0.2`,
140 `v0.3` or `storeroom`.
142 Returns:
143 The vault configuration, as recorded in the configuration
144 file.
146 This may or may not be a valid configuration according to
147 `vault` or `derivepassphrase`.
149 Raises:
150 IsADirectoryError:
151 The requested format requires a configuration file, but
152 `path` points to a directory instead.
153 NotADirectoryError:
154 The requested format requires a configuration directory,
155 but `path` points to something else instead.
156 OSError:
157 There was an OS error while accessing the configuration
158 file/directory.
159 RuntimeError:
160 Something went wrong during data collection, e.g. we
161 encountered unsupported or corrupted data in the
162 configuration file/directory.
163 json.JSONDecodeError:
164 An internal JSON data structure failed to parse from
165 disk. The configuration file/directory is probably
166 corrupted.
167 exporter.NotAVaultConfigError:
168 The file/directory contents are not in the claimed
169 configuration format.
170 ValueError:
171 The requested format is invalid.
172 ModuleNotFoundError:
173 The requested format requires support code, which failed
174 to load because of missing Python libraries.
176 """
179_export_vault_config_data_registry: dict[
180 str,
181 ExportVaultConfigDataFunction,
182] = {}
185def register_export_vault_config_data_handler(
186 *names: str,
187) -> Callable[[ExportVaultConfigDataFunction], ExportVaultConfigDataFunction]:
188 if not names: 1akm
189 msg = 'No names given to export_data handler registry' 1k
190 raise ValueError(msg) 1k
191 if '' in names: 1akm
192 msg = 'Cannot register export_data handler under an empty name' 1k
193 raise ValueError(msg) 1k
195 def wrapper( 1akm
196 f: ExportVaultConfigDataFunction,
197 ) -> ExportVaultConfigDataFunction:
198 for name in names: 1akm
199 if name in _export_vault_config_data_registry: 1akm
200 msg = f'export_data handler already registered: {name!r}' 1k
201 raise ValueError(msg) 1k
202 _export_vault_config_data_registry[name] = f 1am
203 return f 1am
205 return wrapper 1akm
208def find_vault_config_data_handlers() -> None:
209 """Find all export handlers for vault config data.
211 (This function is idempotent.)
213 Raises:
214 ModuleNotFoundError:
215 A required module was not found.
217 """
218 # Defer imports (and handler registrations) to avoid circular
219 # imports. The modules themselves contain function definitions that
220 # register themselves automatically with
221 # `_export_vault_config_data_registry`.
222 importlib.import_module('derivepassphrase.exporter.storeroom') 1efpoqcrdghibjs
223 importlib.import_module('derivepassphrase.exporter.vault_native') 1efpoqcrdghibjs
226def export_vault_config_data(
227 path: str | bytes | os.PathLike | None = None,
228 key: str | Buffer | None = None,
229 *,
230 format: str, # noqa: A002
231) -> Any: # noqa: ANN401
232 """Export the full vault-native configuration stored in `path`.
234 See [`ExportVaultConfigDataFunction`][] for an explanation of the
235 call signature, and the exceptions to expect.
237 """ # noqa: DOC201,DOC501
238 find_vault_config_data_handlers() 1efpoqcrdghibjts
239 handler = _export_vault_config_data_registry.get(format) 1efpoqcrdghibjts
240 if handler is None: 1efpoqcrdghibjts
241 msg = INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format(fmt=format) 1t
242 raise ValueError(msg) 1t
243 return handler(path, key, format=format) 1efpoqcrdghibjs