Coverage for src\derivepassphrase\_types.py: 100.000%

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

2#

3# SPDX-License-Identifier: Zlib

4

5"""Types used by derivepassphrase."""

6

7 from __future__ import annotations (empty)

8

9 import enum (empty)

10 import json (empty)

11 import math (empty)

12 import string (empty)

13 import warnings (empty)

14 from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, cast (empty)

15

16 from typing_extensions import ( (empty)

17 Buffer,

18 NamedTuple,

19 NotRequired,

20 TypeAlias,

21 TypedDict,

22 deprecated,

23 get_overloads,

24 overload,

25 runtime_checkable,

26)

27

28 if TYPE_CHECKING: (empty)

29 from collections.abc import Callable, Iterator, Sequence

30 from typing import Literal

31

32 from typing_extensions import (

33 Any,

34 Required,

35 TypeIs,

36 )

37

38 __all__ = ( (empty)

39 'SSH_AGENT',

40 'SSH_AGENTC',

41 'SSHKeyCommentPair',

42 'VaultConfig',

43 'is_vault_config',

44)

45

46

47# For type checking purposes, not actual use, so no coverage.

48 class _Omitted: # pragma: no cover (empty)

49 def __bool__(self) -> bool: (empty)

50 return False

51

52 def __repr__(self) -> str: (empty)

53 return '...'

54

55

56 class VaultConfigGlobalSettings(TypedDict, total=False): (empty)

57 r"""Configuration for vault: global settings.

58

59 Attributes:

60 key:

61 The base64-encoded ssh public key to use, overriding the

62 master passphrase. Optional.

63 phrase:

64 The master passphrase. Optional.

65 unicode_normalization_form:

66 The preferred Unicode normalization form; we warn the user

67 if textual passphrases do not match their normalized forms.

68 Optional, and a `derivepassphrase` extension.

69 length:

70 Desired passphrase length.

71 repeat:

72 The maximum number of immediate character repetitions

73 allowed in the passphrase. Disabled if set to 0.

74 lower:

75 Optional constraint on ASCII lowercase characters. If

76 positive, include this many lowercase characters

77 somewhere in the passphrase. If 0, avoid lowercase

78 characters altogether.

79 upper:

80 Same as `lower`, but for ASCII uppercase characters.

81 number:

82 Same as `lower`, but for ASCII digits.

83 space:

84 Same as `lower`, but for the space character.

85 dash:

86 Same as `lower`, but for the hyphen-minus and underscore

87 characters.

88 symbol:

89 Same as `lower`, but for all other hitherto unlisted

90 ASCII printable characters (except backquote).

91

92 """

93

94 key: NotRequired[str] (empty)

95 """""" (empty)

96 phrase: NotRequired[str] (empty)

97 """""" (empty)

98 unicode_normalization_form: NotRequired[ (empty)

99 Literal['NFC', 'NFD', 'NFKC', 'NFKD']

100 ]

101 """""" (empty)

102 length: NotRequired[int] (empty)

103 """""" (empty)

104 repeat: NotRequired[int] (empty)

105 """""" (empty)

106 lower: NotRequired[int] (empty)

107 """""" (empty)

108 upper: NotRequired[int] (empty)

109 """""" (empty)

110 number: NotRequired[int] (empty)

111 """""" (empty)

112 space: NotRequired[int] (empty)

113 """""" (empty)

114 dash: NotRequired[int] (empty)

115 """""" (empty)

116 symbol: NotRequired[int] (empty)

117 """""" (empty)

118

119

120 class VaultConfigServicesSettings(VaultConfigGlobalSettings, total=False): (empty)

121 r"""Configuration for vault: services settings.

122

123 Attributes:

124 notes:

125 Optional notes for this service, to display to the user when

126 generating the passphrase.

127 key:

128 As per the global settings.

129 phrase:

130 As per the global settings.

131 unicode_normalization_form:

132 As per the global settings.

133 length:

134 As per the global settings.

135 repeat:

136 As per the global settings.

137 lower:

138 As per the global settings.

139 upper:

140 As per the global settings.

141 number:

142 As per the global settings.

143 space:

144 As per the global settings.

145 dash:

146 As per the global settings.

147 symbol:

148 As per the global settings.

149

150 """

151

152 notes: NotRequired[str] (empty)

153 """""" (empty)

154

155

156 _VaultConfig = TypedDict( (empty)

157 '_VaultConfig',

158 {'global': NotRequired[VaultConfigGlobalSettings]},

159 total=False,

160)

161

162

163 class VaultConfig(TypedDict, _VaultConfig, total=False): (empty)

