Coverage for src / derivepassphrase / exporter / __init__.py: 100.000%
46 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 21:34 +0200
1# SPDX-FileCopyrightText: 2026 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) 1cdeb
37 self.format = format 1cdeb
39 def __str__(self) -> str: # pragma: no cover [failsafe]
40 """""" # noqa: D419
41 formatted_format = (
42 f"vault {self.format} configuration"
43 if self.format
44 else "vault configuration"
45 )
46 return f"Not a {formatted_format}: {self.path!r}"
49def get_vault_key() -> bytes:
50 """Automatically determine the vault(1) master key/password.
52 Query the `VAULT_KEY`, `LOGNAME`, `USER` and `USERNAME` environment
53 variables, in that order. This is the same algorithm that vault
54 uses.
56 Returns:
57 The master key/password. This is generally used as input to
58 a key-derivation function to determine the *actual* encryption
59 and signing keys for the vault configuration.
61 Raises:
62 KeyError:
63 We cannot find any of the named environment variables.
64 Please set `VAULT_KEY` manually to the desired value.
66 """
68 def getenv_environb(env_var: str) -> bytes: # pragma: no cover [external] 1fcdeghijkbmn
69 return os.environb.get(env_var.encode("UTF-8"), b"") # type: ignore[attr-defined] 1fcdeghijkbmn
71 def getenv_environ(env_var: str) -> bytes: # pragma: no cover [external] 1fcdeghijkbmn
72 return os.environ.get(env_var, "").encode("UTF-8") 1fcdghijkbmn
74 getenv: Callable[[str], bytes] = ( 1fcdeghijkbmn
75 getenv_environb if os.supports_bytes_environ else getenv_environ
76 )
77 username = ( 1fcdeghijkbmn
78 getenv("VAULT_KEY")
79 or getenv("LOGNAME")
80 or getenv("USER")
81 or getenv("USERNAME")
82 )
83 if not username: 1fcdeghijkbmn
84 env_var = "VAULT_KEY" 1m
85 raise KeyError(env_var) 1m
86 return username 1fcdeghijkbn
89def get_vault_path() -> pathlib.Path:
90 """Automatically determine the vault(1) configuration path.
92 Query the `VAULT_PATH` environment variable, or default to
93 `~/.vault`. This is the same algorithm that vault uses. If not
94 absolute, then `VAULT_PATH` is relative to the home directory.
96 Returns:
97 The vault configuration path. Depending on the vault version,
98 this may be a file or a directory.
100 Raises:
101 RuntimeError:
102 We cannot determine the home directory. Please set `HOME`
103 manually to the correct value.
105 """
106 return pathlib.Path( 1fpghijkbuv
107 "~", os.environ.get("VAULT_PATH", ".vault")
108 ).expanduser()
111class ExportVaultConfigDataFunction(Protocol):
112 """Typing protocol for vault config data export handlers."""
114 def __call__(
115 self,
116 path: str | bytes | os.PathLike | None = None,
117 key: str | Buffer | None = None,
118 *,
119 format: str, # noqa: A002
120 ) -> Any: # noqa: ANN401
121 """Export the full vault-native configuration stored in `path`.
123 Args:
124 path:
125 The path to the vault configuration file or directory.
126 If not given, then query [`get_vault_path`][] for the
127 correct value.
128 key:
129 Encryption key/password for the configuration file or
130 directory, usually the username, or passed via the
131 `VAULT_KEY` environment variable. If not given, then
132 query [`get_vault_key`][] for the value.
133 format:
134 The format to attempt parsing as. Must be `v0.2`,
135 `v0.3` or `storeroom`.
137 Returns:
138 The vault configuration, as recorded in the configuration
139 file.
141 This may or may not be a valid configuration according to
142 `vault` or `derivepassphrase`.
144 Raises:
145 IsADirectoryError:
146 The requested format requires a configuration file, but
147 `path` points to a directory instead.
148 NotADirectoryError:
149 The requested format requires a configuration directory,
150 but `path` points to something else instead.
151 OSError:
152 There was an OS error while accessing the configuration
153 file/directory.
154 RuntimeError:
155 Something went wrong during data collection, e.g. we
156 encountered unsupported or corrupted data in the
157 configuration file/directory.
158 json.JSONDecodeError:
159 An internal JSON data structure failed to parse from
160 disk. The configuration file/directory is probably
161 corrupted.
162 exporter.NotAVaultConfigError:
163 The file/directory contents are not in the claimed
164 configuration format.
165 ValueError:
166 The requested format is invalid.
167 ModuleNotFoundError:
168 The requested format requires support code, which failed
169 to load because of missing Python libraries.
171 """
174_export_vault_config_data_registry: dict[
175 str,
176 ExportVaultConfigDataFunction,
177] = {}
180def register_export_vault_config_data_handler(
181 *names: str,
182) -> Callable[[ExportVaultConfigDataFunction], ExportVaultConfigDataFunction]:
183 if not names: 1alo
184 msg = "No names given to export_data handler registry" 1l
185 raise ValueError(msg) 1l
186 if "" in names: 1alo
187 msg = "Cannot register export_data handler under an empty name" 1l
188 raise ValueError(msg) 1l
190 def wrapper( 1alo
191 f: ExportVaultConfigDataFunction,
192 ) -> ExportVaultConfigDataFunction:
193 for name in names: 1alo
194 if name in _export_vault_config_data_registry: 1alo
195 msg = f"export_data handler already registered: {name!r}" 1l
196 raise ValueError(msg) 1l
197 _export_vault_config_data_registry[name] = f 1ao
198 return f 1ao
200 return wrapper 1alo
203def find_vault_config_data_handlers() -> None:
204 """Find all export handlers for vault config data.
206 (This function is idempotent.)
208 Raises:
209 ModuleNotFoundError:
210 A required module was not found.
212 """
213 # Defer imports (and handler registrations) to avoid circular
214 # imports. The modules themselves contain function definitions that
215 # register themselves automatically with
216 # `_export_vault_config_data_registry`.
217 importlib.import_module("derivepassphrase.exporter.storeroom") 1fcqderpghijkbs
218 importlib.import_module("derivepassphrase.exporter.vault_native") 1fcqderpghijkbs
221def export_vault_config_data(
222 path: str | bytes | os.PathLike | None = None,
223 key: str | Buffer | None = None,
224 *,
225 format: str, # noqa: A002
226) -> Any: # noqa: ANN401
227 """Export the full vault-native configuration stored in `path`.
229 See [`ExportVaultConfigDataFunction`][] for an explanation of the
230 call signature, and the exceptions to expect.
232 """ # noqa: DOC201,DOC501
233 find_vault_config_data_handlers() 1fcqderpghijkbts
234 handler = _export_vault_config_data_registry.get(format) 1fcqderpghijkbts
235 if handler is None: 1fcqderpghijkbts
236 msg = INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format(fmt=format) 1t
237 raise ValueError(msg) 1t
238 return handler(path, key, format=format) 1fcqderpghijkbs