Coverage for tests\__init__.py: 98.876%
531 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
5from __future__ import annotations
7import base64
8import contextlib
9import copy
10import enum
11import errno
12import importlib.util
13import json
14import logging
15import os
16import pathlib
17import re
18import shlex
19import stat
20import sys
21import tempfile
22import types
23import zipfile
24from typing import TYPE_CHECKING, TypedDict
26import click.testing
27import hypothesis
28import pytest
29from hypothesis import strategies
30from typing_extensions import NamedTuple, assert_never, overload
32from derivepassphrase import _types, cli, ssh_agent, vault
33from derivepassphrase._internals import cli_helpers, cli_machinery
34from derivepassphrase.ssh_agent import socketprovider
36__all__ = ()
38if TYPE_CHECKING:
39 import socket
40 from collections.abc import Callable, Iterator, Mapping, Sequence
41 from contextlib import AbstractContextManager
42 from typing import IO, NotRequired
44 from typing_extensions import Any, Buffer, Self
47class SSHTestKey(NamedTuple):
48 """An SSH test key.
50 Attributes:
51 public_key:
52 The SSH public key string, as used e.g. by OpenSSH's
53 `authorized_keys` file. Includes a comment.
54 public_key_data:
55 The SSH protocol wire format of the public key.
56 private_key:
57 A base64 encoded representation of the private key, in
58 OpenSSH's v1 private key format.
59 private_key_blob:
60 The SSH protocol wire format of the private key.
61 expected_signature:
62 For deterministic signature types, this is the expected
63 signature of the vault UUID. For other types this is
64 `None`.
65 derived_passphrase:
66 For deterministic signature types, this is the "equivalent
67 master passphrase" derived from this key (a transformation
68 of [`expected_signature`][]). For other types this is
69 `None`.
71 """
73 public_key: bytes | str
74 """"""
75 public_key_data: bytes
76 """"""
77 private_key: bytes
78 """"""
79 private_key_blob: bytes | None = None
80 """"""
81 expected_signature: bytes | None = None
82 """"""
83 derived_passphrase: bytes | str | None = None
84 """"""
86 def is_suitable(
87 self,
88 *,
89 client: ssh_agent.SSHAgentClient | None = None,
90 ) -> bool:
91 """Return if this key is suitable for use with vault.
93 Args:
94 client:
95 An optional SSH agent client to check for additional
96 deterministic key types. If not given, assume no such
97 types.
99 """
100 return vault.Vault.is_suitable_ssh_key(
101 self.public_key_data, client=client
102 )
105class ValidationSettings(NamedTuple):
106 """Validation settings for [`VaultTestConfig`][]s.
108 Attributes:
109 allow_unknown_settings:
110 See [`_types.validate_vault_config`][].
112 """
114 allow_unknown_settings: bool
115 """"""
118class VaultTestConfig(NamedTuple):
119 """A (not necessarily valid) sample vault config, plus metadata.
121 Attributes:
122 config:
123 The actual configuration object. Usually a [`dict`][].
124 comment:
125 An explanatory comment for what is wrong with this config,
126 or empty if the config is valid. This is intended as
127 a debugging message to be shown to the user (e.g. when an
128 assertion fails), not as an error message to
129 programmatically match against.
130 validation_settings:
131 See [`_types.validate_vault_config`][].
133 """
135 config: Any
136 """"""
137 comment: str
138 """"""
139 validation_settings: ValidationSettings | None
140 """"""
143TEST_CONFIGS: list[VaultTestConfig] = [
144 VaultTestConfig(None, 'not a dict', None),
145 VaultTestConfig({}, 'missing required keys', None),
146 VaultTestConfig(
147 {'global': None, 'services': {}}, 'bad config value: global', None
148 ),
149 VaultTestConfig(
150 {'global': {'key': 123}, 'services': {}},
151 'bad config value: global.key',
152 None,
153 ),
154 VaultTestConfig(
155 {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},
156 '',
157 None,
158 ),
159 VaultTestConfig({'services': None}, 'bad config value: services', None),
160 VaultTestConfig(
161 {'services': {'1': {}, 2: {}}}, 'bad config value: services."2"', None
162 ),
163 VaultTestConfig(
164 {'services': {'1': {}, '2': 2}}, 'bad config value: services."2"', None
165 ),
166 VaultTestConfig(
167 {'services': {'sv': {'notes': ['sentinel', 'list']}}},
168 'bad config value: services.sv.notes',
169 None,
170 ),
171 VaultTestConfig(
172 {'services': {'sv': {'notes': 'blah blah blah'}}}, '', None
173 ),
174 VaultTestConfig(
175 {'services': {'sv': {'length': '200'}}},
176 'bad config value: services.sv.length',
177 None,
178 ),
179 VaultTestConfig(
180 {'services': {'sv': {'length': 0.5}}},
181 'bad config value: services.sv.length',
182 None,
183 ),
184 VaultTestConfig(
185 {'services': {'sv': {'length': ['sentinel', 'list']}}},
186 'bad config value: services.sv.length',
187 None,
188 ),
189 VaultTestConfig(
190 {'services': {'sv': {'length': -10}}},
191 'bad config value: services.sv.length',
192 None,
193 ),
194 VaultTestConfig(
195 {'services': {'sv': {'lower': '10'}}},
196 'bad config value: services.sv.lower',
197 None,
198 ),
199 VaultTestConfig(
200 {'services': {'sv': {'upper': -10}}},
201 'bad config value: services.sv.upper',
202 None,
203 ),
204 VaultTestConfig(
205 {'services': {'sv': {'number': ['sentinel', 'list']}}},
206 'bad config value: services.sv.number',
207 None,
208 ),
209 VaultTestConfig(
210 {
211 'global': {'phrase': 'my secret phrase'},
212 'services': {'sv': {'length': 10}},
213 },
214 '',
215 None,
216 ),
217 VaultTestConfig(
218 {'services': {'sv': {'length': 10, 'phrase': '...'}}}, '', None
219 ),
220 VaultTestConfig(
221 {'services': {'sv': {'length': 10, 'key': '...'}}}, '', None
222 ),
223 VaultTestConfig(
224 {'services': {'sv': {'upper': 10, 'key': '...'}}}, '', None
225 ),
226 VaultTestConfig(
227 {'services': {'sv': {'phrase': 'abc', 'key': '...'}}}, '', None
228 ),
229 VaultTestConfig(
230 {
231 'global': {'phrase': 'abc'},
232 'services': {'sv': {'phrase': 'abc', 'length': 10}},
233 },
234 '',
235 None,
236 ),
237 VaultTestConfig(
238 {
239 'global': {'key': '...'},
240 'services': {'sv': {'phrase': 'abc', 'length': 10}},
241 },
242 '',
243 None,
244 ),
245 VaultTestConfig(
246 {
247 'global': {'key': '...'},
248 'services': {'sv': {'phrase': 'abc', 'key': '...', 'length': 10}},
249 },
250 '',
251 None,
252 ),
253 VaultTestConfig(
254 {
255 'global': {'key': '...'},
256 'services': {
257 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
258 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
259 },
260 },
261 '',
262 None,
263 ),
264 VaultTestConfig(
265 {
266 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},
267 'services': {
268 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
269 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
270 },
271 },
272 '',
273 None,
274 ),
275 VaultTestConfig(
276 {
277 'global': {'key': '...', 'unicode_normalization_form': True},
278 'services': {},
279 },
280 'bad config value: global.unicode_normalization_form',
281 None,
282 ),
283 VaultTestConfig(
284 {
285 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},
286 'services': {
287 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
288 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
289 },
290 },
291 '',
292 ValidationSettings(True),
293 ),
294 VaultTestConfig(
295 {
296 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},
297 'services': {
298 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
299 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
300 },
301 },
302 'extension/unknown key: .global.unicode_normalization_form',
303 ValidationSettings(False),
304 ),
305 VaultTestConfig(
306 {
307 'global': {'key': '...', 'unknown_key': True},
308 'services': {
309 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
310 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
311 },
312 },
313 '',
314 ValidationSettings(True),
315 ),
316 VaultTestConfig(
317 {
318 'global': {'key': '...', 'unknown_key': True},
319 'services': {
320 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
321 'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
322 },
323 },
324 'unknown key: .global.unknown_key',
325 ValidationSettings(False),
326 ),
327 VaultTestConfig(
328 {
329 'global': {'key': '...'},
330 'services': {
331 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
332 'sv2': {
333 'length': 10,
334 'repeat': 1,
335 'lower': 1,
336 'unknown_key': True,
337 },
338 },
339 },
340 'unknown key: .services.sv2.unknown_key',
341 ValidationSettings(False),
342 ),
343 VaultTestConfig(
344 {
345 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},
346 'services': {
347 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
348 'sv2': {
349 'length': 10,
350 'repeat': 1,
351 'lower': 1,
352 'unknown_key': True,
353 },
354 },
355 },
356 '',
357 ValidationSettings(True),
358 ),
359 VaultTestConfig(
360 {
361 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},
362 'services': {
363 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
364 'sv2': {
365 'length': 10,
366 'repeat': 1,
367 'lower': 1,
368 'unknown_key': True,
369 },
370 },
371 },
372 '',
373 ValidationSettings(True),
374 ),
375 VaultTestConfig(
376 {
377 'global': {'key': '...', 'unicode_normalization_form': 'NFC'},
378 'services': {
379 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
380 'sv2': {
381 'length': 10,
382 'repeat': 1,
383 'lower': 1,
384 'unknown_key': True,
385 },
386 },
387 },
388 '',
389 ValidationSettings(True),
390 ),
391]
392"""The master list of test configurations for vault."""
395def is_valid_test_config(conf: VaultTestConfig, /) -> bool:
396 """Return true if the test config is valid.
398 Args:
399 conf: The test config to check.
401 """
402 return not conf.comment and conf.validation_settings in {
403 None,
404 (True,),
405 }
408def _test_config_ids(val: VaultTestConfig) -> Any: # pragma: no cover
409 """pytest id function for VaultTestConfig objects."""
410 assert isinstance(val, VaultTestConfig)
411 return val[1] or (val[0], val[1], val[2])
414@strategies.composite
415def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]:
416 """Hypothesis strategy for full vault service configurations.
418 Returns a sample configuration with restrictions on length, repeat
419 count, and all character classes, while ensuring the settings are
420 not obviously unsatisfiable.
422 Args:
423 draw:
424 The `draw` function, as provided for by hypothesis.
426 """
427 repeat = draw(strategies.integers(min_value=0, max_value=10)) 2e r s NbOb
428 lower = draw(strategies.integers(min_value=0, max_value=10)) 2e r s NbOb
429 upper = draw(strategies.integers(min_value=0, max_value=10)) 2e r s NbOb
430 number = draw(strategies.integers(min_value=0, max_value=10)) 2e r s NbOb
431 space = draw(strategies.integers(min_value=0, max_value=repeat)) 2e r s NbOb
432 dash = draw(strategies.integers(min_value=0, max_value=10)) 2e r s NbOb
433 symbol = draw(strategies.integers(min_value=0, max_value=10)) 2e r s NbOb
434 length = draw( 2e r s NbOb
435 strategies.integers(
436 min_value=max(1, lower + upper + number + space + dash + symbol),
437 max_value=70,
438 )
439 )
440 hypothesis.assume(lower + upper + number + dash + symbol > 0) 2e r s NbOb
441 hypothesis.assume(lower + upper + number + space + symbol > 0) 2e r s NbOb
442 hypothesis.assume(repeat >= space) 2e r s NbOb
443 return { 2e r s NbOb
444 'lower': lower,
445 'upper': upper,
446 'number': number,
447 'space': space,
448 'dash': dash,
449 'symbol': symbol,
450 'repeat': repeat,
451 'length': length,
452 }
455def is_smudgable_vault_test_config(conf: VaultTestConfig) -> bool:
456 """Check whether this vault test config can be effectively smudged.
458 A "smudged" test config is one where falsy values (in the JavaScript
459 sense) can be replaced by other falsy values without changing the
460 meaning of the config.
462 Args:
463 conf: A test config to check.
465 Returns:
466 True if the test config can be smudged, False otherwise.
468 """
469 config = conf.config 2a Abc
470 return bool( 2a Abc
471 isinstance(config, dict)
472 and ('global' not in config or isinstance(config['global'], dict))
473 and ('services' in config and isinstance(config['services'], dict))
474 and all(isinstance(x, dict) for x in config['services'].values())
475 and (config['services'] or config.get('global'))
476 )
479@strategies.composite
480def smudged_vault_test_config(
481 draw: strategies.DrawFn,
482 config: Any = strategies.sampled_from(TEST_CONFIGS).filter( # noqa: B008
483 is_smudgable_vault_test_config
484 ),
485) -> Any:
486 """Hypothesis strategy to replace falsy values with other falsy values.
488 Uses [`_types.js_truthiness`][] internally, which is tested
489 separately by
490 [`tests.test_derivepassphrase_types.test_100_js_truthiness`][].
492 Args:
493 draw:
494 The `draw` function, as provided for by hypothesis.
495 config:
496 A strategy which generates [`VaultTestConfig`][] objects.
498 Returns:
499 A new [`VaultTestConfig`][] where some falsy values have been
500 replaced or added.
502 """
504 falsy = (None, False, 0, 0.0, '', float('nan')) 2Abc
505 falsy_no_str = (None, False, 0, 0.0, float('nan')) 2Abc
506 falsy_no_zero = (None, False, '', float('nan')) 2Abc
507 conf = draw(config) 2Abc
508 hypothesis.assume(is_smudgable_vault_test_config(conf)) 2Abc
509 obj = copy.deepcopy(conf.config) 2Abc
510 services: list[dict[str, Any]] = list(obj['services'].values()) 2Abc
511 if 'global' in obj: 2Abc
512 services.append(obj['global']) 2Abc
513 assert all(isinstance(x, dict) for x in services), ( 2Abc
514 'is_smudgable_vault_test_config guard failed to '
515 'ensure each settings dict is a dict'
516 )
517 for service in services: 2Abc
518 for key in ('phrase',): 2Abc
519 value = service.get(key) 2Abc
520 if not _types.js_truthiness(value) and value != '': 2Abc
521 service[key] = draw(strategies.sampled_from(falsy_no_str)) 2Abc
522 for key in ( 2Abc
523 'notes',
524 'key',
525 'length',
526 'repeat',
527 ):
528 value = service.get(key) 2Abc
529 if not _types.js_truthiness(value): 2Abc
530 service[key] = draw(strategies.sampled_from(falsy)) 2Abc
531 for key in ( 2Abc
532 'lower',
533 'upper',
534 'number',
535 'space',
536 'dash',
537 'symbol',
538 ):
539 value = service.get(key) 2Abc
540 if not _types.js_truthiness(value) and value != 0: 2Abc
541 service[key] = draw(strategies.sampled_from(falsy_no_zero)) 2Abc
542 hypothesis.assume(obj != conf.config) 2Abc
543 return VaultTestConfig(obj, conf.comment, conf.validation_settings) 2Abc
546class KnownSSHAgent(str, enum.Enum):
547 """Known SSH agents.
549 Attributes:
550 UNKNOWN (str):
551 Not a known agent, or not known statically.
552 Pageant (str):
553 The agent from Simon Tatham's PuTTY suite.
554 OpenSSHAgent (str):
555 The agent from OpenBSD's OpenSSH suite.
556 FakeSSHAgentSocket (str):
557 The fake agent pseudo-socket defined in this test suite.
559 """
561 UNKNOWN = '(unknown)'
562 """"""
563 Pageant = 'Pageant'
564 """"""
565 OpenSSHAgent = 'OpenSSHAgent'
566 """"""
567 FakeSSHAgentSocket = 'FakeSSHAgentSocket'
568 """"""
571class SpawnedSSHAgentInfo(NamedTuple):
572 """Info about a spawned SSH agent, as provided by some fixtures.
574 Differs from [`RunningSSHAgentInfo`][] in that this info object
575 already provides a functional client connected to the agent, but not
576 the address.
578 Attributes:
579 agent_type:
580 The agent's type.
581 client:
582 An SSH agent client connected to this agent.
583 isolated:
584 Whether this agent was spawned specifically for this test
585 suite, with attempts to isolate it from the user. If false,
586 then the user may be interacting with the agent externally,
587 meaning e.g. keys other than the test keys may be visible in
588 this agent.
590 """
592 agent_type: KnownSSHAgent
593 """"""
594 client: ssh_agent.SSHAgentClient
595 """"""
596 isolated: bool
597 """"""
600class RunningSSHAgentInfo(NamedTuple):
601 """Info about a running SSH agent, as provided by some fixtures.
603 Differs from [`SpawnedSSHAgentInfo`][] in that this info object
604 provides only an address of the agent, not a functional client
605 already connected to it. The running SSH agent may or may not be
606 isolated.
608 Attributes:
609 socket:
610 A socket address to connect to the agent.
611 agent_type:
612 The agent's type.
614 """
616 socket: str | type[_types.SSHAgentSocket]
617 """"""
618 agent_type: KnownSSHAgent
619 """"""
621 def require_external_address(self) -> str: # pragma: no cover
622 if not isinstance(self.socket, str): 2Ub
623 pytest.skip( 2Ub
624 reason='This test requires a real, externally resolvable ' 2Ub
625 'address for the SSH agent socket.'
626 )
627 return self.socket
630ALL_KEYS: Mapping[str, SSHTestKey] = {
631 'ed25519': SSHTestKey(
632 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
633b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
634QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju
635xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg
636AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw
637idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC
638-----END OPENSSH PRIVATE KEY-----
639""",
640 private_key_blob=bytes.fromhex("""
641 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
642 00 00 00 20
643 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
644 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
645 00 00 00 40
646 1b 33 f0 3c eb d9 e4 59 96 de da da 77 0e 6f cb
647 ea 08 ad be 6a 47 fd a4 59 b7 cb 01 20 8d c4 c5
648 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
649 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
650 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 74
651 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
652"""),
653 public_key=rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase
654""",
655 public_key_data=bytes.fromhex("""
656 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
657 00 00 00 20
658 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
659 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
660"""),
661 expected_signature=bytes.fromhex("""
662 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
663 00 00 00 40
664 f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
665 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
666 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
667 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
668 """),
669 derived_passphrase=rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==',
670 ),
671 # Currently only supported by PuTTY (which is deficient in other
672 # niceties of the SSH agent and the agent's client).
673 'ed448': SSHTestKey(
674 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
675b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz
676c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT
677zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA
678ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM
679/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373
6809TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm
681GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp
682dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ==
683-----END OPENSSH PRIVATE KEY-----
684""",
685 private_key_blob=bytes.fromhex("""
686 00 00 00 09 73 73 68 2d 65 64 34 34 38
687 00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04
688 c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
689 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
690 b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
691 00 00 00 72 33 b1
692 88 31 1b d6 24 1d d8 0f a4 88 a4 01 76 ba e7 57
693 e2 8c d9 7b 45 fd d7 bc 0f c2 20 6b b3 7e f7 f5
694 33 35 9f de 0d 89 be a1 af 11 8d 98 29 36 88 14
695 03 7b 62 80 2c f8 00 e2 f6 72 d3 4f 56 bb cc 04
696 c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
697 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
698 b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
699 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
700 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
701"""),
702 public_key=rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase
703""",
704 public_key_data=bytes.fromhex("""
705 00 00 00 09 73 73 68 2d 65 64 34 34 38
706 00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04
707 c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19
708 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69
709 b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00
710 """),
711 expected_signature=bytes.fromhex("""
712 00 00 00 09 73 73 68 2d 65 64 34 34 38
713 00 00 00 72 06 86
714 f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67
715 97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8
716 2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84
717 3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1
718 db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80
719 db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53
720 7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00
721 """),
722 derived_passphrase=rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA',
723 ),
724 'rsa': SSHTestKey(
725 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
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
762-----END OPENSSH PRIVATE KEY-----
763""",
764 private_key_blob=bytes.fromhex("""
765 00 00 00 07 73 73 68 2d 72 73 61
766 00 00 01 81 00
767 b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0
768 f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99
769 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a
770 ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2
771 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30
772 de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee
773 ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d
774 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18
775 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9
776 da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42
777 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c
778 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6
779 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc
780 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97
781 f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06
782 c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a
783 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a
784 ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11
785 cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f
786 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66
787 fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38
788 b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
789 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
790 d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
791 00 00 00 03 01 00 01
792 00 00 01 7f
793 70 d5 58 4f e3 a6 e3 1f 7e 48 8b 5c cf 96 ce
794 07 21 88 3a 3c 96 50 7f 1f f6 b4 63 9e be 48 2e
795 ec 1a 6c 80 e0 bc 56 95 1b ec 4c d4 ed 61 58 00
796 ea cc c9 d2 11 c9 27 1d 19 13 60 4b 2a 60 fc a4
797 02 dc 45 be 33 85 12 1e f4 ea a4 54 27 e7 d5 66
798 4a 91 18 46 88 de 85 b2 58 a4 ce 85 c0 88 60 89
799 ad 38 db db d0 b3 af fc 74 36 1f 60 17 0a 87 aa
800 8d 24 f5 aa bf f1 4a e9 63 06 d7 01 f2 ec 31 b3
801 71 2a 68 ed 72 53 b4 28 66 90 dc 31 7f 69 94 7e
802 d1 99 cf 67 35 f6 5c 82 b9 e4 b4 f9 36 75 e5 f6
803 f9 8f c0 e6 dc dd 56 01 b5 21 ea 98 a7 a0 81 30
804 b3 4c ba 12 f4 38 f7 43 7a 18 01 a0 3b a3 c7 ea
805 e1 91 9d 60 a6 8d 70 71 42 c8 33 de 0d 2f b5 52
806 1c 13 d7 d0 b8 21 f5 3f b7 8f 51 d5 3f e6 27 30
807 0b e1 7c eb df 72 d2 e5 6c 9d 3a f9 36 06 44 03
808 2e 17 62 cf 08 06 4c fe 6d 05 13 20 2e ad 18 16
809 c4 08 cb 5e 6f b8 92 9d 86 00 95 e5 c3 cd 9d 78
810 39 10 f8 ff 1c 1c d4 fa b3 d6 47 47 96 12 cb 79
811 69 67 7e 79 f7 86 4c 38 b4 7c 2a a5 6f 86 02 46
812 17 a8 bd d3 5f 41 88 5f 75 48 3c 63 13 eb 29 e9
813 d3 46 7c df 16 fb ae 2d 1d d0 ef d9 ad 0c b2 8f
814 bc 42 3c b1 ee c4 e9 8b ef 84 8c b5 3c 23 2e f7
815 fd ee 01 f2 a6 d7 33 28 5e c8 0f d4 51 f8 0c b4
816 d2 92 86 1e ac 2e 5a c3 f6 1d 92 9b d4 45 4e e1
817 00 00 00 c0
818 26 1d ee ed 67 e7 8e 18 4a 65 86 c0 5b 84 ec 3c
819 d5 c3 32 b0 74 fe de f1 c8 04 20 16 69 28 ec 97
820 c4 4b 2a c9 48 b8 35 70 ef 90 30 e8 83 80 3e f3
821 b9 0f 51 4a 93 48 29 d3 ec 9c 89 da e6 97 a1 f7
822 bf 66 56 36 f6 1e 09 e2 92 3c 89 10 29 81 a4 b7
823 70 64 a8 57 ec c9 c8 be 02 b0 e6 59 d0 5c 76 e8
824 12 d5 82 6b 53 79 f2 5e df b2 bc f0 6e 2f 69 15
825 4b 94 a7 d7 52 85 73 5f 7e 69 54 0a 30 d1 b1 52
826 db 2b 90 ba fa 26 88 43 20 96 15 82 bc 99 ce 9d
827 a4 d9 08 a0 f2 9f f9 78 3e 10 90 88 15 0a 08 4e
828 a7 ae df 41 9c d7 e3 3e 7d d6 5c da 08 6a d1 be
829 97 2e d8 61 fe e1 c4 af 2f 1b eb c3 39 2a 09 4c
830 00 00 00 c1 00
831 f0 d6 2f 88 75 cb cf 57 fd ee fe af 4e fe 4c cb
832 a4 a3 b7 b5 b4 fc f6 c7 35 ce 18 d9 b0 33 63 0c
833 01 a5 b8 da fa 9f 7f 22 ab f5 d8 45 9b bb 51 32
834 fd 04 6f 84 80 55 5f 21 45 7b 5e c6 13 e5 ef 81
835 3d fd 77 55 2f 78 af 36 7b 99 a0 ac 3a 55 0b 5f
836 e5 5d 30 ed b3 06 e7 07 22 87 9c f5 15 25 45 9a
837 df 07 6d 41 d9 6e d8 18 c8 5b a6 86 b9 94 dd e5
838 28 94 3b 69 f1 e8 75 76 54 32 9d 1c 4c 56 ce 99
839 bc c9 81 2c ce db f3 44 ec 18 55 c2 6b dc 53 34
840 c3 24 63 0c 2a ec 41 3b ac 3d f5 82 83 29 12 a6
841 d0 f3 4b 14 3c f6 ea b5 a6 f4 b7 4d 4b 63 15 7d
842 5b 8f 31 3a 73 4f 6e 87 40 6e 29 15 a8 1e ab d7
843 00 00 00 c1 00
844 bc d1 05 77 59 17 c9 1b 48 16 b9 31 35 01 55 34
845 f3 80 a4 26 d4 2e fb da 02 7c 4f 4d 43 24 45 d9
846 b1 5b 4e bf b9 94 0b 5f e6 fd bc ba 1e 4e 2d 3f
847 2b c3 06 e1 a3 f6 11 ea e0 de dd 3e a1 ae b4 76
848 f5 ab 99 f4 00 3b a8 42 34 56 ec 15 f0 e5 0b c2
849 d8 40 03 f7 5c 5e c5 da 2b 20 0e 41 81 75 3e aa
850 5b 41 ab 3c c1 57 35 6d 17 bf a3 39 93 a3 7f 33
851 a5 69 35 fd 23 92 39 bd ec 9e 4d a4 f1 66 3d 57
852 5d 4c e2 6e d0 4d 74 c1 09 26 9e e2 7e e7 18 9a
853 86 00 03 01 3b 2b e5 65 59 a8 03 10 ad b2 f0 cb
854 5e f7 2f 44 f8 dd 2e 3b 68 fe 87 ce 6c 42 df d4
855 21 bd a2 13 fb e1 72 00 60 a7 ad 78 d9 69 d2 09
856 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
857 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
858"""),
859 public_key=rb"""ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase
860""",
861 public_key_data=bytes.fromhex("""
862 00 00 00 07 73 73 68 2d 72 73 61
863 00 00 00 03 01 00 01
864 00 00 01 81 00
865 b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0
866 f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99
867 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a
868 ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2
869 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30
870 de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee
871 ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d
872 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18
873 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9
874 da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42
875 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c
876 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6
877 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc
878 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97
879 f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06
880 c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a
881 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a
882 ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11
883 cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f
884 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66
885 fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38
886 b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
887 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
888 d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
889"""),
890 expected_signature=bytes.fromhex("""
891 00 00 00 07 73 73 68 2d 72 73 61
892 00 00 01 80
893 a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be
894 79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68
895 f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92
896 f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5
897 88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97
898 65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3
899 55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25
900 d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e
901 50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f
902 91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d
903 67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27
904 22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26
905 bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55
906 e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba
907 b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5
908 64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4
909 72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3
910 f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20
911 e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46
912 83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81
913 da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19
914 a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec
915 de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38
916 e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce
917"""),
918 derived_passphrase=rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O',
919 ),
920 'dsa1024': SSHTestKey(
921 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
922b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
923NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa
9240C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9
925Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H
926ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA
927UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm
928qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX
929BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs
930G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr
931l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/
932GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh
933MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6
934xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt
935/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P
9362yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW
937ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T
938mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH
939u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW
9400gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH
941-----END OPENSSH PRIVATE KEY-----
942""",
943 private_key_blob=bytes.fromhex("""
944 00 00 00 07 73 73 68 2d 64 73 73
945 00 00 00 81 00
946 bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
947 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68
948 cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f
949 d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8
950 bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9
951 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29
952 a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50
953 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed
954 00 00 00 15 00 f7 d9 ce 64
955 e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb
956 00 00 00 80
957 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f
958 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc
959 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b
960 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e
961 ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df
962 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4
963 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be
964 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28
965 00 00 00 80
966 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80
967 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d
968 e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98
969 c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52
970 bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a
971 a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
972 a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
973 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
974 00 00 00 15 00 c9 1d f7 a7
975 8f 81 09 f5 69 1e 86 97 49 6a d3 c1 96 a0 96 d2
976 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
977 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
978"""),
979 public_key=rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase
980""",
981 public_key_data=bytes.fromhex("""
982 00 00 00 07 73 73 68 2d 64 73 73
983 00 00 00 81 00
984 bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
985 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68
986 cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f
987 d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8
988 bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9
989 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29
990 a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50
991 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed
992 00 00 00 15 00 f7 d9 ce 64
993 e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb
994 00 00 00 80
995 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f
996 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc
997 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b
998 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e
999 ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df
1000 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4
1001 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be
1002 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28
1003 00 00 00 80
1004 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80
1005 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d
1006 e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98
1007 c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52
1008 bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a
1009 a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
1010 a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
1011 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
1012"""),
1013 expected_signature=None,
1014 derived_passphrase=None,
1015 ),
1016 'ecdsa256': SSHTestKey(
1017 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
1018b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
10191zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S
10203SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP
1021mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5
1022Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb
1023oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp
1024dGhvdXQgcGFzc3BocmFzZQECAwQ=
1025-----END OPENSSH PRIVATE KEY-----
1026""",
1027 private_key_blob=bytes.fromhex("""
1028 00 00 00 13 65 63 64
1029 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1030 00 00 00 08 6e 69 73 74 70 32 35 36
1031 00 00 00 41 04
1032 cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55
1033 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
1034 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
1035 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
1036 00 00 00 21 00
1037 a2 25 ff 79 f4 a4 a5 48 c6 96 64 5d 31 ad 8a 2e
1038 fc d9 0a f8 c8 87 2f 1d da 71 8c ef d0 b0 8a 8a
1039 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1040 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1041"""),
1042 public_key=rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase
1043""",
1044 public_key_data=bytes.fromhex("""
1045 00 00 00 13 65 63 64
1046 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1047 00 00 00 08 6e 69 73 74 70 32 35 36
1048 00 00 00 41 04
1049 cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55
1050 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
1051 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
1052 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
1053"""),
1054 expected_signature=None,
1055 derived_passphrase=None,
1056 ),
1057 'ecdsa384': SSHTestKey(
1058 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
1059b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
10601zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ
1061DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N
1062ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0
1063cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh
1064au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx
1065JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B
10662OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA==
1067-----END OPENSSH PRIVATE KEY-----
1068""",
1069 private_key_blob=bytes.fromhex("""
1070 00 00 00 13 65 63 64
1071 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
1072 00 00 00 08 6e 69 73 74 70 33 38 34
1073 00 00 00 61 04
1074 a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f
1075 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16
1076 ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9
1077 a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
1078 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
1079 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
1080 00 00 00 31 00
1081 f9 b1 3c bc a7 e0 75 72 7f c3 84 e9 97 aa eb a2
1082 d4 17 31 bc 04 aa cf 37 af 90 19 5b 42 80 5a ef
1083 7c b4 e0 1d 8e 76 a5 4e 55 19 30 65 64 51 3b 3d
1084 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1085 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1086"""),
1087 public_key=rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase
1088""",
1089 public_key_data=bytes.fromhex("""
1090 00 00 00 13 65 63 64
1091 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34
1092 00 00 00 08 6e 69 73 74 70 33 38 34
1093 00 00 00 61 04
1094 a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f
1095 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16
1096 ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9
1097 a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
1098 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
1099 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
1100"""),
1101 expected_signature=None,
1102 derived_passphrase=None,
1103 ),
1104 'ecdsa521': SSHTestKey(
1105 private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY-----
1106b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
11071zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQASVOdwDznmlcGqiLvFtYeVtrAEiVz
1108iIfsL7jEM8Utu/m8WSkPFQtjwqdFw+WfZ0mi6qMbEFgi/ELzZSKVteCSbcMAhqAkOMFKiD
1109u4bxvsM6bT02Ru7q2yT41ySyGhUD0QySBnI6Ckt/wnQ1TEpj8zDKiRErxs9e6QLGElNRkz
1110LPMs+mMAAAEY2FXeh9hV3ocAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
1111AAAIUEAElTncA855pXBqoi7xbWHlbawBIlc4iH7C+4xDPFLbv5vFkpDxULY8KnRcPln2dJ
1112ouqjGxBYIvxC82UilbXgkm3DAIagJDjBSog7uG8b7DOm09Nkbu6tsk+NckshoVA9EMkgZy
1113OgpLf8J0NUxKY/MwyokRK8bPXukCxhJTUZMyzzLPpjAAAAQSFqUmKK7lGQzxT6GKZSLDju
1114U3otwLYnuj+/5AdzuB/zotu95UdFv9I2DNXzd9E4WAyz6IqBBNcsMkxrzHAdqsYDAAAAG3
1115Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQ==
1116-----END OPENSSH PRIVATE KEY-----
1117""",
1118 private_key_blob=bytes.fromhex("""
1119 00 00 00 13 65 63 64
1120 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31
1121 00 00 00 08 6e 69 73 74 70 35 32 31
1122 00 00 00 85 04 00 49 53 9d
1123 c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12
1124 25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29
1125 0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b
1126 10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86
1127 a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64
1128 6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67
1129 23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12
1130 bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63
1131 00 00 00 41 21
1132 6a 52 62 8a ee 51 90 cf 14 fa 18 a6 52 2c 38 ee
1133 53 7a 2d c0 b6 27 ba 3f bf e4 07 73 b8 1f f3 a2
1134 db bd e5 47 45 bf d2 36 0c d5 f3 77 d1 38 58 0c
1135 b3 e8 8a 81 04 d7 2c 32 4c 6b cc 70 1d aa c6 03
1136 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69
1137 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65
1138"""),
1139 public_key=rb"""ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABJU53APOeaVwaqIu8W1h5W2sASJXOIh+wvuMQzxS27+bxZKQ8VC2PCp0XD5Z9nSaLqoxsQWCL8QvNlIpW14JJtwwCGoCQ4wUqIO7hvG+wzptPTZG7urbJPjXJLIaFQPRDJIGcjoKS3/CdDVMSmPzMMqJESvGz17pAsYSU1GTMs8yz6Yw== test key without passphrase
1140""",
1141 public_key_data=bytes.fromhex("""
1142 00 00 00 13 65 63 64
1143 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36
1144 00 00 00 08 6e 69 73 74 70 35 32 31
1145 00 00 00 85 04 00 49 53 9d
1146 c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12
1147 25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29
1148 0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b
1149 10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86
1150 a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64
1151 6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67
1152 23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12
1153 bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63
1154"""),
1155 expected_signature=None,
1156 derived_passphrase=None,
1157 ),
1158}
1159"""The master list of SSH test keys."""
1160SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
1161 k: v for k, v in ALL_KEYS.items() if v.is_suitable()
1162}
1163"""The subset of SSH test keys suitable for use with vault."""
1164UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = {
1165 k: v for k, v in ALL_KEYS.items() if not v.is_suitable()
1166}
1167"""The subset of SSH test keys not suitable for use with vault."""
1169DUMMY_SERVICE = 'service1'
1170"""A standard/sample service name."""
1171DUMMY_PASSPHRASE = 'my secret passphrase'
1172"""A standard/sample passphrase."""
1173DUMMY_KEY1 = SUPPORTED_KEYS['ed25519'].public_key_data
1174"""A sample universally supported SSH test key (in wire format)."""
1175DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII')
1176"""
1177A sample universally supported SSH test key (in `authorized_keys` format).
1178"""
1179DUMMY_KEY2 = SUPPORTED_KEYS['rsa'].public_key_data
1180"""A second supported SSH test key (in wire format)."""
1181DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII')
1182"""A second supported SSH test key (in `authorized_keys` format)."""
1183DUMMY_KEY3 = SUPPORTED_KEYS['ed448'].public_key_data
1184"""A third supported SSH test key (in wire format)."""
1185DUMMY_KEY3_B64 = base64.standard_b64encode(DUMMY_KEY3).decode('ASCII')
1186"""A third supported SSH test key (in `authorized_keys` format)."""
1187DUMMY_CONFIG_SETTINGS = {
1188 'length': 10,
1189 'upper': 1,
1190 'lower': 1,
1191 'repeat': 5,
1192 'number': 1,
1193 'space': 1,
1194 'dash': 1,
1195 'symbol': 1,
1196}
1197"""Sample vault settings."""
1198DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o'
1199"""
1200The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_PASSPHRASE`][].
1201"""
1202DUMMY_RESULT_KEY1 = b'E<b<{ -7iG'
1203"""
1204The passphrase derived from [`DUMMY_SERVICE`][] using [`DUMMY_KEY1`][].
1205"""
1206DUMMY_PHRASE_FROM_KEY1_RAW = (
1207 b'\x00\x00\x00\x0bssh-ed25519'
1208 b'\x00\x00\x00@\xf0\x98\x19\x80l\x1a\x97\xd5&\x03n'
1209 b'\xcc\xe3e\x8f\x86f\x07\x13\x19\x13\t!33\xf9\xe46S'
1210 b'\x1d\xaf\xfd\r\x08\x1f\xec\xf8s\x9b\x8c_U9\x16|ST,'
1211 b'\x1eR\xbb0\xed\x7f\x89\xe2/iQU\xd8\x9e\xa6\x02'
1212)
1213"""
1214The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (raw format).
1215"""
1216DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg=='
1217"""
1218The "equivalent master passphrase" derived from [`DUMMY_KEY1`][] (in base64).
1219"""
1221VAULT_MASTER_KEY = 'vault key'
1222"""
1223The storage passphrase used to encrypt all sample vault native configurations.
1224"""
1225VAULT_V02_CONFIG = 'P7xeh5y4jmjpJ2pFq4KUcTVoaE9ZOEkwWmpVTURSSWQxbGt6emN4aFE4eFM3anVPbDRNTGpOLzY3eDF5aE1YTm5LNWh5Q1BwWTMwM3M5S083MWRWRFlmOXNqSFJNcStGMWFOS3c2emhiOUNNenZYTmNNMnZxaUErdlRoOGF2ZHdGT1ZLNTNLOVJQcU9jWmJrR3g5N09VcVBRZ0ZnSFNUQy9HdFVWWnFteVhRVkY3MHNBdnF2ZWFEbFBseWRGelE1c3BFTnVUckRQdWJSL29wNjFxd2Y2ZVpob3VyVzRod3FKTElTenJ1WTZacTJFOFBtK3BnVzh0QWVxcWtyWFdXOXYyenNQeFNZbWt1MDU2Vm1kVGtISWIxWTBpcWRFbyswUVJudVVhZkVlNVpGWDA4WUQ2Q2JTWW81SnlhQ2Zxa3cxNmZoQjJES0Uyd29rNXpSck5iWVBrVmEwOXFya1NpMi9saU5LL3F0M3N3MjZKekNCem9ER2svWkZ0SUJLdmlHRno0VlQzQ3pqZTBWcTM3YmRiNmJjTkhqUHZoQ0NxMW1ldW1XOFVVK3pQMEtUMkRMVGNvNHFlOG40ck5KcGhsYXg1b1VzZ1NYU1B2T3RXdEkwYzg4NWE3YWUzOWI1MDI0MThhMWZjODQ3MDA2OTJmNDQ0MDkxNGFiNmRlMGQ2YjZiNjI5NGMwN2IwMmI4MGZi'
1226"""
1227A sample vault native configuration, in v0.2 format, encoded in base64
1228and encrypted with [`VAULT_MASTER_KEY`][].
1229"""
1230VAULT_V02_CONFIG_DATA = {
1231 'global': {
1232 'phrase': DUMMY_PASSPHRASE.rstrip('\n'),
1233 },
1234 'services': {
1235 '(meta)': {
1236 'notes': 'This config was originally in v0.2 format.',
1237 },
1238 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1239 },
1240}
1241"""
1242The plaintext contents (a vault native configuration) stored in
1243[`VAULT_V02_CONFIG`][].
1244"""
1245VAULT_V03_CONFIG = 'sBPBrr8BFHPxSJkV/A53zk9zwDQHFxLe6UIusCVvzFQre103pcj5xxmE11lMTA0U2QTYjkhRXKkH5WegSmYpAnzReuRsYZlWWp6N4kkubf+twZ9C3EeggPm7as2Af4TICHVbX4uXpIHeQJf9y1OtqrO+SRBrgPBzgItoxsIxebxVKgyvh1CZQOSkn7BIzt9xKhDng3ubS4hQ91fB0QCumlldTbUl8tj4Xs5JbvsSlUMxRlVzZ0OgAOrSsoWELXmsp6zXFa9K6wIuZa4wQuMLQFHiA64JO1CR3I+rviWCeMlbTOuJNx6vMB5zotKJqA2hIUpN467TQ9vI4g/QTo40m5LT2EQKbIdTvBQAzcV4lOcpr5Lqt4LHED5mKvm/4YfpuuT3I3XCdWfdG5SB7ciiB4Go+xQdddy3zZMiwm1fEwIB8XjFf2cxoJdccLQ2yxf+9diedBP04EsMHrvxKDhQ7/vHl7xF2MMFTDKl3WFd23vvcjpR1JgNAKYprG/e1p/7'
1246"""
1247A sample vault native configuration, in v0.3 format, encoded in base64
1248and encrypted with [`VAULT_MASTER_KEY`][].
1249"""
1250VAULT_V03_CONFIG_DATA = {
1251 'global': {
1252 'phrase': DUMMY_PASSPHRASE.rstrip('\n'),
1253 },
1254 'services': {
1255 '(meta)': {
1256 'notes': 'This config was originally in v0.3 format.',
1257 },
1258 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1259 },
1260}
1261"""
1262The plaintext contents (a vault native configuration) stored in
1263[`VAULT_V03_CONFIG`][].
1264"""
1265VAULT_STOREROOM_CONFIG_ZIPPED = b"""
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
1296dnAibw1brWVXM++8iYcUg84TMbJXntFYCyrNw1NF+008I02PeH4C8oDID6fIoKvsw3p7WJJ/I9Yp
1297a6oJzlJiP5JGxRxZPj50N6EMtzNB+tZoIGxgtOFVpiJ05yMQFztY6I6LKIgvXW/s919GIjGshqdM
1298XVPFxaKG4p9Iux/xazf48FY8O7SMmbQC1VsXIYo+7eSpIY67VzrCoh41wXPklOWS6CV8RR/JBSqq
12998lHkcz8L21lMCOrVR1Cs0ls4HLIhUkqr9YegTJ67VM7xevUsgOI7BkPDldiulRgX+sdPheCyCacu
1300e7/b/nk0SXWF7ZBxsR1awYqwkFKz41/1bZDsETsmd8n1DHycGIvRULv3yYhKcvWQ4asAMhP1ks5k
1301AgOcrM+JFvpYA86Ja8HCqCg8LihEI1e7+m8F71Lpavv/UEsDBBQAAAAIAJ1WGVnKO2Ji+AEAAGsC
1302AAACAAAAMWENx7dyo0AAANDen+GWAonMzbggLsJakgGBOhBLlGBZsjz373eve7+fKyJTM/Sff85/
1303P5QMwMFfAWipfXwvFPWU582cd3t7JVV5pBV0Y1clL4eKUd0w1m1M5JrkgW5PlfpOVedgABSe4zPY
1304LnSIZVuen5Eua9QY8lQ7rxW7YIqeajhgLfL54BIcY90fd8ANixlcM8V23Z03U35Txba0BbSguc0f
1305NRF83cWp+7rOYgNO9wWLs915oQmWAqAtqRYCiWlgAtxYFg0MnNS4/G80FvFmQTh0cjwcF1xEVPeW
1306l72ky84PEA0QMgRtQW+HXWtE0/vQTtNKzvNqPfrGZCldL5nk9PWhhPEQ/azyW11bz2eB+aM0g0r7
13070/5YkO9er10YonsBT1rEn0lfBXDHwtwbxG2bdqELTuEtX2+OEih7K43rN2EvpXX47azaNpe/drIz
1308wgAdhpfZ/mZwaGFX0c7r5HCTnroNRi5Bx/vu7m1A7Nt1dix4Gl/aPLCWQzpwmdIMJDiqD1RGpc5v
1309+pDLrpfhZOVhLjAPSQ0V7mm/XNSca8oIsDjwdvR438RQCU56mrlypklS4/tJAe0JZNZIgBmJszjG
1310AFbsmNYTJ9GmULB9lXmTWmrME592S285iWU5SsJcE1s+3oQw9QrvWB+e3bGAd9e+VFmFqr6+/gFQ
1311SwECHgMUAAAACACdVhlZ01RRk9IAAADmAAAABQAAAAAAAAABAAAApIEAAAAALmtleXNQSwECHgMU
1312AAAACACdVhlZ3Wlf4QUDAADUAwAAAgAAAAAAAAABAAAApIH1AAAAMDBQSwECHgMUAAAACACdVhlZ
1313/aaiwcoBAAArAgAAAgAAAAAAAAABAAAApIEaBAAAMDNQSwECHgMUAAAACACdVhlZs14oCcgBAAAr
1314AgAAAgAAAAAAAAABAAAApIEEBgAAMDlQSwECHgMUAAAACACdVhlZyjtiYvgBAABrAgAAAgAAAAAA
1315AAABAAAApIHsBwAAMWFQSwUGAAAAAAUABQDzAAAABAoAAAAA
1316"""
1317"""
1318A sample vault native configuration, in storeroom format, encrypted with
1319[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)
1320and then encoded in base64.
1321"""
1322VAULT_STOREROOM_CONFIG_DATA = {
1323 'global': {
1324 'phrase': DUMMY_PASSPHRASE.rstrip('\n'),
1325 },
1326 'services': {
1327 '(meta)': {
1328 'notes': 'This config was originally in storeroom format.',
1329 },
1330 DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),
1331 },
1332}
1333"""
1334The parsed vault configuration stored in
1335[`VAULT_STOREROOM_CONFIG_ZIPPED`][].
1336"""
1338VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE = """
1339// Executed in the top-level directory of the vault project code, in Node.js.
1340const storeroom = require('storeroom')
1341const Store = require('./lib/store.js')
1342let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1343await store._storeroom.put('/services/array/', ['entry1','entry2'])
1344// The resulting "broken-dir" was then zipped manually.
1345"""
1346"""
1347The JavaScript source for the script that generated the storeroom
1348archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED`][].
1349"""
1350VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED = b"""
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
1385"""
1386"""
1387A sample corrupted storeroom archive, encrypted with
1388[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)
1389and then encoded in base64.
1391The archive contains a directory `/services/array/` that claims to have
1392two child items 'entry1' and 'entry2', but no such child items are
1393present in the archive. See
1394[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED_JAVASCRIPT_SOURCE`][] for
1395the exact script that created this archive.
1396"""
1398VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE = """
1399// Executed in the top-level directory of the vault project code, in Node.js.
1400const storeroom = require('storeroom')
1401const Store = require('./lib/store.js')
1402let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1403await store._storeroom.put('/services/array/', 'not a directory index')
1404// The resulting "broken-dir" was then zipped manually.
1405"""
1406"""
1407The JavaScript source for the script that generated the storeroom
1408archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2`][].
1409"""
1410VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2 = b"""
1411UEsDBAoAAAAAAM6NSVmrcHdV5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV3ZS9LZkJp
1412L0V0OUcrZmxYM3gxaFU4ZjE4YlE3S253bHoxN0IxSDE3cUhVOGdWK2RpWWY5MTdFZ0YrSStidEpZ
1413VXBzWVZVck45OC9uLzdsZnl2NUdGVEg2NWZxVy93YjlOc2MxeEZ4ck43Q3p4eTZ5MVAxZzFPb2VK
1414b0RZU3J6YXlwT0E2M3pidmk0ZTRiREMyNXhPTXl5NHBoMDFGeGdnQmpSNnpUcmR2UDk2UlZQd0I5
1415WitOZkZWZUlXT1NQN254ZFNYMGdFbkZ4SDBmWDkzNTFaTTZnPVBLAwQKAAAAAADOjUlZJg3/BhcC
1416AAAXAgAAAgAAADBieyJ2ZXJzaW9uIjoxfQpBVXJJMjNDQ2VpcW14cUZRMlV4SUpBaUoxNEtyUzh2
1417SXpIa2xROURBaFRlVHNFMmxPVUg4WUhTcUk1cXRGSHBqY3c1WkRkZmRtUlEwQXVGRjllY3lkam14
1418dDdUemRYLzNmNFUvTGlVV2dLRmQ1K1FEN3BlVlE1bWpqeHNlUEpHTDlhTWlKaGxSUVB4SmtUbjBx
1419U2poM1RUT0ZZbVAzV0JkdlUyWnF2RzhaSDk2cU1WcnZsQ0dMRmZTc2svVXlvcHZKdENONUVXcTRZ
1420SDUwNFNiejFIUVhWd2RjejlrS1BuR3J6SVA4ZmZtZnhXQ0U0TmtLb0ZPQXZuNkZvS3FZdGlGbFE9
1421PQpBVXBMUVMrMG9VeEZTeCtxbTB3SUtyM1MvTVJxYWJJTFlEUnY0aHlBMVE2TGR2Nlk0UmJ0enVz
1422NzRBc0cxbVhhenlRU2hlZVowdk0xM2ZyTFA4YlV0VHBaRyszNXF1eUhLM2NaWVJRZUxKM0JzejZz
1423b0xaQjNZTkpNenFxTTQrdzM1U0FZZ2lMU1NkN05NeWVrTHNhRUIzRDFOajlTRk85K3NGNEpFMWVL
1424UXpNMkltNk9qOUNVQjZUSTV3UitibksxN1BnY2RaeTZUMVRMWElVREVxcDg4dWdsWmRFTVcrNU9k
1425aE5ZbXEzZERWVWV4UnJpM1AwUmVBSi9KMGdJNkNoUUE9PVBLAwQKAAAAAADOjUlZTNfdphcCAAAX
1426AgAAAgAAADBmeyJ2ZXJzaW9uIjoxfQpBWVJqOVpIUktGUEVKOHM2YVY2TkRoTk5jQlZ5cGVYUmdz
1427cnBldFQ0cGhJRGROWFdGYzRia0daYkJxMngwRDFkcVNjYWk5UzEveDZ2K28zRE0rVEF2OVE3ZFVR
1428QWVKR3RmRkhJZDZxWW0ybEdNSnF5WTRNWm14aE9YdXliend0V3Q4Mnhvb041QTZNcWpINmxKQllD
1429UUN3ZEJjb3RER0EwRnlnVTEzeHV2WnIzT1puZnFFRGRqbzMxNkw5aExDN1RxMTYwUHpBOXJOSDMz
1430ZkNBcUhIVXZiYlFQQWErekw1d3dEN3FlWkY2MHdJaEwvRmk5L3JhNGJDcHZRNC9ORWpRd3c9PQpB
1431WWNGUDB1Y2xMMHh3ZDM2UXZXbm4wWXFsOU5WV0s3c05CMTdjdmM3N3VDZ0J2OE9XYkR5UHk5d05h
1432R2NQQzdzcVdZdHpZRlBHR0taVjhVUzA1YTVsV1BabDNGVFNuQXNtekxPelBlcFZxaitleDU3aEsx
1433QnV1bHkrUCtYQkE0YUtsaDM3c0RJL3I0UE1BVlJuMDNoSDJ5dEhDMW9PbjF0V1M5Q1NLV1pSMThh
1434djdTT0RBMVBNRnFYTmZKZVNTaVJiQ2htbDdOcFVLbjlXSGJZandybDlqN0JSdy9kWjhNQldCb3Ns
1435Nlc1dGZtdnJMVHhGRFBXYUgzSUp0T0czMEI1M3c9PVBLAwQKAAAAAADOjUlZn9rNID8CAAA/AgAA
1436AgAAADFkeyJ2ZXJzaW9uIjoxfQpBYWFBb3lqaGljVDZ4eXh1c0U0RVlDZCtxbE81Z0dEYTBNSFVS
1437MmgrSW9QMHV4UkY3b1BRS2czOHlQUEN3Ny9MYVJLQ0dQZ0RyZ2RpTWJTeUwzZ3ZNMFhseVpVMVBW
1438QVJvNEFETU9lbXgrOWhtS0hjQWNKMG5EeW5oSkhGYTYyb2xyQUNxekZzblhKNVBSeEVTVzVEbUh0
1439Ui9nRm5Wa1FvalhyVW4ybmpYMjVVanZQaXhlMU96Y0daMmQ0MjdVTGdnY1hqMkhSdjJiZldDNDUw
1440SGFXS3FDckZlYWlrQ2xkUUM2WGV3SkxZUjdvQUY3UjVha2ttK3M2MXNCRTVCaTg0QmJLWHluc1NG
1441ejE0TXFrd2JMK1VMYVk9CkFUT3dqTUFpa3Q4My9NTW5KRXQ2b3EyNFN4KzJKNDc2K2gyTmEzbHUr
1442MDg0cjlBT25aaUk0TmlYV0N1Q0lzakEzcTBwUHFJS1VXZHlPQW9uM2VHY0huZUppWUtVYllBaUJI
1443MVNmbnhQQkMzZkFMRklybkQ4Y0VqeGpPcUFUaTQ5dE1mRmtib0dNQ3dEdFY0V3NJL0tLUlRCOFd1
1444MnNXK2J0V3QzVWlvZG9ZeUVLTDk3ekNNemZqdGptejF4SDhHTXY5WDVnaG9NSW5RQVNvYlRreVZ4
1445dWo5YnlDazdNbU0vK21ZL3AwZE9oYVY0Nncwcm04UGlvWEtzdzR4bXB3ditDWC9PRXV3Uy9meDJT
1446Y0lOQnNuYVRiWT1QSwECHgMKAAAAAADOjUlZq3B3VeYAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA
1447LmtleXNQSwECHgMKAAAAAADOjUlZJg3/BhcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGJQSwEC
1448HgMKAAAAAADOjUlZTNfdphcCAAAXAgAAAgAAAAAAAAAAAAAApIFAAwAAMGZQSwECHgMKAAAAAADO
1449jUlZn9rNID8CAAA/AgAAAgAAAAAAAAAAAAAApIF3BQAAMWRQSwUGAAAAAAQABADDAAAA1gcAAAAA
1450"""
1451"""
1452A sample corrupted storeroom archive, encrypted with
1453[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)
1454and then encoded in base64.
1456The archive contains a directory `/services/array/` whose list of child
1457items does not adhere to the serialization format. See
1458[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED2_JAVASCRIPT_SOURCE`][] for
1459the exact script that created this archive.
1460"""
1462VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE = """
1463// Executed in the top-level directory of the vault project code, in Node.js.
1464const storeroom = require('storeroom')
1465const Store = require('./lib/store.js')
1466let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1467await store._storeroom.put('/services/array/', [null, 1, true, [], {}])
1468// The resulting "broken-dir" was then zipped manually.
1469"""
1470"""
1471The JavaScript source for the script that generated the storeroom
1472archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3`][].
1473"""
1474VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3 = b"""
1475UEsDBAoAAAAAAEOPSVnVlcff5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV4dVBHUDBi
1476YkxrUVdvWnV5ZUJQRy8xdmM2MCt6MThOa3BsS09ydFAvUTVnQmxkYVpIOG10dTE5VWZFNGdGRGRj
1477eHJtWUd4eXZDZFNqcVlOaDh4cTlzM3VydkdRTWFwcnhtdlZGZUxoSW4zZnVlTDAweEk0ZmlLenZN
1478MmthUlRsNWNORGh3eUNlWVk4dzhBcXNhYjNyVWVsOEE0eVQ0cHU2d2tmQ3dTWUdqeG5HR29EcWJK
1479VnVJVWNpZVBEcU9PTzU2b0MyMG9lT01adFVkTUtxV28zYnFZPVBLAwQKAAAAAABDj0lZ77OVHxcC
1480AAAXAgAAAgAAADBjeyJ2ZXJzaW9uIjoxfQpBZllFQVVobEkyU2lZeGlrdWh0RzRNbUN3L1V2THBN
1481VVhwVlB0NlRwdzRyNGdocVJhbGZWZ0hxUHFtbTczSnltdFFrNnZnR2JRdUpiQmVlYjYwOHNrMGk4
1482ZFJVZjNwdlc2SnUyejljQkdwOG5mTFpTdlNad1lLN09UK2gzSDNDcmoxbXNicEZUcHVldW81NXc1
1483dGdYMnBuWXNWTVcrczdjaHEyMUIya2lIVEZrdGt1MXlaRzhPYkVUQjNCOFNGODVVbi9CUjFEMHJ1
1484ME9zOWl4ZWM2VmNTMitTZndtNnNtSlk2ZW9ZNTJzOGJNRGdYMndjQ0srREdkOEo2VWp0NG5OQVE9
1485PQpBUWlPRnRZcmJybWUycEwxRFpGT1BjU0RHOUN2cVkvbHhTWGIwaVJUdmtIWFc2bEtHL0p4RUtU
1486d3RTc0RTeDhsMTUvaHRmbWpOQ2tuTzhLVEFoKzhRQm5FbjZ0a2x5Y3BmeEIrTUxLRjFCM1Q1bjcv
1487T0VUMExMdmgxU2k1bnRRNXhTUHZZNWtXeUMyZjhXUXFZb3FSNU5JVENMeDV6dWNsQ3dGb2kvVXc4
1488OWNNWjM1MHBSbThzUktJbjJFeDUrQ1JwS3ZHdnBHbFJaTmk5VHZmVkNic1FCalR3MC9aeklTdzVQ
1489NW9BVWE2U1ExUVFnNHg4VUNkY0s2QUNLaFluY0d4TVE9PVBLAwQKAAAAAABDj0lZGk9LVj8CAAA/
1490AgAAAgAAADE0eyJ2ZXJzaW9uIjoxfQpBY1g2NVpMUWk4ck9pUlIyWGEwQlFHQVhQVWF2aHNJVGVY
1491c2dzRk9OUmFTRzJCQlg0SGxJRHpwRUd5aDUrZ2czZVRwWDFNOERua3pMeTVzcWRkMFpmK3padTgz
1492Qm52Y1JPREVIVDllUW91YUtPTWltdlRYanNuSXAxUHo5VGY1TlRkRjNJVTd2V1lhUDg4WTI5NG1i
1493c1VVL2RKVTZqZ3ZDbUw2cE1VZ28xUU12bGJnaVp3cDV1RDFQZXlrSXdKVWdJSEgxTEpnYi9xU2tW
1494c25leW1XY1RXR0NobzRvZGx3S2hJWmFCelhvNFhlN2U1V2I2VHA3Rkk5VUpVcmZIRTAvcVdrZUZE
1495VmxlazY3cUx3ZFZXcU9DdFk9CkFhSGR0QjhydmQ0U3N4ZmJ5eU1OOHIzZEoxeHA5NmFIRTQvalNi
1496Z05hZWttaDkyb2ROM1F4MUlqYXZsYVkxeEt1eFF3KzlwTHFIcTF5a1JSRjQzL2RVWGFIRk5UU0NX
1497OVFsdmd3KzMwa1ZhSEdXRllvbFRnRWE4djQ3b3VrbGlmc01PZGM0YVNKb2R4ZUFJcVc3Q1cwdDVR
1498b2RUbWREUXpqc3phZkQ4R2VOd2NFQjdGMHI2RzNoZEJlQndxd3Z6eENVYnpSUmU5bEQ3NjQ3RFp1
1499bEo1U3c4amlvV0paTW40NlZhV3BYUXk4UnNva3hHaW00WUpybUZIQ2JkVU9qSWJsUmQ1Z3VhUDNU
1500M0NxeHRPdC94b1BhOD1QSwMECgAAAAAAQ49JWVJM8QYXAgAAFwIAAAIAAAAxNnsidmVyc2lvbiI6
1501MX0KQVlCWDF6M21qUlQrand4M2FyNkFpemxnalJZbUM0ZHg5NkxVQVBTVHNMWXJKVHFtWnd5N0Jy
1502OFlCcElVamorMHdlT3lNaUtLVnFwaER3RXExNWFqUmlSZUVEQURTVHZwWmlLZUlnZjR5elUzZXNP
1503eDJ2U2J1bXhTK0swUGZVa2tsSy9TRmRiU3EvUHFMRjBDRTVCMXNyKzJLYTB2WlJmak94R3VFeFRD
1504RXozN0ZlWDNNR3NCNkhZVHEzaUJWcUR6NVB6eHpCWWM5Kyt6RitLS1RnMVp2NGRtRmVQTC9JSEY5
1505WnV6TWlqRXdCRkE3WnJ0dkRqd3ZYcWtsMVpsR0c4eUV3PT0KQVhUWkRLVnNleldpR1RMUVZqa2hX
1506bXBnK05MYlM0M2MxZEpvK2xGcC9yWUJYZkw3Wll5cGdjWE5IWXNzd01nc2VSSTAzNmt6bGZkdGNa
1507bTdiUUN6M2JuQmZ6ZlorZFFuT2Y5STVSU2l0QzB2UmsydkQrOFdwbmRPSzNucGY5S0VpWklOSzVq
1508TEZGTTJDTkNmQzBabXNRUlF3T0k2N3l5ZHhjVnFDMXBnWHV6QXRXamlsSUpnN0p6eUtsY3BJUGJu
1509SUc0UzRSUlhIdW1wZnpoeWFZWkd6T0FDamRSYTZIMWJxYkJkZXFaSHMvQXJvM25mVjdlbjhxSUE5
1510aVUrbnNweXFnPT1QSwECHgMKAAAAAABDj0lZ1ZXH3+YAAADmAAAABQAAAAAAAAAAAAAApIEAAAAA
1511LmtleXNQSwECHgMKAAAAAABDj0lZ77OVHxcCAAAXAgAAAgAAAAAAAAAAAAAApIEJAQAAMGNQSwEC
1512HgMKAAAAAABDj0lZGk9LVj8CAAA/AgAAAgAAAAAAAAAAAAAApIFAAwAAMTRQSwECHgMKAAAAAABD
1513j0lZUkzxBhcCAAAXAgAAAgAAAAAAAAAAAAAApIGfBQAAMTZQSwUGAAAAAAQABADDAAAA1gcAAAAA
1514"""
1515"""
1516A sample corrupted storeroom archive, encrypted with
1517[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)
1518and then encoded in base64.
1520The archive contains a directory `/services/array/` whose list of child
1521items are not all valid item names. See
1522[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED3_JAVASCRIPT_SOURCE`][] for
1523the exact script that created this archive.
1524"""
1526VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE = """
1527// Executed in the top-level directory of the vault project code, in Node.js.
1528const storeroom = require('storeroom')
1529const Store = require('./lib/store.js')
1530let store = new Store(storeroom.createFileAdapter('./broken-dir'), 'vault key')
1531await store._storeroom.put('/dir/subdir/', [])
1532await store._storeroom.put('/dir/', [])
1533// The resulting "broken-dir" was then zipped manually.
1534"""
1535"""
1536The JavaScript source for the script that generated the storeroom
1537archive in [`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4`][].
1538"""
1539VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4 = b"""
1540UEsDBAoAAAAAAE+5SVloORS+5gAAAOYAAAAFAAAALmtleXN7InZlcnNpb24iOjF9CkV6dWRoNkRQ
1541YTlNSWFabHZ5TytVYTFuamhjV2hIaTFBU0lKYW5zcXBxVlA0blN2V0twUzdZOUc2bjFSbi8vUnVM
1542VitwcHp5SC9RQk83R0hFenNVMzdCUzFwUmVVeGhxUVlVTE56OXZvQ0crM1ZaL3VncU44dDJiU05m
1543Nyt5K3hiNng2aVlFUmNZYTJ0UkhzZVdIc0laTE9ha2lDb0lRVGV3cndwYjVMM2pnd0E3SXBzaDkz
1544QkxHSzM5dXNYNmo0R0I2WkRUeW5JcGk4V3JkbDhnWVZCN0tVPVBLAwQKAAAAAABPuUlZ663uUhcC
1545AAAXAgAAAgAAADAzeyJ2ZXJzaW9uIjoxfQpBV2wzS2gzd21ZSFVZZU1RR3BLSVowdVd1VXFna09h
1546YmRjNzNYYXVsZTNtVS9sN2Zvd1AyS21jbFp3ZDM5V3lYVzRTcEw4R0l4YStDZW51S3V0Wm5nb0FR
1547bWlnaUJUbkFaais5TENCcGNIWlZNY2RBVkgxKzBFNGpsanZ1UkVwZ0tPS05LZjRsTUl1QnZ4VmFB
1548ZkdwNHJYNEZ4MmpPSlk1Y3NQZzBBRFBoZVAwN29GWVQ3alorSUNEK1AxNGZPdWpwMGRUeDRrTDIy
1549LzlqalRDNXBCNVF5NW5iOUx3Zk5DUWViSUVpaTZpbU0vRmFrK1dtV05tMndqMERSTEc4RHY3ZkE9
1550PQpBU0c3NTNGTVVwWmxjK3E1YXRzcC93OUNqN2JPOFlpY24wZHg2UGloTmwzUS9WSjVVeGJmU3l0
1551ZDFDNDBRU2xXeTJqOTJDWUd3VER6eEdBMXVnb0FCYi9kTllTelVwbHJFb3BuUVphYXdsdTVwV2x0
1552Y1E5WTcveWN4S2E4b0JaaGY3RkFYcGo2c01wUW9zNzI5VFVabFd4UmI4VFRtN2FrVnR1OXcvYXlK
1553RS9reDh4ZUYxSGJlc3Q4N1IxTGg2ODd3dS9XVUN2ZjNXYXo1VjNnZWY0RnpUTXg0bkpqSlZOd0U0
1554SzAxUTlaVzQ0bmVvbExPUVI1MkZDeDZvbml3RW9tenc9PVBLAwQKAAAAAABPuUlZRXky4CsCAAAr
1555AgAAAgAAADEweyJ2ZXJzaW9uIjoxfQpBWmlYWVlvNUdCY2d5dkFRaGtyK2ZjUkdVSkdabDd2dE5w
1556T2Mrd1VzbXJhQWhRN3dKdlYraGhKcTlrcWNKQnBWU0gyUTBTTVVhb29iNjBJM1NYNUNtTkJRU2FH
1557M3prd0Y0T2F4TnpCZUh0NFlpaDd4Y3p2ak4xR0hISDJQYW0xam05K09ja3JLVmNMVURtNXRKb2ZC
1558Z1E4Q2NwMGZMVkdEaURjNWF0MjVMc2piQVcvNkZFSnJ5VVBHWis4UVdYRmlWMGdtVVZybVc3VUFy
1559dGhJQitWNTdZS1BORi95Nng2OU43UTFQbmp1cUczdlpybzljMEJ3d012NWoyc3BMMTJHcTdzTDZE
1560alB1d0dHbnB2MkVZQTFLbmc9CkFTdjQwUkgzRmxzbGVlU1NjRlZNRmh3dEx6eEYxK2xpcmxEL29X
1561alJLQ05qVWZhUVpJTWpqMWRoVkhOakNUTWhWZ1ZONkl3b04xTnFOMEV6cmdhaTFBWnNiMm9UczYw
1562QkI1UGh0U0hhQ2U2WllUeE1JemFPS2FIK0w2eHhtaXIrTlQxNTRXS0x5amJMams3MU1na3Nwa0Yy
1563WDBJMnlaWW5IUUM0bmdEL24yZzRtSVI2Q1hWL0JOUXNzeTBEeXdGLzN6eGRRYWw5cFBtVk1qYnFu
1564cHY5SFNqRTg4S25naVpBWFhJWU1OVGF2L3Q3Y3dEWGdNekhKTlU0Y2xnVUtIQVZ3QT09UEsDBAoA
1565AAAAAE+5SVkPfKx9FwIAABcCAAACAAAAMWR7InZlcnNpb24iOjF9CkFYbHNLRzQwZG5ibTJvcXdY
1566U2ZrSWp3Mmxpa0lDS3hVOXU3TU52VkZ1NEJ2R1FVVitSVVdsS3MxL25TSlBtM2U2OTRvVHdoeDFo
1567RFF3U0M5U0QvbXd5bnpjSTloUnRCUWVXMkVMOVU5L1ZGcHFsVWY3Z1ZOMHZ0ZWpXYnV4QnhsZlRD
1568Tys4SFBwU2Zaa2VOUld5R2JNdzBFSU9LTmxRYjk3OUF0c1g3THR0NytaTkJnakZHYkZxaHdwa3kx
1569WUNDVng1UmNZZ2tma2ZjWnVncGpzc1RzNVFvK1p3QXBEcDZ4V3JjSHMxUDhvNktBRzAwcjZZbkNM
1570N2ErU1dwZmVNTUJhZz09CkFadVF0cFZMWmVvb292NkdyQlpnb3B6VmRGUXBlK1h6QXZuZ2dPVnZM
1571VWtCYVF2akl5K1VLdXVUVlFoQ1JiMVp6dGZQL2dsNnoxOEsyZW5sQlo2bGJTZnoxTlBWeUVzYXB3
1572dDVpUVh4azd5UkJlZks1cFlsNTduUXlmcFZQbzlreFpnOVdHTkV3NVJ5MkExemhnNGl6TWxLRmJh
1573UjZFZ0FjQ3NFOXAveGRLa29ZNjhOUlZmNXJDM3lMQjc3ZWgyS1hCUld2WDNZcE9XdW00OGtsbmtI
1574akJjMFpiQmUrT3NZb3d5cXpoRFA2ZGQxRlFnMlFjK09vc3B4V0sycld4M01HZz09UEsBAh4DCgAA
1575AAAAT7lJWWg5FL7mAAAA5gAAAAUAAAAAAAAAAAAAAKSBAAAAAC5rZXlzUEsBAh4DCgAAAAAAT7lJ
1576Weut7lIXAgAAFwIAAAIAAAAAAAAAAAAAAKSBCQEAADAzUEsBAh4DCgAAAAAAT7lJWUV5MuArAgAA
1577KwIAAAIAAAAAAAAAAAAAAKSBQAMAADEwUEsBAh4DCgAAAAAAT7lJWQ98rH0XAgAAFwIAAAIAAAAA
1578AAAAAAAAAKSBiwUAADFkUEsFBgAAAAAEAAQAwwAAAMIHAAAAAA==
1579"""
1580"""
1581A sample corrupted storeroom archive, encrypted with
1582[`VAULT_MASTER_KEY`][]. The configuration is compressed (zip archive)
1583and then encoded in base64.
1585The archive contains two directories `/dir/` and `/dir/subdir/`, where
1586`/dir/subdir/` is a correctly serialized directory, but `/dir/` does not
1587contain `/dir/subdir/` in its list of child items. See
1588[`VAULT_STOREROOM_BROKEN_DIR_CONFIG_ZIPPED4_JAVASCRIPT_SOURCE`][] for
1589the exact script that created this archive.
1590"""
1592CANNOT_LOAD_CRYPTOGRAPHY = (
1593 "Cannot load the required Python module 'cryptography'."
1594)
1595"""
1596The expected `derivepassphrase` error message when the `cryptography`
1597module cannot be loaded, which is needed e.g. by the `export vault`
1598subcommands.
1599"""
1601skip_if_cryptography_support = pytest.mark.skipif(
1602 importlib.util.find_spec('cryptography') is not None,
1603 reason='cryptography support available; cannot test "no support" scenario',
1604)
1605"""
1606A cached pytest mark to skip this test if cryptography support is
1607available. Usually this means that the test targets
1608`derivepassphrase`'s fallback functionality, which is not available
1609whenever the primary functionality is.
1610"""
1611skip_if_no_cryptography_support = pytest.mark.skipif(
1612 importlib.util.find_spec('cryptography') is None,
1613 reason='no "cryptography" support',
1614)
1615"""
1616A cached pytest mark to skip this test if cryptography support is not
1617available. Usually this means that the test targets the
1618`derivepassphrase export vault` subcommand, whose functionality depends
1619on cryptography support being available.
1620"""
1621skip_if_on_the_annoying_os = pytest.mark.skipif(
1622 sys.platform == 'win32',
1623 reason='The Annoying OS behaves differently.',
1624)
1625"""
1626A cached pytest mark to skip this test if running on The Annoying
1627Operating System, a.k.a. Microsoft Windows. Usually this is due to
1628unnecessary and stupid differences in the OS internals, and these
1629differences are deemed irreconcilable in the context of the decorated
1630test, so the test is to be skipped.
1632See also:
1633 [`xfail_on_the_annoying_os`][]
1635"""
1636skip_if_no_multiprocessing_support = pytest.mark.skipif(
1637 importlib.util.find_spec('multiprocessing') is None,
1638 reason='no "multiprocessing" support',
1639)
1640"""
1641A cached pytest mark to skip this test if multiprocessing support is not
1642available. Usually this means that the test targets the concurrency
1643features of `derivepassphrase`, which is generally only possible to test
1644in separate processes because the testing machinery operates on
1645process-global state.
1646"""
1648MIN_CONCURRENCY = 4
1649"""
1650The minimum amount of concurrent threads used for testing.
1651"""
1654def get_concurrency_limit() -> int:
1655 """Return the imposed limit on the number of concurrent threads.
1657 We use [`os.process_cpu_count`][] as the limit on Python 3.13 and
1658 higher, and [`os.cpu_count`][] on Python 3.12 and below. On
1659 Python 3.12 and below, we explicitly support the `PYTHON_CPU_COUNT`
1660 environment variable. We guarantee at least [`MIN_CONCURRENCY`][]
1661 many threads in any case.
1663 """ # noqa: RUF002
1664 result: int | None = None 1ae
1665 if sys.version_info >= (3, 13): 1ae
1666 result = os.process_cpu_count() 1ae
1667 else:
1668 with contextlib.suppress(KeyError, ValueError): 1ae
1669 result = result or int(os.environ['PYTHON_CPU_COUNT'], 10) 1ae
1670 with contextlib.suppress(AttributeError): 1ae
1671 result = result or len(os.sched_getaffinity(os.getpid())) 1ae
1672 return max(result if result is not None else 0, MIN_CONCURRENCY) 1ae
1675def get_concurrency_step_count(
1676 settings: hypothesis.settings | None = None,
1677) -> int:
1678 """Return the desired step count for concurrency-related tests.
1680 This is the smaller of the [general concurrency
1681 limit][tests.get_concurrency_limit] and the step count from the
1682 current hypothesis settings.
1684 Args:
1685 settings:
1686 The hypothesis settings for a specific tests. If not given,
1687 then the current profile will be queried directly.
1689 """
1690 if settings is None: # pragma: no cover 1ae
1691 settings = hypothesis.settings()
1692 return min(get_concurrency_limit(), settings.stateful_step_count) 1ae
1695def xfail_on_the_annoying_os(
1696 f: Callable | None = None,
1697 /,
1698 *,
1699 reason: str = '',
1700) -> pytest.MarkDecorator | Any: # pragma: no cover
1701 """Annotate a test which fails on The Annoying OS.
1703 Annotate a test to indicate that it fails on The Annoying Operating
1704 System, a.k.a. Microsoft Windows. Usually this is due to
1705 differences in the design of OS internals, and usually, these
1706 differences are both unnecessary and stupid.
1708 Args:
1709 f:
1710 A callable to decorate. If not given, return the pytest
1711 mark directly.
1712 reason:
1713 An optional, more detailed reason stating why this test
1714 fails on The Annoying OS.
1716 Returns:
1717 The callable, marked as an expected failure on the Annoying OS,
1718 or alternatively a suitable pytest mark if no callable was
1719 passed. The reason will begin with the phrase "The Annoying OS
1720 behaves differently.", and the optional detailed reason, if not
1721 empty, will follow.
1723 """
1724 base_reason = 'The Annoying OS behaves differently.'
1725 full_reason = base_reason if not reason else f'{base_reason} {reason}'
1726 mark = pytest.mark.xfail(
1727 sys.platform == 'win32',
1728 reason=full_reason,
1729 raises=(AssertionError, hypothesis.errors.FailedHealthCheck),
1730 strict=True,
1731 )
1732 return mark if f is None else mark(f)
1735@socketprovider.SocketProvider.register('fake')
1736class FakeSSHAgentSocket:
1737 """A faked SSH agent presenting an [`_types.SSHAgentSocket`][]."""
1739 _SOCKET_IS_CLOSED = 'Socket is closed.'
1740 _NO_FLAG_SUPPORT = 'This fake SSH agent socket does not support flags.'
1741 _PROTOCOL_VIOLATION = 'SSH agent protocol violation.'
1742 _INVALID_REQUEST = 'Invalid request.'
1743 _UNSUPPORTED_REQUEST = 'Unsupported request.'
1745 HEADER_SIZE = 4
1746 CODE_SIZE = 1
1748 def __init__(self) -> None:
1749 """Initialize the agent."""
1750 self.send_to_client = bytearray() 2a k f b g q h i j D v JbPbQbRbY Lb_ | - TbMbSb6 L Gb
1751 self.receive_from_client = bytearray() 2a k f b g q h i j D v JbPbQbRbY Lb_ | - TbMbSb6 L Gb
1752 self.closed = False 2a k f b g q h i j D v JbPbQbRbY Lb_ | - TbMbSb6 L Gb
1754 def __enter__(self) -> Self:
1755 """Return self."""
1756 return self 2a k f b g h i j D v Y Lb_ | - TbMbSb6 L
1758 def __exit__(self, *args: object) -> None:
1759 """Mark the agent's socket as closed."""
1760 self.closed = True 2a k f b g h i j D v Y Lb_ | - TbMbSb6 L
1762 def sendall(self, data: Buffer, flags: int = 0, /) -> None:
1763 """Send data to the SSH agent.
1765 The signature, and behavior, is identical to
1766 [`socket.socket.sendall`][]. Upon successful sending, this
1767 agent will parse the request, call the appropriate handler, and
1768 buffer the result such that it can be read via [`recv`][], in
1769 accordance with the SSH agent protocol.
1771 Args:
1772 data: Binary data to send to the agent.
1773 flags: Reserved. Must be 0.
1775 Returns:
1776 Nothing. The result should be requested via [`recv`][], and
1777 interpreted in accordance with the SSH agent protocol.
1779 Raises:
1780 AssertionError:
1781 The flags argument, if specified, must be 0.
1782 ValueError:
1783 The agent's socket is already closed. No further
1784 requests can be sent.
1786 """
1787 assert not flags, self._NO_FLAG_SUPPORT 2a b D v 7 Bbo Y _ | - Mb6 L
1788 if self.closed: 2a b D v 7 Bbo Y _ | - Mb6 L
1789 raise ValueError(self._SOCKET_IS_CLOSED) 2Mb
1790 self.receive_from_client.extend(memoryview(data)) 2a b D v 7 Bbo Y _ | - 6 L
1791 try: 2a b D v 7 Bbo Y _ | - 6 L
1792 self.parse_client_request_and_dispatch() 2a b D v 7 Bbo Y _ | - 6 L
1793 except ValueError: 1a6L
1794 payload = int.to_bytes(_types.SSH_AGENT.FAILURE.value, 1, 'big') 1a6L
1795 self.send_to_client.extend(int.to_bytes(len(payload), 4, 'big')) 1a6L
1796 self.send_to_client.extend(payload) 1a6L
1797 finally:
1798 self.receive_from_client.clear() 2a b D v 7 Bbo Y _ | - 6 L
1800 def recv(self, count: int, flags: int = 0, /) -> bytes:
1801 """Read data from the SSH agent.
1803 As per the SSH agent protocol, data is only available to be read
1804 immediately after a request via [`sendall`][]. Calls to
1805 [`recv`][] at other points in time that attempt to read data
1806 violate the protocol, and will fail. Notwithstanding the last
1807 sentence, at any point in time, though pointless, it is
1808 additionally permissible to read 0 bytes from the agent, or any
1809 number of bytes from a closed socket.
1811 Args:
1812 count:
1813 Number of bytes to read from the agent.
1814 flags:
1815 Reserved. Must be 0.
1817 Returns:
1818 (A chunk of) the SSH agent's response to the most recent
1819 request. If reading 0 bytes, or if reading from a closed
1820 socket, the returned chunk is always an empty byte string.
1822 Raises:
1823 AssertionError:
1824 The flags argument, if specified, must be 0.
1826 Alternatively, `recv` was called when there was no
1827 response to be obtained, in violation of the SSH agent
1828 protocol.
1830 """
1831 assert not flags, self._NO_FLAG_SUPPORT 2a b D v 7 Bbo Y _ | - MbSb6 L
1832 assert not count or self.closed or self.send_to_client, ( 2a b D v 7 Bbo Y _ | - MbSb6 L
1833 self._PROTOCOL_VIOLATION
1834 )
1835 ret = bytes(self.send_to_client[:count]) 2a b D v 7 Bbo Y _ | - Mb6 L
1836 del self.send_to_client[:count] 2a b D v 7 Bbo Y _ | - Mb6 L
1837 return ret 2a b D v 7 Bbo Y _ | - Mb6 L
1839 def parse_client_request_and_dispatch(self) -> None:
1840 """Parse the client request and call the matching handler.
1842 This agent supports the
1843 [`SSH_AGENTC_REQUEST_IDENTITIES`][_types.SSH_AGENTC.REQUEST_IDENTITIES],
1844 [`SSH_AGENTC_SIGN_REQUEST`][_types.SSH_AGENTC.SIGN_REQUEST] and
1845 the [`SSH_AGENTC_EXTENSION`][_types.SSH_AGENTC.EXTENSION]
1846 request types.
1848 """
1849 def as_extension(name: str, payload: Buffer = b'') -> bytes: 2a b D v 7 Bbo Y _ | - 6 L
1850 string = ssh_agent.SSHAgentClient.string 1bDo_6
1851 return string( 1bDo_6
1852 b'\x1b' + string(name.encode('ascii')) + bytes(payload)
1853 )
1855 if len(self.receive_from_client) < self.HEADER_SIZE + self.CODE_SIZE: 2a b D v 7 Bbo Y _ | - 6 L
1856 raise ValueError(self._INVALID_REQUEST) 16
1857 target_header = ssh_agent.SSHAgentClient.uint32( 2a b D v 7 Bbo Y _ | - 6 L
1858 len(self.receive_from_client) - self.HEADER_SIZE
1859 )
1860 if target_header != self.receive_from_client[: self.HEADER_SIZE]: 2a b D v 7 Bbo Y _ | - 6 L
1861 raise ValueError(self._INVALID_REQUEST) 16
1862 code = _types.SSH_AGENTC( 2a b D v 7 Bbo Y _ | - 6 L
1863 int.from_bytes(
1864 self.receive_from_client[
1865 self.HEADER_SIZE : self.HEADER_SIZE + self.CODE_SIZE
1866 ],
1867 'big',
1868 )
1869 )
1871 result: Buffer | Iterator[int]
1872 if code == _types.SSH_AGENTC.REQUEST_IDENTITIES: 2a b D v 7 Bbo Y _ | - 6 L
1873 result = self.request_identities(list_extended=False) 2a v 7 BbY |
1874 elif code == _types.SSH_AGENTC.SIGN_REQUEST: 1abD7o_-6L
1875 result = self.sign() 17-6L
1876 elif code == _types.SSH_AGENTC.EXTENSION and bytes( 1abDo_6
1877 self.receive_from_client
1878 ) == as_extension('query'):
1879 result = self.query_extensions() 1bDo_
1880 elif code == _types.SSH_AGENTC.EXTENSION and bytes( 1880 ↛ 1883line 1880 didn't jump to line 1883 because the condition on line 1880 was never true1a6
1881 self.receive_from_client
1882 ) == as_extension('list-extended@putty.projects.tartarus.org'):
1883 result = self.request_identities(list_extended=True)
1884 else:
1885 raise ValueError(self._UNSUPPORTED_REQUEST) 1a6
1886 self.send_to_client.extend( 2a b D v 7 Bbo Y _ | -
1887 ssh_agent.SSHAgentClient.string(bytes(result))
1888 )
1890 def query_extensions(self) -> Iterator[int]:
1891 """Answer an `SSH_AGENTC_EXTENSION` request.
1893 Yields:
1894 The bytes payload of the response, without the protocol
1895 framing. The payload is yielded byte by byte, as an
1896 iterable of 8-bit integers.
1898 """
1899 yield from b'\x1d' 1bDo_
1900 yield from ssh_agent.SSHAgentClient.string(b'query') 1bDo_
1901 extension_answers = [ 1bDo_
1902 b'query',
1903 b'list-extended@putty.projects.tartarus.org',
1904 ]
1905 for a in extension_answers: 1bDo_
1906 yield from ssh_agent.SSHAgentClient.string(a) 1bDo_
1908 def request_identities(self, *, list_extended: bool = False) -> Iterator[int]:
1909 """Answer an `SSH_AGENTC_REQUEST_IDENTITIES` request.
1911 Args:
1912 list_extended:
1913 If true, answer an `SSH_AGENTC_EXTENSION` request for
1914 the `list-extended@putty.projects.tartarus.org`
1915 extension. Otherwise, answer an
1916 `SSH_AGENTC_REQUEST_IDENTITIES` request.
1918 Yields:
1919 The bytes payload of the response, without the protocol
1920 framing. The payload is yielded byte by byte, as an
1921 iterable of 8-bit integers.
1923 """
1924 if list_extended: 1924 ↛ 1925line 1924 didn't jump to line 1925 because the condition on line 1924 was never true2a v 7 BbY |
1925 yield from b'\x1d'
1926 yield from ssh_agent.SSHAgentClient.string(
1927 b'list-extended@putty.projects.tartarus.org'
1928 )
1929 else:
1930 yield from b'\x0c' 2a v 7 BbY |
1931 keys = [v for v in ALL_KEYS.values() if v.expected_signature] 2a v 7 BbY |
1932 yield from ssh_agent.SSHAgentClient.uint32(len(keys)) 2a v 7 BbY |
1933 for key in keys: 2a v 7 BbY |
1934 yield from ssh_agent.SSHAgentClient.string(key.public_key_data) 2a v 7 BbY |
1935 yield from ssh_agent.SSHAgentClient.string( 2a v 7 BbY |
1936 b'test key without passphrase'
1937 )
1938 if list_extended: 1938 ↛ 1939line 1938 didn't jump to line 1939 because the condition on line 1938 was never true2a v 7 BbY |
1939 yield from ssh_agent.SSHAgentClient.uint32(0)
1941 def sign(self) -> bytes:
1942 """Answer an `SSH_AGENTC_SIGN_REQUEST` request.
1944 Returns:
1945 The bytes payload of the response, without the protocol
1946 framing.
1948 """
1949 key_blob, rest = ssh_agent.SSHAgentClient.unstring_prefix( 17-6L
1950 self.receive_from_client[self.HEADER_SIZE + self.CODE_SIZE :]
1951 )
1952 sign_data, rest = ssh_agent.SSHAgentClient.unstring_prefix(rest) 17-6L
1953 if len(rest) != 4: 17-6L
1954 raise ValueError(self._INVALID_REQUEST) 16
1955 flags = int.from_bytes(rest, 'big') 17-L
1956 if flags: 17-L
1957 raise ValueError(self._UNSUPPORTED_REQUEST) 1L
1958 if sign_data != vault.Vault.UUID: 17-L
1959 raise ValueError(self._UNSUPPORTED_REQUEST) 1L
1960 for key in ALL_KEYS.values(): 17-L
1961 if key.public_key_data == key_blob: 17-L
1962 if not key.expected_signature: 17-L
1963 raise ValueError(self._UNSUPPORTED_REQUEST) 1L
1964 return b'\x0e' + ssh_agent.SSHAgentClient.string( 17-
1965 key.expected_signature
1966 )
1967 raise ValueError(self._UNSUPPORTED_REQUEST) 1L
1970@socketprovider.SocketProvider.register('fake_with_address')
1971class FakeSSHAgentSocketWithAddress(FakeSSHAgentSocket):
1972 """A [`FakeSSHAgentSocket`][] requiring a specific address."""
1974 ADDRESS = 'fake-ssh-agent:'
1975 """The correct address for connecting to this fake agent."""
1977 def __init__(self) -> None:
1978 """Initialize the agent, based on `SSH_AUTH_SOCK`.
1980 Socket addresses of the form `fake-ssh-agent:<errno_value>` will
1981 raise an [`OSError`][] (or the respective subclass) with the
1982 specified [`errno`][] value. For example,
1983 `fake-ssh-agent:EPERM` will raise a [`PermissionError`][].
1985 Raises:
1986 KeyError:
1987 The `SSH_AUTH_SOCK` environment variable is not set.
1988 OSError:
1989 The address in `SSH_AUTH_SOCK` is unsuited.
1991 """
1992 super().__init__() 2a k f b g q h i j D v JbPbQbRbY LbGb
1993 try: 2a k f b g q h i j D v JbPbQbRbY LbGb
1994 orig_address = os.environ['SSH_AUTH_SOCK'] 2a k f b g q h i j D v JbPbQbRbY LbGb
1995 except KeyError as exc: 2q v Gb
1996 msg = 'SSH_AUTH_SOCK environment variable' 2q v Gb
1997 raise KeyError(msg) from exc 2q v Gb
1998 address = orig_address 2a k f b g h i j D v JbPbQbRbY LbGb
1999 if not address.startswith(self.ADDRESS): 2a k f b g h i j D v JbPbQbRbY LbGb
2000 address = self.ADDRESS + 'ENOENT' 2JbGb
2001 errcode = address.removeprefix(self.ADDRESS) 2a k f b g h i j D v JbPbQbRbY LbGb
2002 if errcode and not ( 2a k f b g h i j D v JbPbQbRbY LbGb
2003 errcode.startswith('E') and hasattr(errno, errcode)
2004 ):
2005 errcode = 'EINVAL' 2v Gb
2006 if errcode: 2a k f b g h i j D v JbPbQbRbY LbGb
2007 errno_val = getattr(errno, errcode) 2v JbGb
2008 raise OSError(errno_val, os.strerror(errno_val), orig_address) 2v JbGb
2011def list_keys(self: Any = None) -> list[_types.SSHKeyCommentPair]:
2012 """Return a list of all SSH test keys, as key/comment pairs.
2014 Intended as a monkeypatching replacement for
2015 [`ssh_agent.SSHAgentClient.list_keys`][].
2017 """
2018 del self # Unused. 1bDo
2019 Pair = _types.SSHKeyCommentPair # noqa: N806 1bDo
2020 return [ 1bDo
2021 Pair(value.public_key_data, f'{key} test key'.encode('ASCII'))
2022 for key, value in ALL_KEYS.items()
2023 ]
2026def sign(
2027 self: Any, key: bytes | bytearray, message: bytes | bytearray
2028) -> bytes:
2029 """Return the signature of `message` under `key`.
2031 Can only handle keys in [`SUPPORTED_KEYS`][], and only the vault
2032 UUID as the message.
2034 Intended as a monkeypatching replacement for
2035 [`ssh_agent.SSHAgentClient.sign`][].
2037 """
2038 del self # Unused. 1bg
2039 assert message == vault.Vault.UUID 1bg
2040 for value in SUPPORTED_KEYS.values(): 1bg
2041 if value.public_key_data == key: # pragma: no branch 1bg
2042 assert value.expected_signature is not None 1bg
2043 return value.expected_signature 1bg
2044 raise AssertionError
2047def list_keys_singleton(self: Any = None) -> list[_types.SSHKeyCommentPair]:
2048 """Return a singleton list of the first supported SSH test key.
2050 The key is returned as a key/comment pair.
2052 Intended as a monkeypatching replacement for
2053 [`ssh_agent.SSHAgentClient.list_keys`][].
2055 """
2056 del self # Unused. 1ao
2057 Pair = _types.SSHKeyCommentPair # noqa: N806 1ao
2058 list1 = [ 1ao
2059 Pair(value.public_key_data, f'{key} test key'.encode('ASCII'))
2060 for key, value in SUPPORTED_KEYS.items()
2061 ]
2062 return list1[:1] 1ao
2065def suitable_ssh_keys(conn: Any) -> Iterator[_types.SSHKeyCommentPair]:
2066 """Return a two-item list of SSH test keys (key/comment pairs).
2068 Intended as a monkeypatching replacement for
2069 `cli_machinery.get_suitable_ssh_keys` to better script and test the
2070 interactive key selection. When used this way, `derivepassphrase`
2071 believes that only those two keys are loaded and suitable.
2073 """
2074 del conn # Unused. 1f8F
2075 Pair = _types.SSHKeyCommentPair # noqa: N806 1f8F
2076 yield from [ 1f8F
2077 Pair(DUMMY_KEY1, b'no comment'),
2078 Pair(DUMMY_KEY2, b'a comment'),
2079 ]
2082def phrase_from_key(
2083 key: bytes,
2084 /,
2085 *,
2086 conn: ssh_agent.SSHAgentClient | socket.socket | None = None,
2087) -> bytes:
2088 """Return the "equivalent master passphrase" for key.
2090 Only works for key [`DUMMY_KEY1`][].
2092 Intended as a monkeypatching replacement for
2093 [`vault.Vault.phrase_from_key`][], bypassing communication with an
2094 actual SSH agent.
2096 """
2097 del conn 1kf
2098 if key == DUMMY_KEY1: # pragma: no branch 1kf
2099 return DUMMY_PHRASE_FROM_KEY1 1kf
2100 raise KeyError(key) # pragma: no cover
2103def provider_entry_provider() -> _types.SSHAgentSocket: # pragma: no cover
2104 """A pseudo provider for a [`_types.SSHAgentSocketProviderEntry`][]."""
2105 msg = 'We are not supposed to be called!'
2106 raise AssertionError(msg)
2109provider_entry1 = _types.SSHAgentSocketProviderEntry(
2110 provider_entry_provider, 'entry1', ('entry1a', 'entry1b', 'entry1c')
2111)
2112"""A sample [`_types.SSHAgentSocketProviderEntry`][]."""
2114provider_entry2 = _types.SSHAgentSocketProviderEntry(
2115 provider_entry_provider, 'entry2', ('entry2d', 'entry2e')
2116)
2117"""A sample [`_types.SSHAgentSocketProviderEntry`][]."""
2119posix_entry = _types.SSHAgentSocketProviderEntry(
2120 socketprovider.SocketProvider.resolve('posix'), 'posix', ()
2121)
2122"""
2123The standard [`_types.SSHAgentSocketProviderEntry`][] for the UNIX
2124domain socket handler on POSIX systems.
2125"""
2127the_annoying_os_entry = _types.SSHAgentSocketProviderEntry(
2128 socketprovider.SocketProvider.resolve('the_annoying_os'),
2129 'the_annoying_os',
2130 (),
2131)
2132"""
2133The standard [`_types.SSHAgentSocketProviderEntry`][] for the named pipe
2134handler on The Annoying Operating System.
2135"""
2137faulty_entry_callable = _types.SSHAgentSocketProviderEntry(
2138 (), # type: ignore[arg-type]
2139 'tuple',
2140 (),
2141)
2142"""
2143A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler
2144is not a callable.
2145"""
2147faulty_entry_name_exists = _types.SSHAgentSocketProviderEntry(
2148 socketprovider.SocketProvider.resolve('the_annoying_os'), 'posix', ()
2149)
2150"""
2151A faulty [`_types.SSHAgentSocketProviderEntry`][]: the indicated handler
2152is already registered with a different callable.
2153"""
2155faulty_entry_alias_exists = _types.SSHAgentSocketProviderEntry(
2156 socketprovider.SocketProvider.resolve('posix'),
2157 'posix',
2158 ('unix_domain', 'the_annoying_os'),
2159)
2160"""
2161A faulty [`_types.SSHAgentSocketProviderEntry`][]: the alias is already
2162registered with a different callable.
2163"""
2166@contextlib.contextmanager
2167def faked_entry_point_list( # noqa: C901
2168 additional_entry_points: Sequence[importlib.metadata.EntryPoint],
2169 remove_conflicting_entries: bool = False,
2170) -> Iterator[Sequence[str]]:
2171 """Yield a context where additional entry points are visible.
2173 Args:
2174 additional_entry_points:
2175 A sequence of entry point objects that should additionally
2176 be visible.
2177 remove_conflicting_entries:
2178 If true, remove all names provided by the additional entry
2179 points, otherwise leave them untouched.
2181 Yields:
2182 A sequence of registry names that are newly available within the
2183 context.
2185 """
2186 true_entry_points = importlib.metadata.entry_points() 1lm
2187 additional_entry_points = list(additional_entry_points) 1lm
2189 if sys.version_info >= (3, 12): 1lm
2190 new_entry_points = importlib.metadata.EntryPoints( 1lm
2191 list(true_entry_points) + additional_entry_points
2192 )
2194 @overload 1lm
2195 def mangled_entry_points( 1lm
2196 *, group: None = None 1lm
2197 ) -> importlib.metadata.EntryPoints: ... 1lm
2199 @overload 1lm
2200 def mangled_entry_points( 1lm
2201 *, group: str 1lm
2202 ) -> importlib.metadata.EntryPoints: ... 1lm
2204 def mangled_entry_points( 1lm
2205 **params: Any,
2206 ) -> importlib.metadata.EntryPoints:
2207 return new_entry_points.select(**params) 1lm
2209 elif sys.version_info >= (3, 10): 1lm
2210 # Compatibility concerns within importlib.metadata: depending on
2211 # whether the .select() API is used, the result is either the dict
2212 # of groups of points (as in < 3.10), or the EntryPoints iterable
2213 # (as in >= 3.12). So our wrapper needs to duplicate that
2214 # interface. FUN.
2215 new_entry_points_dict = { 1lm
2216 k: list(v) for k, v in true_entry_points.items()
2217 }
2218 for ep in additional_entry_points: 1lm
2219 new_entry_points_dict.setdefault(ep.group, []).append(ep) 1lm
2220 new_entry_points = importlib.metadata.EntryPoints([ 1lm
2221 ep for group in new_entry_points_dict.values() for ep in group
2222 ])
2224 @overload 1lm
2225 def mangled_entry_points( 1lm
2226 *, group: None = None 1lm
2227 ) -> dict[ 1lm
2228 str,
2229 list[importlib.metadata.EntryPoint]
2230 | tuple[importlib.metadata.EntryPoint, ...],
2231 ]: ...
2233 @overload 1lm
2234 def mangled_entry_points( 1lm
2235 *, group: str 1lm
2236 ) -> importlib.metadata.EntryPoints: ... 1lm
2238 def mangled_entry_points( 1lm
2239 **params: Any,
2240 ) -> (
2241 importlib.metadata.EntryPoints
2242 | dict[
2243 str,
2244 list[importlib.metadata.EntryPoint]
2245 | tuple[importlib.metadata.EntryPoint, ...],
2246 ]
2247 ):
2248 return ( 1lm
2249 new_entry_points.select(**params)
2250 if params
2251 else new_entry_points_dict
2252 )
2254 else:
2255 new_entry_points: dict[ 1lm
2256 str,
2257 list[importlib.metadata.EntryPoint]
2258 | tuple[importlib.metadata.EntryPoint, ...],
2259 ] = {
2260 group_name: list(group)
2261 for group_name, group in true_entry_points.items()
2262 }
2263 for ep in additional_entry_points: 1lm
2264 new_entry_points.setdefault(ep.group, []) 1lm
2265 new_entry_points[ep.group].append(ep) 1lm
2266 new_entry_points = { 1lm
2267 group_name: tuple(group)
2268 for group_name, group in new_entry_points.items()
2269 }
2271 @overload 1lm
2272 def mangled_entry_points( 1lm
2273 *, group: None = None 1lm
2274 ) -> dict[str, tuple[importlib.metadata.EntryPoint, ...]]: ... 1lm
2276 @overload 1lm
2277 def mangled_entry_points( 1lm
2278 *, group: str 1lm
2279 ) -> tuple[importlib.metadata.EntryPoint, ...]: ... 1lm
2281 def mangled_entry_points( 1lm
2282 *, group: str | None = None
2283 ) -> (
2284 dict[str, tuple[importlib.metadata.EntryPoint, ...]]
2285 | tuple[importlib.metadata.EntryPoint, ...]
2286 ):
2287 return (
2288 new_entry_points.get(group, ())
2289 if group is not None
2290 else new_entry_points
2291 )
2293 registry = socketprovider.SocketProvider.registry 1lm
2294 new_registry = registry.copy() 1lm
2295 keys = [ep.load().key for ep in additional_entry_points] 1lm
2296 aliases = [a for ep in additional_entry_points for a in ep.load().aliases] 1lm
2297 # This functionality is currently unused, so excluded from coverage.
2298 if remove_conflicting_entries: # pragma: no cover 1lm
2299 for name in [*keys, *aliases]:
2300 new_registry.pop(name, None)
2302 with pytest.MonkeyPatch.context() as monkeypatch: 1lm
2303 monkeypatch.setattr( 1lm
2304 socketprovider.SocketProvider, 'registry', new_registry
2305 )
2306 monkeypatch.setattr( 1lm
2307 importlib.metadata, 'entry_points', mangled_entry_points
2308 )
2309 yield (*keys, *aliases) 1lm
2312@contextlib.contextmanager
2313def isolated_config(
2314 monkeypatch: pytest.MonkeyPatch,
2315 runner: CliRunner,
2316 main_config_str: str | None = None,
2317) -> Iterator[None]:
2318 """Provide an isolated configuration setup, as a context.
2320 This context manager sets up (and changes into) a temporary
2321 directory, which holds the user configuration specified in
2322 `main_config_str`, if any. The manager also ensures that the
2323 environment variables `HOME` and `USERPROFILE` are set, and that
2324 `DERIVEPASSPHRASE_PATH` is unset. Upon exiting the context, the
2325 changes are undone and the temporary directory is removed.
2327 Args:
2328 monkeypatch:
2329 A monkeypatch fixture object.
2330 runner:
2331 A `click` CLI runner harness.
2332 main_config_str:
2333 Optional TOML file contents, to be used as the user
2334 configuration.
2336 Returns:
2337 A context manager, without a return value.
2339 """
2340 prog_name = cli_helpers.PROG_NAME 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2341 env_name = prog_name.replace(' ', '_').upper() + '_PATH' 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2342 # TODO(the-13th-letter): Rewrite using parenthesized with-statements.
2343 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2344 with contextlib.ExitStack() as stack: 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2345 stack.enter_context(runner.isolated_filesystem()) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2346 stack.enter_context( 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2347 cli_machinery.StandardCLILogging.ensure_standard_logging()
2348 )
2349 stack.enter_context( 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2350 cli_machinery.StandardCLILogging.ensure_standard_warnings_logging()
2351 )
2352 cwd = str(pathlib.Path.cwd().resolve()) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2353 monkeypatch.setenv('HOME', cwd) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2354 monkeypatch.setenv('APPDATA', cwd) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2355 monkeypatch.setenv('LOCALAPPDATA', cwd) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2356 monkeypatch.delenv(env_name, raising=False) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2357 config_dir = cli_helpers.config_filename(subsystem=None) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2358 config_dir.mkdir(parents=True, exist_ok=True) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2359 if isinstance(main_config_str, str): 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2360 cli_helpers.config_filename('user configuration').write_text( 1JPQRS
2361 main_config_str, encoding='UTF-8'
2362 )
2363 try: 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2364 yield 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2365 finally:
2366 cli_helpers.config_filename('write lock').unlink(missing_ok=True) 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFb3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2369@contextlib.contextmanager
2370def isolated_vault_config(
2371 monkeypatch: pytest.MonkeyPatch,
2372 runner: CliRunner,
2373 vault_config: Any,
2374 main_config_str: str | None = None,
2375) -> Iterator[None]:
2376 """Provide an isolated vault configuration setup, as a context.
2378 Uses [`isolated_config`][] internally. Beyond those actions, this
2379 manager also loads the specified vault configuration into the
2380 context.
2382 Args:
2383 monkeypatch:
2384 A monkeypatch fixture object.
2385 runner:
2386 A `click` CLI runner harness.
2387 vault_config:
2388 A valid vault configuration, to be integrated into the
2389 context.
2390 main_config_str:
2391 Optional TOML file contents, to be used as the user
2392 configuration.
2394 Returns:
2395 A context manager, without a return value.
2397 """
2398 with isolated_config( 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2399 monkeypatch=monkeypatch, runner=runner, main_config_str=main_config_str
2400 ):
2401 config_filename = cli_helpers.config_filename(subsystem='vault') 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2402 with config_filename.open('w', encoding='UTF-8') as outfile: 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2403 json.dump(vault_config, outfile) 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2404 yield 2e k f b g M G N O I ( Z 0 8 F q p 1 2 h i j J P Q R S z rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X
2407@contextlib.contextmanager
2408def isolated_vault_exporter_config(
2409 monkeypatch: pytest.MonkeyPatch,
2410 runner: CliRunner,
2411 vault_config: str | bytes | None = None,
2412 vault_key: str | None = None,
2413) -> Iterator[None]:
2414 """Provide an isolated vault configuration setup, as a context.
2416 Works similarly to [`isolated_config`][], except that no user
2417 configuration is accepted or integrated into the context. This
2418 manager also accepts a serialized vault-native configuration and
2419 a vault encryption key to integrate into the context.
2421 Args:
2422 monkeypatch:
2423 A monkeypatch fixture object.
2424 runner:
2425 A `click` CLI runner harness.
2426 vault_config:
2427 An optional serialized vault-native configuration, to be
2428 integrated into the context. If a text string, then the
2429 contents are written to the file `.vault`. If a byte
2430 string, then it is treated as base64-encoded zip file
2431 contents, which---once inside the `.vault` directory---will
2432 be extracted into the current directory.
2433 vault_key:
2434 An optional encryption key presumably for the stored
2435 vault-native configuration. If given, then the environment
2436 variable `VAULT_KEY` will be populated with this key while
2437 the context is active.
2439 Returns:
2440 A context manager, without a return value.
2442 """
2443 # TODO(the-13th-letter): Remove the fallback implementation.
2444 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.10
2445 if TYPE_CHECKING: 2H # ' n w t x u y A E B C nbobpb} K d ~
2446 chdir: Callable[..., AbstractContextManager]
2447 else:
2448 try: 2H # ' n w t x u y A E B C nbobpb} K d ~
2449 chdir = contextlib.chdir # type: ignore[attr] 2H # ' n w t x u y A E B C nbobpb} K d ~
2450 except AttributeError: 2H # ' n w t x u y A E B C nbobpb} K d ~
2452 @contextlib.contextmanager 2H # ' n w t x u y A E B C nbobpb} K d ~
2453 def chdir( 2H # ' n w t x u y A E B C nbobpb} K d ~
2454 newpath: str | bytes | os.PathLike,
2455 ) -> Iterator[None]: # pragma: no branch
2456 oldpath = pathlib.Path.cwd().resolve() 1nAEBCd
2457 os.chdir(newpath) 1nAEBCd
2458 yield 1nAEBCd
2459 os.chdir(oldpath) 1nAEBCd
2461 with runner.isolated_filesystem(): 2H # ' n w t x u y A E B C nbobpb} K d ~
2462 cwd = str(pathlib.Path.cwd().resolve()) 2H # ' n w t x u y A E B C nbobpb} K d ~
2463 monkeypatch.setenv('HOME', cwd) 2H # ' n w t x u y A E B C nbobpb} K d ~
2464 monkeypatch.setenv('USERPROFILE', cwd) 2H # ' n w t x u y A E B C nbobpb} K d ~
2465 monkeypatch.delenv( 2H # ' n w t x u y A E B C nbobpb} K d ~
2466 cli_helpers.PROG_NAME.replace(' ', '_').upper() + '_PATH',
2467 raising=False,
2468 )
2469 monkeypatch.delenv('VAULT_PATH', raising=False) 2H # ' n w t x u y A E B C nbobpb} K d ~
2470 monkeypatch.delenv('VAULT_KEY', raising=False) 2H # ' n w t x u y A E B C nbobpb} K d ~
2471 monkeypatch.delenv('LOGNAME', raising=False) 2H # ' n w t x u y A E B C nbobpb} K d ~
2472 monkeypatch.delenv('USER', raising=False) 2H # ' n w t x u y A E B C nbobpb} K d ~
2473 monkeypatch.delenv('USERNAME', raising=False) 2H # ' n w t x u y A E B C nbobpb} K d ~
2474 if vault_key is not None: 2H # ' n w t x u y A E B C nbobpb} K d ~
2475 monkeypatch.setenv('VAULT_KEY', vault_key) 2H # w t x u y A B C nbobK d
2476 vault_config_path = pathlib.Path('.vault').resolve() 2H # ' n w t x u y A E B C nbobpb} K d ~
2477 # TODO(the-13th-letter): Rewrite using structural pattern matching.
2478 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2479 if isinstance(vault_config, str): 2H # ' n w t x u y A E B C nbobpb} K d ~
2480 vault_config_path.write_text(f'{vault_config}\n', encoding='UTF-8') 2H # ' n w t x u y nbobpbK d
2481 elif isinstance(vault_config, bytes): 1nAEBC}d~
2482 vault_config_path.mkdir(parents=True, mode=0o700, exist_ok=True) 1nAEBCd
2483 # TODO(the-13th-letter): Rewrite using parenthesized
2484 # with-statements.
2485 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2486 with contextlib.ExitStack() as stack: 1nAEBCd
2487 stack.enter_context(chdir(vault_config_path)) 1nAEBCd
2488 tmpzipfile = stack.enter_context( 1nAEBCd
2489 tempfile.NamedTemporaryFile(suffix='.zip')
2490 )
2491 for line in vault_config.splitlines(): 1nAEBCd
2492 tmpzipfile.write(base64.standard_b64decode(line)) 1nAEBCd
2493 tmpzipfile.flush() 1nAEBCd
2494 tmpzipfile.seek(0, 0) 1nAEBCd
2495 with zipfile.ZipFile(tmpzipfile.file) as zipfileobj: 1nAEBCd
2496 zipfileobj.extractall() 1nAEBCd
2497 elif vault_config is None: 1}~
2498 pass 1}~
2499 else: # pragma: no cover
2500 assert_never(vault_config)
2501 try: 2H # ' n w t x u y A E B C nbobpb} K d ~
2502 yield 2H # ' n w t x u y A E B C nbobpb} K d ~
2503 finally:
2504 cli_helpers.config_filename('write lock').unlink(missing_ok=True) 2H # ' n w t x u y A E B C nbobpb} K d ~
2507def auto_prompt(*args: Any, **kwargs: Any) -> str:
2508 """Return [`DUMMY_PASSPHRASE`][].
2510 Intended as a monkeypatching replacement for
2511 `cli.prompt_for_passphrase` to better script and test the
2512 interactive passphrase queries.
2514 """
2515 del args, kwargs # Unused. 1`{G9
2516 return DUMMY_PASSPHRASE 1`{G9
2519def make_file_readonly(
2520 pathname: str | bytes | os.PathLike[str],
2521 /,
2522 *,
2523 try_race_free_implementation: bool = True,
2524) -> None:
2525 """Mark a file as read-only.
2527 On POSIX, this entails removing the write permission bits for user,
2528 group and other, and ensuring the read permission bit for user is
2529 set.
2531 Unfortunately, The Annoying OS (a.k.a. Microsoft Windows) has its
2532 own rules: Set exactly(?) the read permission bit for user to make
2533 the file read-only, and set exactly(?) the write permission bit for
2534 user to make the file read/write; all other permission bit settings
2535 are ignored.
2537 The cross-platform procedure therefore is:
2539 1. Call `os.stat` on the file, noting the permission bits.
2540 2. Calculate the new permission bits POSIX-style.
2541 3. Call `os.chmod` with permission bit `stat.S_IREAD`.
2542 4. Call `os.chmod` with the correct POSIX-style permissions.
2544 If the platform supports it, we use a file descriptor instead of
2545 a path name. Otherwise, we use the same path name multiple times,
2546 and are susceptible to race conditions.
2548 """
2549 fname: int | str | bytes | os.PathLike
2550 if try_race_free_implementation and {os.stat, os.chmod} <= os.supports_fd: 1p
2551 # The Annoying OS (v11 at least) supports fstat and fchmod, but
2552 # does not support changing the file mode on file descriptors
2553 # for read-only files.
2554 fname = os.open( 1p
2555 pathname,
2556 os.O_RDWR
2557 | getattr(os, 'O_CLOEXEC', 0)
2558 | getattr(os, 'O_NOCTTY', 0),
2559 )
2560 else:
2561 fname = pathname 1p
2562 try: 1p
2563 orig_mode = os.stat(fname).st_mode # noqa: PTH116 1p
2564 new_mode = ( 1p
2565 orig_mode & ~stat.S_IWUSR & ~stat.S_IWGRP & ~stat.S_IWOTH
2566 | stat.S_IREAD
2567 )
2568 os.chmod(fname, stat.S_IREAD) # noqa: PTH101 1p
2569 os.chmod(fname, new_mode) # noqa: PTH101 1p
2570 finally:
2571 if isinstance(fname, int): 1p
2572 os.close(fname) 1p
2575class ReadableResult(NamedTuple):
2576 """Helper class for formatting and testing click.testing.Result objects."""
2578 exception: BaseException | None
2579 exit_code: int
2580 stdout: str
2581 stderr: str
2583 def clean_exit(
2584 self, *, output: str = '', empty_stderr: bool = False
2585 ) -> bool:
2586 """Return whether the invocation exited cleanly.
2588 Args:
2589 output:
2590 An expected output string.
2592 """
2593 return ( 2a e abbbcbdbebfbgbhbibjbkb` { k f b g M G N O ( lb8 mbJ H 9 $ % HbIb) T r * s + , c ! U V W X # ' n o
2594 (
2595 not self.exception
2596 or (
2597 isinstance(self.exception, SystemExit)
2598 and self.exit_code == 0
2599 )
2600 )
2601 and (not output or output in self.stdout)
2602 and (not empty_stderr or not self.stderr)
2603 )
2605 def error_exit(
2606 self,
2607 *,
2608 error: str | re.Pattern[str] | type[BaseException] = BaseException,
2609 record_tuples: Sequence[tuple[str, int, str]] = (),
2610 ) -> bool:
2611 """Return whether the invocation exited uncleanly.
2613 Args:
2614 error:
2615 An expected error message, or an expected numeric error
2616 code, or an expected exception type.
2618 """
2620 def error_match(error: str | re.Pattern[str], line: str) -> bool: 2. G / : ; I Z = ? 0 F q p 1 2 h i j @ [ ] ^ P Q R S z 3 4 HbIbV 5 w t x u y K d
2621 return ( 1.G/:;IZ=?0Fqp12hij@[]^PQRSz34V5K
2622 error in line
2623 if isinstance(error, str)
2624 else error.match(line) is not None
2625 )
2627 # TODO(the-13th-letter): Rewrite using structural pattern matching.
2628 # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2629 if isinstance(error, type): 2. G / : ; I Z = ? 0 F q p 1 2 h i j @ [ ] ^ P Q R S z 3 4 HbIbV 5 w t x u y K d
2630 return isinstance(self.exception, error) 2HbIb
2631 else: # noqa: RET505
2632 assert isinstance(error, (str, re.Pattern)) 1.G/:;IZ=?0Fqp12hij@[]^PQRSz34V5wtxuyKd
2633 return ( 1.G/:;IZ=?0Fqp12hij@[]^PQRSz34V5wtxuyKd
2634 isinstance(self.exception, SystemExit)
2635 and self.exit_code > 0
2636 and (
2637 not error
2638 or any(
2639 error_match(error, line)
2640 for line in self.stderr.splitlines(True)
2641 )
2642 or error_emitted(error, record_tuples)
2643 )
2644 )
2647class CliRunner:
2648 """An abstracted CLI runner class.
2650 Intended to provide similar functionality and scope as the
2651 [`click.testing.CliRunner`][] class, though not necessarily
2652 `click`-specific. Also allows for seamless migration away from
2653 `click`, if/when we decide this.
2655 """
2657 _SUPPORTS_MIX_STDERR_ATTRIBUTE = not hasattr(click.testing, 'StreamMixer')
2658 """
2659 True if and only if [`click.testing.CliRunner`][] supports the
2660 `mix_stderr` attribute. It was removed in 8.2.0 in favor of the
2661 `click.testing.StreamMixer` class.
2663 See also
2664 [`pallets/click#2523`](https://github.com/pallets/click/pull/2523).
2665 """
2667 def __init__(
2668 self,
2669 *,
2670 mix_stderr: bool = False,
2671 color: bool | None = None,
2672 ) -> None:
2673 self.color = color 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o
2674 self.mix_stderr = mix_stderr 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o
2676 class MixStderrAttribute(TypedDict): 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o
2677 mix_stderr: NotRequired[bool] 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o
2679 mix_stderr_args: MixStderrAttribute = ( 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o
2680 {'mix_stderr': mix_stderr}
2681 if self._SUPPORTS_MIX_STDERR_ATTRIBUTE
2682 else {}
2683 )
2684 self.click_testing_clirunner = click.testing.CliRunner( 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbHbIbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~ o
2685 **mix_stderr_args
2686 )
2688 def invoke(
2689 self,
2690 cli: click.BaseCommand,
2691 args: Sequence[str] | str | None = None,
2692 input: str | bytes | IO[Any] | None = None,
2693 env: Mapping[str, str | None] | None = None,
2694 catch_exceptions: bool = True,
2695 color: bool | None = None,
2696 **extra: Any,
2697 ) -> ReadableResult:
2698 if color is None: # pragma: no cover 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o
2699 color = self.color if self.color is not None else False 2a e abbbcbdbebfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o
2700 raw_result = self.click_testing_clirunner.invoke( 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o
2701 cli,
2702 args=args,
2703 input=input,
2704 env=env,
2705 catch_exceptions=catch_exceptions,
2706 color=color,
2707 **extra,
2708 )
2709 # In 8.2.0, r.stdout is no longer a property aliasing the
2710 # `output` attribute, but rather the raw stdout value.
2711 try: 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o
2712 stderr = raw_result.stderr 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o
2713 except ValueError: 2HbIb! o
2714 stderr = raw_result.stdout 2HbIb! o
2715 return ReadableResult( 2a e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z H 3 9 4 $ % HbIb) T r * s + , c ! U V W 5 X # ' n w t x u y K d o
2716 raw_result.exception,
2717 raw_result.exit_code,
2718 (raw_result.stdout if not self.mix_stderr else raw_result.output)
2719 or '',
2720 stderr or '',
2721 )
2722 return ReadableResult.parse(raw_result)
2724 def isolated_filesystem(
2725 self,
2726 temp_dir: str | os.PathLike[str] | None = None,
2727 ) -> AbstractContextManager[str]:
2728 return self.click_testing_clirunner.isolated_filesystem( 2e abbbcbdbebqbfbgbhbibjbkb` { k f b g M . G N / O : ; I ( lbZ = ? 0 8 F q p 1 2 h i j @ [ mb] ^ J P Q R S z CbDbEbFbH 3 9 4 $ % rbsbtbub) vbwbT xbybr * s + zb, c ! U V W 5 X # ' n w t x u y A E B C nbobpb} K d ~
2729 temp_dir=temp_dir
2730 )
2733def parse_sh_export_line(line: str, *, env_name: str) -> str:
2734 """Parse the output of typical SSH agents' SSH_AUTH_SOCK lines.
2736 Intentionally parses only a small subset of sh(1) syntax which works
2737 with current OpenSSH and PuTTY output. We require exactly one
2738 variable setting, and one export instruction, both on the same line,
2739 and perhaps combined into one statement. Terminating semicolons
2740 after each command are ignored.
2742 Args:
2743 line:
2744 A line of sh(1) script to parse.
2745 env_name:
2746 The name of the environment variable to expect.
2748 Returns:
2749 The parsed environment variable value.
2751 Raises:
2752 ValueError:
2753 Cannot parse the sh script. Perhaps it is too complex,
2754 perhaps it is malformed.
2756 """
2757 line = line.rstrip('\r\n') 2Kb
2758 shlex_parser = shlex.shlex( 2Kb
2759 instream=line, posix=True, punctuation_chars=True
2760 )
2761 shlex_parser.whitespace = ' \t' 2Kb
2762 tokens = list(shlex_parser) 2Kb
2763 orig_tokens = tokens.copy() 2Kb
2764 if tokens[-1] == ';': 2Kb
2765 tokens.pop() 2Kb
2766 if tokens[-3:] == [';', 'export', env_name]: 2Kb
2767 tokens[-3:] = [] 2Kb
2768 tokens[:0] = ['export'] 2Kb
2769 if not ( 2Kb
2770 len(tokens) == 2
2771 and tokens[0] == 'export'
2772 and tokens[1].startswith(f'{env_name}=')
2773 ):
2774 msg = f'Cannot parse sh line: {orig_tokens!r} -> {tokens!r}' 2Kb
2775 raise ValueError(msg) 2Kb
2776 return tokens[1].split('=', 1)[1] 2Kb
2779def message_emitted_factory(
2780 level: int,
2781 *,
2782 logger_name: str = cli.PROG_NAME,
2783) -> Callable[[str | re.Pattern[str], Sequence[tuple[str, int, str]]], bool]:
2784 """Return a function to test if a matching message was emitted.
2786 Args:
2787 level: The level to match messages at.
2788 logger_name: The name of the logger to match against.
2790 """
2792 def message_emitted(
2793 text: str | re.Pattern[str],
2794 record_tuples: Sequence[tuple[str, int, str]],
2795 ) -> bool:
2796 """Return true if a matching message was emitted.
2798 Args:
2799 text: Substring or pattern to match against.
2800 record_tuples: Items to match.
2802 """
2804 def check_record(record: tuple[str, int, str]) -> bool: 1MNOIJzH394$%TcUWXwtxuyd
2805 if record[:2] != (logger_name, level): 1MNOJzH394$%TcUWXwtxuyd
2806 return False 1$%tu
2807 if isinstance(text, str): 1MNOJzH394$%TcUWXwtxuyd
2808 return text in record[2] 1MNOJzH394$%TcUWXwtxuyd
2809 return text.match(record[2]) is not None # pragma: no cover
2811 return any(map(check_record, record_tuples)) 1MNOIJzH394$%TcUWXwtxuyd
2813 return message_emitted
2816# No need to assert debug messages as of yet.
2817info_emitted = message_emitted_factory(logging.INFO)
2818warning_emitted = message_emitted_factory(logging.WARNING)
2819deprecation_warning_emitted = message_emitted_factory(
2820 logging.WARNING, logger_name=f'{cli.PROG_NAME}.deprecation'
2821)
2822deprecation_info_emitted = message_emitted_factory(
2823 logging.INFO, logger_name=f'{cli.PROG_NAME}.deprecation'
2824)
2825error_emitted = message_emitted_factory(logging.ERROR)
2828class Parametrize(types.SimpleNamespace):
2829 VAULT_CONFIG_FORMATS_DATA = pytest.mark.parametrize(
2830 ['config', 'format', 'config_data'],
2831 [
2832 pytest.param(
2833 VAULT_V02_CONFIG,
2834 'v0.2',
2835 VAULT_V02_CONFIG_DATA,
2836 id='0.2',
2837 ),
2838 pytest.param(
2839 VAULT_V03_CONFIG,
2840 'v0.3',
2841 VAULT_V03_CONFIG_DATA,
2842 id='0.3',
2843 ),
2844 pytest.param(
2845 VAULT_STOREROOM_CONFIG_ZIPPED,
2846 'storeroom',
2847 VAULT_STOREROOM_CONFIG_DATA,
2848 id='storeroom',
2849 ),
2850 ],
2851 )