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

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

2# 

3# SPDX-License-Identifier: Zlib 

4 

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

6 

7from __future__ import annotations 

8 

9import importlib 

10import os 

11import pathlib 

12from typing import TYPE_CHECKING, Protocol 

13 

14if TYPE_CHECKING: 

15 from collections.abc import Callable 

16 from typing import Any 

17 

18 from typing_extensions import Buffer 

19 

20__all__ = () 

21 

22 

23INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT = ( 

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

25) 

26 

27 

28class NotAVaultConfigError(ValueError): 

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

30 

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

38 

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

48 

49 

50def get_vault_key() -> bytes: 

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

78 

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

92 

93 

94def get_vault_path() -> pathlib.Path: 

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( 1efoghibjuv

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

113 ).expanduser() 

114 

115 

116class ExportVaultConfigDataFunction(Protocol): 

117 """Typing protocol for vault config data export handlers.""" 

118 

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

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[ 

180 str, 

181 ExportVaultConfigDataFunction, 

182] = {} 

183 

184 

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

194 

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

204 

205 return wrapper 1akm

206 

207 

208def find_vault_config_data_handlers() -> None: 

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

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

224 

225 

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

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() 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