164 r"""Configuration for vault. For typing purposes.

165

166 Usually stored as JSON.

167

168 Attributes:

169 global (NotRequired[VaultConfigGlobalSettings]):

170 Global settings.

171 services (Required[dict[str, VaultConfigServicesSettings]]):

172 Service-specific settings.

173

174 """

175

176 services: Required[dict[str, VaultConfigServicesSettings]] (empty)

177

178

179 def json_path(path: Sequence[str | int], /) -> str: (empty)

180 r"""Transform a series of keys and indices into a JSONPath selector.

181

182 The resulting JSONPath selector conforms to RFC 9535, is always

183 rooted at the JSON root node (i.e., starts with `$`), and only

184 contains name and index selectors (in shorthand dot notation, where

185 possible).

186

187 Args:

188 path:

189 A sequence of object keys or array indices to navigate to

190 the desired JSON value, starting from the root node.

191

192 Returns:

193 A valid JSONPath selector (a string) identifying the desired

194 JSON value.

195

196 Examples:

197 >>> json_path(['global', 'phrase'])

198 '$.global.phrase'

199 >>> json_path(['services', 'service name with spaces', 'length'])

200 '$.services["service name with spaces"].length'

201 >>> json_path(['services', 'special\u000acharacters', 'notes'])

202 '$.services["special\\ncharacters"].notes'

203 >>> print(json_path(['services', 'special\u000acharacters', 'notes']))

204 $.services["special\ncharacters"].notes

205 >>> json_path(['custom_array', 2, 0])

206 '$.custom_array[2][0]'

207

208 """

209

210 def needs_longhand(x: str | int) -> bool: 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

211 initial = ( 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

212 frozenset(string.ascii_lowercase)

213 | frozenset(string.ascii_uppercase)

214 | frozenset('_')

215 )

216 chars = initial | frozenset(string.digits) 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

217 return not ( 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

218 isinstance(x, str)

219 and x

220 and set(x).issubset(chars)

221 and x[:1] in initial

222 )

223

224 chunks = ['$'] 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

225 chunks.extend( 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

226 f'[{json.dumps(x)}]' if needs_longhand(x) else f'.{x}' for x in path

227 )

228 return ''.join(chunks) 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

229

230

231 class _VaultConfigValidator: (empty)

232 INVALID_CONFIG_ERROR = 'vault config is invalid' (empty)

233

234 def __init__(self, maybe_config: Any) -> None: # noqa: ANN401 (empty)

235 self.maybe_config = maybe_config 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

236

237 def traverse_path(self, path: tuple[str, ...]) -> Any: # noqa: ANN401 (empty)

238 obj = self.maybe_config 16 ctx 1acjnkfgmoiphlebd

239 for key in path: 16 ctx 1acjnkfgmoiphlebd

240 obj = obj[key] 16 ctx 1acjnkfgmoiphlebd

241 return obj 16 ctx 1acjnkfgmoiphlebd

242

243 def walk_subconfigs( (empty)

244 self,

245 ) -> Iterator[tuple[tuple[str] | tuple[str, str], str, Any]]:

246 obj = cast('dict[str, dict[str, Any]]', self.maybe_config) 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

247 if isinstance(obj.get('global', False), dict): 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

248 for k, v in list(obj['global'].items()): 47 ctx 1acjzVABqkfrWCXYDEZ012345g6FGHtIJipuevKLMNwxysbd

249 yield ('global',), k, v 47 ctx 1acjzVABqkfrWCXYDEZ012345g6FGHtIJipuevKLMNwxysbd

250 for sv_name, sv_obj in list(obj['services'].items()): 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

251 for k, v in list(sv_obj.items()): 47 ctx 1acjnzOVABqkfrWCDEgmPFGHtQRSIJTUohluevKLMNwxysbd

252 yield ('services', sv_name), k, v 46 ctx 1acjnzOVABqkfrCDEgmPFGHtQRSIJTUohluevKLMNwxysbd

253

254 def validate( # noqa: C901,PLR0912 (empty)

255 self,

256 *,

257 allow_unknown_settings: bool = False,

258 ) -> None:

259 err_obj_not_a_dict = 'vault config is not a dict' 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

260 err_non_str_service_name = ( 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

261 'vault config contains non-string service name {sv_name!r}'

262 )

263 err_not_a_dict = 'vault config entry {json_path_str} is not a dict' 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

264 err_not_a_string = 'vault config entry {json_path_str} is not a string' 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

265 err_not_an_int = 'vault config entry {json_path_str} is not an integer' 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

266 err_unknown_setting = ( 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

267 'vault config entry {json_path_str} uses unknown setting {key!r}'

268 )

