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

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

2# 

3# SPDX-License-Identifier: Zlib 

4 

5from __future__ import annotations 

6 

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 

25 

26import click.testing 

27import hypothesis 

28import pytest 

29from hypothesis import strategies 

30from typing_extensions import NamedTuple, assert_never, overload 

31 

32from derivepassphrase import _types, cli, ssh_agent, vault 

33from derivepassphrase._internals import cli_helpers, cli_machinery 

34from derivepassphrase.ssh_agent import socketprovider 

35 

36__all__ = () 

37 

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 

43 

44 from typing_extensions import Any, Buffer, Self 

45 

46 

47class SSHTestKey(NamedTuple): 

48 """An SSH test key. 

49 

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

70 

71 """ 

72 

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

85 

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. 

92 

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. 

98 

99 """ 

100 return vault.Vault.is_suitable_ssh_key( 

101 self.public_key_data, client=client 

102 ) 

103 

104 

105class ValidationSettings(NamedTuple): 

106 """Validation settings for [`VaultTestConfig`][]s. 

107 

108 Attributes: 

109 allow_unknown_settings: 

110 See [`_types.validate_vault_config`][]. 

111 

112 """ 

113 

114 allow_unknown_settings: bool 

115 """""" 

116 

117 

118class VaultTestConfig(NamedTuple): 

119 """A (not necessarily valid) sample vault config, plus metadata. 

120 

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

132 

133 """ 

134 

135 config: Any 

136 """""" 

137 comment: str 

138 """""" 

139 validation_settings: ValidationSettings | None 

140 """""" 

141 

142 

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

393 

394 

395def is_valid_test_config(conf: VaultTestConfig, /) -> bool: 

396 """Return true if the test config is valid. 

397 

398 Args: 

399 conf: The test config to check. 

400 

401 """ 

402 return not conf.comment and conf.validation_settings in { 

403 None, 

404 (True,), 

405 } 

406 

407 

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

412 

413 

414@strategies.composite 

415def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]: 

416 """Hypothesis strategy for full vault service configurations. 

417 

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. 

421 

422 Args: 

423 draw: 

424 The `draw` function, as provided for by hypothesis. 

425 

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 } 

453 

454 

455def is_smudgable_vault_test_config(conf: VaultTestConfig) -> bool: 

456 """Check whether this vault test config can be effectively smudged. 

457 

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. 

461 

462 Args: 

463 conf: A test config to check. 

464 

465 Returns: 

466 True if the test config can be smudged, False otherwise. 

467 

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 ) 

477 

478 

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. 

487 

488 Uses [`_types.js_truthiness`][] internally, which is tested 

489 separately by 

490 [`tests.test_derivepassphrase_types.test_100_js_truthiness`][]. 

491 

492 Args: 

493 draw: 

494 The `draw` function, as provided for by hypothesis. 

495 config: 

496 A strategy which generates [`VaultTestConfig`][] objects. 

497 

498 Returns: 

499 A new [`VaultTestConfig`][] where some falsy values have been 

500 replaced or added. 

501 

502 """ 

503 

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

544 

545 

546class KnownSSHAgent(str, enum.Enum): 

547 """Known SSH agents. 

548 

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. 

558 

559 """ 

560 

561 UNKNOWN = '(unknown)' 

562 """""" 

563 Pageant = 'Pageant' 

564 """""" 

565 OpenSSHAgent = 'OpenSSHAgent' 

566 """""" 

567 FakeSSHAgentSocket = 'FakeSSHAgentSocket' 

568 """""" 

569 

570 

571class SpawnedSSHAgentInfo(NamedTuple): 

572 """Info about a spawned SSH agent, as provided by some fixtures. 

573 

574 Differs from [`RunningSSHAgentInfo`][] in that this info object 

575 already provides a functional client connected to the agent, but not 

576 the address. 

577 

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. 

589 

590 """ 

591 

592 agent_type: KnownSSHAgent 

593 """""" 

594 client: ssh_agent.SSHAgentClient 

595 """""" 

596 isolated: bool 

597 """""" 

