Coverage for src\derivepassphrase\exporter\__init__.py: 100.000%

1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>

2#

3# SPDX-License-Identifier: Zlib

4

5"""Foreign configuration exporter for derivepassphrase."""

6

7 from __future__ import annotations (empty)

8

9 import importlib (empty)

10 import os (empty)

11 import pathlib (empty)

12 from typing import TYPE_CHECKING, Protocol (empty)

13

14 if TYPE_CHECKING: (empty)

15 from collections.abc import Callable

16 from typing import Any

17

18 from typing_extensions import Buffer

19

20 __all__ = () (empty)

21

22

23 INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT = ( (empty)

24 'Invalid vault native configuration format: {fmt!r}'

25)

26

27

28 class NotAVaultConfigError(ValueError): (empty)

29 """The `path` does not hold a `format`-type vault configuration."""

30

31 def __init__( (empty)

32 self,

33 path: str | bytes | os.PathLike,

34 format: str | None = None, # noqa: A002

35 ) -> None:

36 self.path = os.fspath(path) 3 ctx 1cdb

37 self.format = format 3 ctx 1cdb

38

39 def __str__(self) -> str: # pragma: no cover (empty)

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}'

48

49

50 def get_vault_key() -> bytes: (empty)

51 """Automatically determine the vault(1) master key/password.

52

53 Query the `VAULT_KEY`, `LOGNAME`, `USER` and `USERNAME` environment

54 variables, in that order. This is the same algorithm that vault

55 uses.

56

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.

61

62 Raises:

63 KeyError:

64 We cannot find any of the named environment variables.

65 Please set `VAULT_KEY` manually to the desired value.

66

67 """

68

69 def getenv_environb(env_var: str) -> bytes: # pragma: no cover 11 ctx 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]

73

74 def getenv_environ(env_var: str) -> bytes: # pragma: no cover 11 ctx 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') 11 ctx 1efcdghibjln

78

79 getenv: Callable[[str], bytes] = ( 11 ctx 1efcdghibjln

80 getenv_environb if os.supports_bytes_environ else getenv_environ

81 )

82 username = ( 11 ctx 1efcdghibjln

83 getenv('VAULT_KEY')

84 or getenv('LOGNAME')

85 or getenv('USER')

86 or getenv('USERNAME')

87 )

88 if not username: 11 ctx 1efcdghibjln

89 env_var = 'VAULT_KEY' 1 ctx 1l

90 raise KeyError(env_var) 1 ctx 1l

91 return username 10 ctx 1efcdghibjn

92

93

94 def get_vault_path() -> pathlib.Path: (empty)

95 """Automatically determine the vault(1) configuration path.

96

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.

100

101 Returns:

102 The vault configuration path. Depending on the vault version,

103 this may be a file or a directory.

104

105 Raises:

106 RuntimeError:

107 We cannot determine the home directory. Please set `HOME`

108 manually to the correct value.

109

110 """

111 return pathlib.Path( 10 ctx 1efoghibjuv

112 '~', os.environ.get('VAULT_PATH', '.vault')

113 ).expanduser()

114

115

116 class ExportVaultConfigDataFunction(Protocol): (empty)

117 """Typing protocol for vault config data export handlers.""" (empty)

118

119 def __call__( (empty)

120 self,

121 path: str | bytes | os.PathLike | None = None, (empty)

122 key: str | Buffer | None = None, (empty)

123 *,

124 format: str, # noqa: A002 (empty)

125 ) -> Any: # noqa: ANN401 (empty)

126 """Export the full vault-native configuration stored in `path`.

127

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`.

141

142 Returns:

143 The vault configuration, as recorded in the configuration

144 file.

145

146 This may or may not be a valid configuration according to

147 `vault` or `derivepassphrase`.

148

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.

175

176 """

177

178

179 _export_vault_config_data_registry: dict[ (empty)

180 str,

181 ExportVaultConfigDataFunction,

182] = {}

183

184

185 def register_export_vault_config_data_handler( (empty)

186 *names: str,

187) -> Callable[[ExportVaultConfigDataFunction], ExportVaultConfigDataFunction]:

188 if not names: 3 ctx 1akm

189 msg = 'No names given to export_data handler registry' 1 ctx 1k

190 raise ValueError(msg) 1 ctx 1k

191 if '' in names: 3 ctx 1akm

192 msg = 'Cannot register export_data handler under an empty name' 1 ctx 1k

193 raise ValueError(msg) 1 ctx 1k

194

195 def wrapper( 3 ctx 1akm

196 f: ExportVaultConfigDataFunction,

197 ) -> ExportVaultConfigDataFunction:

198 for name in names: 3 ctx 1akm

199 if name in _export_vault_config_data_registry: 3 ctx 1akm

200 msg = f'export_data handler already registered: {name! r}' 1 ctx 1k

201 raise ValueError(msg) 1 ctx 1k

202 _export_vault_config_data_registry[name] = f 2 ctx 1am

203 return f 2 ctx 1am

204

205 return wrapper 3 ctx 1akm

206

207

208 def find_vault_config_data_handlers() -> None: (empty)

209 """Find all export handlers for vault config data.

210

211 (This function is idempotent.)

212

213 Raises:

214 ModuleNotFoundError:

215 A required module was not found.

216

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') 14 ctx 1efpoqcrdghibjs

223 importlib.import_module('derivepassphrase.exporter.vault_native') 14 ctx 1efpoqcrdghibjs

224

225

226 def export_vault_config_data( (empty)

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`.

233

234 See [`ExportVaultConfigDataFunction`][] for an explanation of the

235 call signature, and the exceptions to expect.

236

237 """ # noqa: DOC201,DOC501

238 find_vault_config_data_handlers() 15 ctx 1efpoqcrdghibjts

239 handler = _export_vault_config_data_registry.get(format) 15 ctx 1efpoqcrdghibjts

240 if handler is None: 15 ctx 1efpoqcrdghibjts

241 msg = INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format(fmt=format) 1 ctx 1t

242 raise ValueError(msg) 1 ctx 1t

243 return handler(path, key, format=format) 14 ctx 1efpoqcrdghibjs