269 err_bad_number0 = 'vault config entry {json_path_str} is negative' 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

270 err_bad_number1 = 'vault config entry {json_path_str} is not positive' 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

271

272 kwargs: dict[str, Any] = { 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

273 'allow_unknown_settings': allow_unknown_settings,

274 }

275 if not isinstance(self.maybe_config, dict): 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

276 raise TypeError(err_obj_not_a_dict.format(**kwargs)) 5 ctx 19!#bd

277 if 'global' in self.maybe_config: 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

278 o_global = self.maybe_config['global'] 48 ctx 1acjzVABqkfrWCXYDEZ012345g6FGH7tIJipuevKLMNwxysbd

279 if not isinstance(o_global, dict): 48 ctx 1acjzVABqkfrWCXYDEZ012345g6FGH7tIJipuevKLMNwxysbd

280 kwargs['json_path_str'] = json_path(['global']) 3 ctx 17bd

281 raise TypeError(err_not_a_dict.format(**kwargs)) 3 ctx 17bd

282 if not isinstance(self.maybe_config.get('services'), dict): 61 ctx 1acjnzOVABqkfr8WCXYDEZ012345gmP6FGH7tQRSIJTUoiphluevKLMNwxysbd

283 kwargs['json_path_str'] = json_path(['services']) 4 ctx 187bd

284 raise TypeError(err_not_a_dict.format(**kwargs)) 4 ctx 187bd

285 for sv_name, service in self.maybe_config['services'].items(): 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

286 if not isinstance(sv_name, str): 47 ctx 1acjnzOVABqkfrWCDEgmPFGHtQRSIJTUohluevKLMNwxysbd

287 kwargs['sv_name'] = sv_name 2 ctx 1bd

288 raise TypeError(err_non_str_service_name.format(**kwargs)) 2 ctx 1bd

289 if not isinstance(service, dict): 47 ctx 1acjnzOVABqkfrWCDEgmPFGHtQRSIJTUohluevKLMNwxysbd

290 kwargs['json_path_str'] = json_path(['services', sv_name]) 2 ctx 1bd

291 raise TypeError(err_not_a_dict.format(**kwargs)) 2 ctx 1bd

292 for path, key, value in self.walk_subconfigs(): 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

293 kwargs['path'] = path 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

294 kwargs['key'] = key 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

295 kwargs['value'] = value 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

296 kwargs['json_path_str'] = json_path([*path, key]) 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

297 # TODO(the-13th-letter): Rewrite using structural pattern

298 # matching.

299 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9

300 if key in {'key', 'phrase'}: 59 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysbd

301 if not isinstance(value, str): 47 ctx 1czVABqfrWCXYDEZ012345gm6FGHtIJiphluevKLMNwxysbd

302 raise TypeError(err_not_a_string.format(**kwargs)) 3 ctx 1cbd

303 elif key == 'unicode_normalization_form' and path == ('global',): 45 ctx 1acjnzOABkfrCDEgmPFGHtQRSIJTUoihluevKLMNwxysbd

304 if not isinstance(value, str): 6 ctx 1cfrebd

305 raise TypeError(err_not_a_string.format(**kwargs)) 2 ctx 1bd

306 if not allow_unknown_settings: 6 ctx 1cfrebd

307 raise ValueError(err_unknown_setting.format(**kwargs)) 1 ctx 1d

308 elif key == 'notes' and path != ('global',): 45 ctx 1acjnzOABkfrCDEgmPFGHtQRSIJTUoihluevKLMNwxysbd

309 if not isinstance(value, str): 20 ctx 1cfrtJhluevKLMNwxysbd

310 raise TypeError(err_not_a_string.format(**kwargs)) 2 ctx 1bd

311 elif key in { 39 ctx 1acjnzOABkfrCDEgmPFGHtQRSITUoihuevwxysbd

312 'length',

313 'repeat',

314 'lower',

315 'upper',

316 'number',

317 'space',

318 'dash',

319 'symbol',

320 }:

321 if not isinstance(value, int): 39 ctx 1acjnzOABkfrCDEgmPFGHtQRSITUoihuevwxysbd

322 raise TypeError(err_not_an_int.format(**kwargs)) 3 ctx 1cbd

323 if key == 'length' and value < 1: 39 ctx 1acjnzOABkfrCDEgmPFGHtQRSITUoihuevwxysbd

324 raise ValueError(err_bad_number1.format(**kwargs)) 2 ctx 1bd

325 if key != 'length' and value < 0: 39 ctx 1acjnzOABkfrCDEgmPFGHtQRSITUoihuevwxysbd

326 raise ValueError(err_bad_number0.format(**kwargs)) 3 ctx 1cbd