598 

599 

600class RunningSSHAgentInfo(NamedTuple): 

601 """Info about a running SSH agent, as provided by some fixtures. 

602 

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. 

607 

608 Attributes: 

609 socket: 

610 A socket address to connect to the agent. 

611 agent_type: 

612 The agent's type. 

613 

614 """ 

615 

616 socket: str | type[_types.SSHAgentSocket] 

617 """""" 

618 agent_type: KnownSSHAgent 

619 """""" 

620 

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 

628 

629 

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

1168 

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

1220 

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

1337 

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. 

1390 

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

1397 

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. 

1455 

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

1461 

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. 

1519 

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

1525 

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. 

1584 

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

1591 

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

1600 

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. 

1631 

1632See also: 

1633 [`xfail_on_the_annoying_os`][] 

1634 

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

1647 

1648MIN_CONCURRENCY = 4 

1649""" 

1650The minimum amount of concurrent threads used for testing. 

1651""" 

1652 

1653 

1654def get_concurrency_limit() -> int: 

1655 """Return the imposed limit on the number of concurrent threads. 

1656 

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. 

1662 

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

1673 

1674 

1675def get_concurrency_step_count( 

1676 settings: hypothesis.settings | None = None, 

1677) -> int: 

1678 """Return the desired step count for concurrency-related tests. 

1679 

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. 

1683 

1684 Args: 

1685 settings: 

1686 The hypothesis settings for a specific tests. If not given, 

1687 then the current profile will be queried directly. 

1688 

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

1693 

1694 

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. 

1702 

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. 

1707 

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. 

1715 

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. 

1722 

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) 

1733 

1734 

1735@socketprovider.SocketProvider.register('fake') 

1736class FakeSSHAgentSocket: 

1737 """A faked SSH agent presenting an [`_types.SSHAgentSocket`][].""" 

1738 

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

1744 

1745 HEADER_SIZE = 4 

1746 CODE_SIZE = 1 

1747 

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

1753 

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

1757 

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

1761 

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

1763 """Send data to the SSH agent. 

1764 

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. 

1770 

1771 Args: 

1772 data: Binary data to send to the agent. 

1773 flags: Reserved. Must be 0. 

1774 

1775 Returns: 

1776 Nothing. The result should be requested via [`recv`][], and 

1777 interpreted in accordance with the SSH agent protocol. 

1778 

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. 

1785 

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

1799 

1800 def recv(self, count: int, flags: int = 0, /) -> bytes: 

1801 """Read data from the SSH agent. 

1802 

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. 

1810 

1811 Args: 

1812 count: 

1813 Number of bytes to read from the agent. 

1814 flags: 

1815 Reserved. Must be 0. 

1816 

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. 

1821 

1822 Raises: 

1823 AssertionError: 

1824 The flags argument, if specified, must be 0. 

1825 

1826 Alternatively, `recv` was called when there was no 

1827 response to be obtained, in violation of the SSH agent 

1828 protocol. 

1829 

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

1838 

1839 def parse_client_request_and_dispatch(self) -> None: 

1840 """Parse the client request and call the matching handler. 

1841 

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. 

1847 

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 ) 

1854 

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 ) 

1870 

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 ) 

1889 

1890 def query_extensions(self) -> Iterator[int]: 

1891 """Answer an `SSH_AGENTC_EXTENSION` request. 

1892 

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. 

1897 

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_

1907 

1908 def request_identities(self, *, list_extended: bool = False) -> Iterator[int]: 

1909 """Answer an `SSH_AGENTC_REQUEST_IDENTITIES` request. 

1910 

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. 

1917 

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. 

1922 

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) 

1940 

1941 def sign(self) -> bytes: 

1942 """Answer an `SSH_AGENTC_SIGN_REQUEST` request. 

1943 

1944 Returns: 

1945 The bytes payload of the response, without the protocol 

1946 framing. 

1947 

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

1968 

1969 

1970@socketprovider.SocketProvider.register('fake_with_address') 

1971class FakeSSHAgentSocketWithAddress(FakeSSHAgentSocket): 