327 elif not allow_unknown_settings: 6 ctx 1cfrebd

328 raise ValueError(err_unknown_setting.format(**kwargs)) 1 ctx 1d

329

330 def clean_up_falsy_values(self) -> Iterator[CleanupStep]: # noqa: C901 (empty)

331 obj = self.maybe_config 18 ctx 1acjnqkf9gmoiphlebd

332 if ( 18 ctx 1acjnqkf9gmoiphlebd

333 not isinstance(obj, dict)

334 or 'services' not in obj

335 or not isinstance(obj['services'], dict)

336 ):

337 # Defensive programming, so no coverage.

338 raise ValueError(self.INVALID_CONFIG_ERROR) # pragma: no cover 3 ctx 19bd

339 if 'global' in obj and not isinstance(obj['global'], dict): 17 ctx 1acjnqkfgmoiphlebd

340 # Defensive programming, so no coverage.

341 raise ValueError(self.INVALID_CONFIG_ERROR) # pragma: no cover 2 ctx 1bd

342 if not all( 17 ctx 1acjnqkfgmoiphlebd

343 isinstance(service_obj, dict)

344 for service_obj in obj['services'].values()

345 ):

346 # Defensive programming, so no coverage.

347 raise ValueError(self.INVALID_CONFIG_ERROR) # pragma: no cover 2 ctx 1bd

348

349 def falsy(value: Any) -> bool: # noqa: ANN401 17 ctx 1acjnqkfgmoiphlebd

350 return not js_truthiness(value) 16 ctx 1acjnkfgmoiphlebd

351

352 def falsy_but_not_zero(value: Any) -> bool: # noqa: ANN401 17 ctx 1acjnqkfgmoiphlebd

353 return not js_truthiness(value) and not ( 11 ctx 1acjfgmihebd

354 isinstance(value, int) and value == 0

355 )

356

357 def falsy_but_not_string(value: Any) -> bool: # noqa: ANN401 17 ctx 1acjnqkfgmoiphlebd

358 return not js_truthiness(value) and value != '' # noqa: PLC1901 11 ctx 1cfgmiphlebd

359

360 for path, key, value in self.walk_subconfigs(): 17 ctx 1acjnqkfgmoiphlebd

361 service_obj = self.traverse_path(path) 16 ctx 1acjnkfgmoiphlebd

362 # TODO(the-13th-letter): Rewrite using structural pattern

363 # matching.

364 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9

365 if key == 'phrase' and falsy_but_not_string(value): 16 ctx 1acjnkfgmoiphlebd

366 yield CleanupStep( 2 ctx 1ce

367 (*path, key), service_obj[key], 'replace', ''

368 )

369 service_obj[key] = '' 2 ctx 1ce

370 elif key == 'notes' and falsy(value): 16 ctx 1acjnkfgmoiphlebd

371 yield CleanupStep( 3 ctx 1cle

372 (*path, key), service_obj[key], 'remove', None

373 )

374 service_obj.pop(key) 3 ctx 1cle

375 elif key == 'key' and falsy(value): 16 ctx 1acjnkfgmoiphlebd

376 if path == ('global',): 6 ctx 1ciphle

377 yield CleanupStep( 3 ctx 1cip

378 (*path, key), service_obj[key], 'remove', None

379 )

380 service_obj.pop(key) 3 ctx 1cip

381 else:

382 yield CleanupStep( 4 ctx 1chle

383 (*path, key), service_obj[key], 'replace', ''

384 )

385 service_obj[key] = '' 4 ctx 1chle

386 elif key == 'length' and falsy(value): 16 ctx 1acjnkfgmoiphlebd

387 yield CleanupStep( 2 ctx 1ce

388 (*path, key), service_obj[key], 'replace', 20

389 )

390 service_obj[key] = 20 2 ctx 1ce

391 elif key == 'repeat' and falsy_but_not_zero(value): 16 ctx 1acjnkfgmoiphlebd

392 yield CleanupStep((*path, key), service_obj[key], 'replace', 0) 2 ctx 1ce

393 service_obj[key] = 0 2 ctx 1ce

394 elif key in { 16 ctx 1acjnkfgmoiphlebd

395 'lower',

396 'upper',

397 'number',

398 'space',

399 'dash',

400 'symbol',

401 } and falsy_but_not_zero(value):

402 yield CleanupStep( 2 ctx 1ce

403 (*path, key), service_obj[key], 'remove', None

404 )

405 service_obj.pop(key) 2 ctx 1ce

406

407

408@overload (empty)

409@deprecated( (empty)

410 'allow_derivepassphrase_extensions argument is deprecated since v0.4.0, ' (empty)

411 'to be removed in v1.0: no extensions are defined'

412)

413 def validate_vault_config( (empty)

414 obj: Any, # noqa: ANN401 (empty)

415 /,

416 *,

417 allow_derivepassphrase_extensions: bool, (empty)

418 allow_unknown_settings: bool = False, (empty)

419) -> None: ... (empty)

420

421

422@overload (empty)

423 def validate_vault_config( (empty)

424 obj: Any, # noqa: ANN401 (empty)

425 /,

426 *,

427 allow_unknown_settings: bool = False, (empty)

428) -> None: ... (empty)

429

430

431 def validate_vault_config( (empty)

432 obj: Any,

433 /,

434 *,

435 allow_unknown_settings: bool = False,

436 allow_derivepassphrase_extensions: bool = _Omitted(), # type: ignore[assignment]

437) -> None:

438 """Check that `obj` is a valid vault config.

439

440 Args:

441 obj:

442 The object to test.

443 allow_unknown_settings:

444 If false, abort on unknown settings.

445 allow_derivepassphrase_extensions:

446 (Deprecated.) Ignored since v0.4.0.

447

448 Raises:

449 TypeError:

450 An entry in the vault config, or the vault config itself,

451 has the wrong type.

452 ValueError:

453 An entry in the vault config is not allowed, or has a

454 disallowed value.

455

456 Warning: Deprecated argument

457 **v0.4.0**:

458 The `allow_derivepassphrase_extensions` keyword argument is

459 deprecated, and will be removed in v1.0. There are no

460 specified `derivepassphrase` extensions.

461

462 """

463 # TODO(the-13th-letter): Remove this block in v1.0.

464 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-allow-derivepassphrase-extensions

465 # TODO(the-13th-letter): Add tests that trigger the deprecation warning,

466 # then include this in coverage.

467 if not isinstance( 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

468 allow_derivepassphrase_extensions, _Omitted

469 ): # pragma: no cover

470 warnings.warn(

471 get_overloads(validate_vault_config)[0].__deprecated__, # type: ignore[attr-defined]

472 DeprecationWarning,

473 stacklevel=2,

474 )

475

476 return _VaultConfigValidator(obj).validate( 64 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#bd

477 allow_unknown_settings=allow_unknown_settings

478 )

479

480

481 def is_vault_config(obj: Any) -> TypeIs[VaultConfig]: # noqa: ANN401 (empty)

482 """Check if `obj` is a valid vault config, according to typing.

483

484 Args:

485 obj: The object to test.

486

487 Returns:

488 True if this is a vault config, false otherwise.

489

490 """ # noqa: DOC501

491 try: 63 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#b

492 validate_vault_config( 63 ctx 1acjnzOVABqkf9r8WCXYDEZ012345gmP6FGH7tQRSI!JTUoiphluevKLMNwxys#b

493 obj,

494 allow_unknown_settings=True,

495 )

496 except (TypeError, ValueError) as exc: 6 ctx 1987!#b

497 if 'vault config ' not in str(exc): # pragma: no cover 6 ctx 1987!#b

498 # Defensive programming, so no coverage.

499 raise

500 return False 6 ctx 1987!#b

501 return True 58 ctx 1acjnzOVABqkfrWCXYDEZ012345gmP6FGHtQRSIJTUoiphluevKLMNwxysb

502

503

504 def js_truthiness(value: Any, /) -> bool: # noqa: ANN401 (empty)

505 """Return the truthiness of the value, according to JavaScript/ECMAScript.

506

507 Like Python, ECMAScript considers certain values to be false in

508 a boolean context, and every other value to be true. These

509 considerations do not agree: ECMAScript considers [`math.nan`][] to

510 be false too, and empty arrays and objects/dicts to be true,

511 contrary to Python. Because of these discrepancies, we cannot defer

512 to [`bool`][] for ECMAScript truthiness checking, and need

513 a separate, explicit predicate.

514

515 (Some falsy values in ECMAScript aren't defined in Python:

516 `undefined`, and `document.all`. We do not implement support for

517 those.)

518

519 !!! note

520

521 We cannot use a simple `value not in falsy_values` check,

522 because [`math.nan`][] behaves in annoying and obstructive ways.

523 In general, `float('NaN') == float('NaN')` is false, and

524 `float('NaN') != math.nan` and `math.nan != math.nan` are true.

525 CPython says `float('NaN') in [math.nan]` is false, PyPy3 says

526 it is true. Seemingly the only reliable and portable way to

527 check for [`math.nan`][] is to use [`math.isnan`][] directly.

528

529 Args:

530 value: The value to test.

531

532 """ # noqa: RUF002