1972 """A [`FakeSSHAgentSocket`][] requiring a specific address.""" 

1973 

1974 ADDRESS = 'fake-ssh-agent:' 

1975 """The correct address for connecting to this fake agent.""" 

1976 

1977 def __init__(self) -> None: 

1978 """Initialize the agent, based on `SSH_AUTH_SOCK`. 

1979 

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

1984 

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. 

1990 

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

2009 

2010 

2011def list_keys(self: Any = None) -> list[_types.SSHKeyCommentPair]: 

2012 """Return a list of all SSH test keys, as key/comment pairs. 

2013 

2014 Intended as a monkeypatching replacement for 

2015 [`ssh_agent.SSHAgentClient.list_keys`][]. 

2016 

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 ] 

2024 

2025 

2026def sign( 

2027 self: Any, key: bytes | bytearray, message: bytes | bytearray 

2028) -> bytes: 

2029 """Return the signature of `message` under `key`. 

2030 

2031 Can only handle keys in [`SUPPORTED_KEYS`][], and only the vault 

2032 UUID as the message. 

2033 

2034 Intended as a monkeypatching replacement for 

2035 [`ssh_agent.SSHAgentClient.sign`][]. 

2036 

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 

2045 

2046 

2047def list_keys_singleton(self: Any = None) -> list[_types.SSHKeyCommentPair]: 

2048 """Return a singleton list of the first supported SSH test key. 

2049 

2050 The key is returned as a key/comment pair. 

2051 

2052 Intended as a monkeypatching replacement for 

2053 [`ssh_agent.SSHAgentClient.list_keys`][]. 

2054 

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

2063 

2064 

2065def suitable_ssh_keys(conn: Any) -> Iterator[_types.SSHKeyCommentPair]: 

2066 """Return a two-item list of SSH test keys (key/comment pairs). 

2067 

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. 

2072 

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 ] 

2080 

2081 

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. 

2089 

2090 Only works for key [`DUMMY_KEY1`][]. 

2091 

2092 Intended as a monkeypatching replacement for 

2093 [`vault.Vault.phrase_from_key`][], bypassing communication with an 

2094 actual SSH agent. 

2095 

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 

2101 

2102 

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) 

2107 

2108 

2109provider_entry1 = _types.SSHAgentSocketProviderEntry( 

2110 provider_entry_provider, 'entry1', ('entry1a', 'entry1b', 'entry1c') 

2111) 

2112"""A sample [`_types.SSHAgentSocketProviderEntry`][].""" 

2113 

2114provider_entry2 = _types.SSHAgentSocketProviderEntry( 

2115 provider_entry_provider, 'entry2', ('entry2d', 'entry2e') 

2116) 

2117"""A sample [`_types.SSHAgentSocketProviderEntry`][].""" 

2118 

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

2126 

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

2136 

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

2146 

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

2154 

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

2164 

2165 

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. 

2172 

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. 

2180 

2181 Yields: 

2182 A sequence of registry names that are newly available within the 

2183 context. 

2184 

2185 """ 

2186 true_entry_points = importlib.metadata.entry_points() 1lm

2187 additional_entry_points = list(additional_entry_points) 1lm

2188 

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 ) 

2193 

2194 @overload 1lm

2195 def mangled_entry_points( 1lm

2196 *, group: None = None 1lm

2197 ) -> importlib.metadata.EntryPoints: ... 1lm

2198 

2199 @overload 1lm

2200 def mangled_entry_points( 1lm

2201 *, group: str 1lm

2202 ) -> importlib.metadata.EntryPoints: ... 1lm

2203 

2204 def mangled_entry_points( 1lm

2205 **params: Any, 

2206 ) -> importlib.metadata.EntryPoints: 

2207 return new_entry_points.select(**params) 1lm

2208 

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

2223 

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 ]: ... 

2232 

2233 @overload 1lm

2234 def mangled_entry_points( 1lm

2235 *, group: str 1lm

2236 ) -> importlib.metadata.EntryPoints: ... 1lm

2237 

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 ) 

2253 

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 } 