533 try: 18 ctx 1acjnqkfgmoiphle$bd

534 if value in {None, False, 0, 0.0, ''}: # noqa: B033 18 ctx 1acjnqkfgmoiphle$bd

535 return False 14 ctx 1acjnqkfgoiphle

536 except TypeError: 3 ctx 1cbd

537 # All falsy values are hashable, so this can't be falsy.

538 return True 3 ctx 1cbd

539 return not (isinstance(value, float) and math.isnan(value)) 17 ctx 1acjnkfgmoiphle$bd

540

541

542 class CleanupStep(NamedTuple): (empty)

543 """A single executed step during vault config cleanup.

544

545 Attributes:

546 path:

547 A sequence of object keys or array indices to navigate to

548 the JSON value that was cleaned up.

549 old_value:

550 The old value.

551 action:

552 Either `'replace'` if `old_value` was replaced with

553 `new_value`, or `'remove'` if `old_value` was removed.

554 new_value:

555 The new value.

556

557 """

558

559 path: Sequence[str | int] (empty)

560 """""" (empty)

561 old_value: Any (empty)

562 """""" (empty)

563 action: Literal['replace', 'remove'] (empty)

564 """""" (empty)

565 new_value: Any (empty)

566 """""" (empty)

567

568

569 def clean_up_falsy_vault_config_values( (empty)

570 obj: Any, # noqa: ANN401

571) -> Sequence[CleanupStep] | None:

572 """Convert falsy values in a vault config to correct types, in-place.

573

574 Needed for compatibility with vault(1), which sometimes uses only

575 truthiness checks.

576

577 If vault(1) considered `obj` to be valid, then after clean up,

578 `obj` will be valid as per [`validate_vault_config`][].

579

580 Args:

581 obj:

582 A presumed valid vault configuration save for using falsy

583 values of the wrong type.

584

585 Returns:

586 A list of 4-tuples `(key_tup, old_value, action, new_value)`,

587 indicating the cleanup actions performed. `key_tup` is

588 a sequence of object keys and/or array indices indicating the

589 JSON path to the leaf value that was cleaned up, `old_value` is

590 the old value, `new_value` is the new value, and `action` is

591 either `replace` (`old_value` was replaced with `new_value`) or

592 `remove` (`old_value` was removed, and `new_value` is

593 meaningless).

594

595 If cleanup was never attempted because of an obviously invalid

596 vault configuration, then `None` is returned, directly.

597

598 """

599 try: 18 ctx 1acjnqkf9gmoiphlebd

600 return list(_VaultConfigValidator(obj).clean_up_falsy_values()) 18 ctx 1acjnqkf9gmoiphlebd

601 except ValueError: 3 ctx 19bd

602 return None 3 ctx 19bd

603

604

605# TODO(the-13th-letter): Use type variables local to each class.

606# https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.11

607 T_Buffer = TypeVar('T_Buffer', bound=Buffer) (empty)

608""" (empty)

609 A [`TypeVar`][] for classes implementing the [`Buffer`][] interface.

610

611 Warning:

612 Non-public attribute, provided for didactical and educational

613 purposes only. Subject to change without notice, including

614 removal.

615

616"""

617

618

619 class SSHKeyCommentPair(NamedTuple, Generic[T_Buffer]): (empty)

620 """SSH key plus comment pair. For typing purposes.

621

622 Attributes:

623 key: SSH key.

624 comment: SSH key comment.

625

626 """

627

628 key: T_Buffer (empty)

629 """""" (empty)

630 comment: T_Buffer (empty)

631 """""" (empty)

632

633 def toreadonly(self) -> SSHKeyCommentPair[bytes]: (empty)

634 """Return a copy with read-only entries."""

635 return SSHKeyCommentPair( 1 ctx 1*

636 key=bytes(self.key),

637 comment=bytes(self.comment),

638 )

639

640

641 class SSH_AGENTC(enum.Enum): # noqa: N801 (empty)

642 """SSH agent protocol numbers: client requests.

643

644 Attributes:

645 REQUEST_IDENTITIES (int):

646 List identities. Expecting

647 [`SSH_AGENT.IDENTITIES_ANSWER`][].

648 SIGN_REQUEST (int):

649 Sign data. Expecting [`SSH_AGENT.SIGN_RESPONSE`][].

650 ADD_IDENTITY (int):

651 Add an (SSH2) identity.

652 REMOVE_IDENTITY (int):

653 Remove an (SSH2) identity.

654 ADD_ID_CONSTRAINED (int):

655 Add an (SSH2) identity, including key constraints.

656 EXTENSION (int):

657 Issue a named request that isn't part of the core agent

658 protocol. Expecting [`SSH_AGENT.EXTENSION_RESPONSE`][] or

659 [`SSH_AGENT.EXTENSION_FAILURE`][] if the named request is

660 supported, [`SSH_AGENT.FAILURE`][] otherwise.

661

662 """

663

664 REQUEST_IDENTITIES = 11 (empty)

665 """""" (empty)

666 SIGN_REQUEST = 13 (empty)

667 """""" (empty)

668 ADD_IDENTITY = 17 (empty)

669 """""" (empty)

670 REMOVE_IDENTITY = 18 (empty)

671 """""" (empty)

672 ADD_ID_CONSTRAINED = 25 (empty)

673 """""" (empty)

674 EXTENSION = 27 (empty)

675 """""" (empty)

676

677

678 class SSH_AGENT(enum.Enum): # noqa: N801 (empty)

679 """SSH agent protocol numbers: server replies.

680

681 Attributes:

682 FAILURE (int):

683 Generic failure code.

684 SUCCESS (int):

685 Generic success code.

686 IDENTITIES_ANSWER (int):

687 Successful answer to [`SSH_AGENTC.REQUEST_IDENTITIES`][].

688 SIGN_RESPONSE (int):

689 Successful answer to [`SSH_AGENTC.SIGN_REQUEST`][].

690 EXTENSION_FAILURE (int):

691 Unsuccessful answer to [`SSH_AGENTC.EXTENSION`][].

692 EXTENSION_RESPONSE (int):

693 Successful answer to [`SSH_AGENTC.EXTENSION`][].

694

695 """

696

697 FAILURE = 5 (empty)

698 """""" (empty)

699 SUCCESS = 6 (empty)

700 """""" (empty)

701 IDENTITIES_ANSWER = 12 (empty)

702 """""" (empty)

703 SIGN_RESPONSE = 14 (empty)

704 """""" (empty)

705 EXTENSION_FAILURE = 28 (empty)

706 """""" (empty)

707 EXTENSION_RESPONSE = 29 (empty)

708 """""" (empty)

709

710

711 class StoreroomKeyPair(NamedTuple, Generic[T_Buffer]): (empty)

712 """A pair of AES256 keys, one for encryption and one for signing.

713

714 Attributes:

715 encryption_key:

716 AES256 key, used for encryption with AES256-CBC (with PKCS#7

717 padding).

718 signing_key:

719 AES256 key, used for signing with HMAC-SHA256.

720

721 """

722

723 encryption_key: T_Buffer (empty)

724 """""" (empty)

725 signing_key: T_Buffer (empty)

726 """""" (empty)

727

728 def toreadonly(self) -> StoreroomKeyPair[bytes]: (empty)

729 """Return a copy with read-only entries."""

730 return StoreroomKeyPair( 5 ctx 1s%'()

731 encryption_key=bytes(self.encryption_key),

732 signing_key=bytes(self.signing_key),

733 )

734

735

736 class StoreroomMasterKeys(NamedTuple, Generic[T_Buffer]): (empty)

737 """A triple of AES256 keys, for encryption, signing and hashing.

738

739 Attributes:

740 hashing_key:

741 AES256 key, used for hashing with HMAC-SHA256 to derive

742 a hash table slot for an item.

743 encryption_key:

744 AES256 key, used for encryption with AES256-CBC (with PKCS#7

745 padding).

746 signing_key:

747 AES256 key, used for signing with HMAC-SHA256.

748

749 """

750

751 hashing_key: T_Buffer (empty)

752 """""" (empty)

753 encryption_key: T_Buffer (empty)

754 """""" (empty)

755 signing_key: T_Buffer (empty)

756 """""" (empty)

757

758 def toreadonly(self) -> StoreroomMasterKeys[bytes]: (empty)

759 """Return a copy with read-only entries."""

760 return StoreroomMasterKeys( 7 ctx 1s%+,'()

761 hashing_key=bytes(self.hashing_key),

762 encryption_key=bytes(self.encryption_key),

763 signing_key=bytes(self.signing_key),

764 )

765

766

767 class PEP508Extra(str, enum.Enum): (empty)

768 """PEP 508 extras supported by `derivepassphrase`.

769

770 Attributes:

771 EXPORT:

772 The necessary dependencies to allow the `export` subcommand

773 to handle as many foreign configuration formats as possible.

774

775 """

776

777 EXPORT = 'export' (empty)

778 """""" (empty)

779

780 __str__ = str.__str__ (empty)

781 __format__ = str.__format__ # type: ignore[assignment] (empty)

782

783

784 class Feature(str, enum.Enum): (empty)