2270 

2271 @overload 1lm

2272 def mangled_entry_points( 1lm

2273 *, group: None = None 1lm

2274 ) -> dict[str, tuple[importlib.metadata.EntryPoint, ...]]: ... 1lm

2275 

2276 @overload 1lm

2277 def mangled_entry_points( 1lm

2278 *, group: str 1lm

2279 ) -> tuple[importlib.metadata.EntryPoint, ...]: ... 1lm

2280 

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 ) 

2292 

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) 

2301 

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

2310 

2311 

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. 

2319 

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. 

2326 

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. 

2335 

2336 Returns: 

2337 A context manager, without a return value. 

2338 

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

2367 

2368 

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. 

2377 

2378 Uses [`isolated_config`][] internally. Beyond those actions, this 

2379 manager also loads the specified vault configuration into the 

2380 context. 

2381 

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. 

2393 

2394 Returns: 

2395 A context manager, without a return value. 

2396 

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

2405 

2406 

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. 

2415 

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. 

2420 

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. 

2438 

2439 Returns: 

2440 A context manager, without a return value. 

2441 

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 ~

2451 

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

2460 

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 ~

2505 

2506 

2507def auto_prompt(*args: Any, **kwargs: Any) -> str: 

2508 """Return [`DUMMY_PASSPHRASE`][]. 

2509 

2510 Intended as a monkeypatching replacement for 

2511 `cli.prompt_for_passphrase` to better script and test the 

2512 interactive passphrase queries. 

2513 

2514 """ 

2515 del args, kwargs # Unused. 1`{G9

2516 return DUMMY_PASSPHRASE 1`{G9

2517 

2518 

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. 

2526 

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. 

2530 

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. 

2536 

2537 The cross-platform procedure therefore is: 

2538 

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. 

2543 

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. 

2547 

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

2573 

2574 

2575class ReadableResult(NamedTuple): 

2576 """Helper class for formatting and testing click.testing.Result objects.""" 

2577 

2578 exception: BaseException | None 

2579 exit_code: int 

2580 stdout: str 

2581 stderr: str 

2582 

2583 def clean_exit( 

2584 self, *, output: str = '', empty_stderr: bool = False 

2585 ) -> bool: 

2586 """Return whether the invocation exited cleanly. 

2587 

2588 Args: 

2589 output: 

2590 An expected output string. 

2591 

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 ) 

2604 

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. 

2612 

2613 Args: 

2614 error: 

2615 An expected error message, or an expected numeric error 

2616 code, or an expected exception type. 

2617 

2618 """ 

2619 

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 ) 

2626 

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 ) 

2645 

2646 

2647class CliRunner: 

2648 """An abstracted CLI runner class. 

2649 

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. 

2654 

2655 """ 

2656 

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. 

2662 

2663 See also 

2664 [`pallets/click#2523`](https://github.com/pallets/click/pull/2523). 

2665 """ 

2666 

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

2675 

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

2678 

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 ) 

2687 

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) 

2723 

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 ) 

2731 

2732 

2733def parse_sh_export_line(line: str, *, env_name: str) -> str: 

2734 """Parse the output of typical SSH agents' SSH_AUTH_SOCK lines. 

2735 

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. 

2741 

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. 

2747 

2748 Returns: 

2749 The parsed environment variable value. 

2750 

2751 Raises: 

2752 ValueError: 

2753 Cannot parse the sh script. Perhaps it is too complex, 

2754 perhaps it is malformed. 

2755 

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

2777 

2778 

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. 

2785 

2786 Args: 

2787 level: The level to match messages at. 

2788 logger_name: The name of the logger to match against. 

2789 

2790 """ 

2791 

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. 

2797 

2798 Args: 

2799 text: Substring or pattern to match against. 

2800 record_tuples: Items to match. 

2801 

2802 """ 

2803 

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 

2810 

2811 return any(map(check_record, record_tuples)) 1MNOIJzH394$%TcUWXwtxuyd

2812 

2813 return message_emitted 

2814 

2815 

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) 

2826 

2827 

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 )