785 """Optional features supported by `derivepassphrase`.

786

787 Attributes:

788 SSH_KEY:

789 The `vault` subcommand supports using a master SSH key,

790 instead of a master passphrase, if an SSH agent is running

791 and the master SSH key is loaded into it.

792

793 This feature requires Python support for the SSH agent's

794 chosen communication channel technology.

795

796 """

797

798 SSH_KEY = 'master SSH key' (empty)

799 """""" (empty)

800

801 __str__ = str.__str__ (empty)

802 __format__ = str.__format__ # type: ignore[assignment] (empty)

803

804

805 class DerivationScheme(str, enum.Enum): (empty)

806 """Derivation schemes provided by `derivepassphrase`.

807

808 Attributes:

809 VAULT:

810 The derivation scheme used by James Coglan's `vault`.

811

812 """

813

814 VAULT = 'vault' (empty)

815 """""" (empty)

816

817 __str__ = str.__str__ (empty)

818 __format__ = str.__format__ # type: ignore[assignment] (empty)

819

820

821 class ForeignConfigurationFormat(str, enum.Enum): (empty)

822 """Configuration formats supported by `derivepassphrase export`.

823

824 Attributes:

825 VAULT_STOREROOM:

826 The vault "storeroom" format for the `export vault`

827 subcommand.

828 VAULT_V02:

829 The vault-native "v0.2" format for the `export vault`

830 subcommand.

831 VAULT_V03:

832 The vault-native "v0.3" format for the `export vault`

833 subcommand.

834

835 """

836

837 VAULT_STOREROOM = 'vault storeroom' (empty)

838 """""" (empty)

839 VAULT_V02 = 'vault v0.2' (empty)

840 """""" (empty)

841 VAULT_V03 = 'vault v0.3' (empty)

842 """""" (empty)

843

844 __str__ = str.__str__ (empty)

845 __format__ = str.__format__ # type: ignore[assignment] (empty)

846

847

848 class ExportSubcommand(str, enum.Enum): (empty)

849 """Subcommands provided by `derivepassphrase export`.

850

851 Attributes:

852 VAULT:

853 The `export vault` subcommand.

854

855 """

856

857 VAULT = 'vault' (empty)

858 """""" (empty)

859

860 __str__ = str.__str__ (empty)

861 __format__ = str.__format__ # type: ignore[assignment] (empty)

862

863

864 class Subcommand(str, enum.Enum): (empty)

865 """Subcommands provided by `derivepassphrase`.

866

867 Attributes:

868 EXPORT:

869 The `export` subcommand.

870 VAULT:

871 The `vault` subcommand.

872

873 """

874

875 EXPORT = 'export' (empty)

876 """""" (empty)

877 VAULT = 'vault' (empty)

878 """""" (empty)

879

880 __str__ = str.__str__ (empty)

881 __format__ = str.__format__ # type: ignore[assignment] (empty)

882

883

884@runtime_checkable (empty)

885 class SSHAgentSocket(Protocol): (empty)

886 """An abstract networking socket connected to an SSH agent. (empty)

887

888 The abstract socket supports the [`sendall`][socket.sendall] and

889 a [`recv`][socket.recv] operation, with the same signatures and

890 semantics as for "real" sockets. The abstract socket also supports

891 use as a context manager, for automatically closing the socket upon

892 exiting the context.

893

894 """

895

896 def __enter__(self) -> Any: ... # noqa: ANN401 (empty)

897

898 # mypy/typeshed has a *very* lax annotation of

899 # socket.socket.__exit__, which we need to be compatible with.

900 # *sigh*

901 def __exit__( (empty)

902 self,

903 *args: object, (empty)

904 ) -> bool | None: ... (empty)

905

906 def sendall(self, data: Buffer, flags: int = 0, /) -> None: ... (empty)

907

908 def recv(self, data: int, flags: int = 0, /) -> bytes: ... (empty)

909

910

911 SSHAgentSocketProvider: TypeAlias = 'Callable[[], SSHAgentSocket]' (empty)

912"""A callable that provides an SSH agent socket.""" (empty)

913

914

915 class SSHAgentSocketProviderEntry(NamedTuple): (empty)

916 """Registry information for the table of SSH agent socket providers.

917

918 Attributes:

919 provider: The callable that provides the socket.

920 name: The table key which this entry is registered as.

921 aliases: Other keys that shall point to this entry's key.

922

923 Note:

924 The table uses the key as the key, and the provider as the

925 value. It does not store this info object directly.

926

927 """

928

929 provider: SSHAgentSocketProvider (empty)

930 """""" (empty)

931 key: str (empty)

932 """""" (empty)

933 aliases: tuple[str, ...] (empty)