Marco Ricci commited on 2024-07-21 10:09:10
Zeige 13 geänderte Dateien mit 1583 Einfügungen und 913 Löschungen.
... | ... |
@@ -2,9 +2,7 @@ |
2 | 2 |
# |
3 | 3 |
# SPDX-License-Identifier: MIT |
4 | 4 |
|
5 |
-"""Work-alike of vault(1) – a deterministic, stateless password manager |
|
6 |
- |
|
7 |
-""" # noqa: RUF002 |
|
5 |
+"""Work-alike of vault(1) – a deterministic, stateless password manager""" # noqa: RUF002 |
|
8 | 6 |
|
9 | 7 |
from __future__ import annotations |
10 | 8 |
|
... | ... |
@@ -19,8 +17,9 @@ from typing_extensions import assert_type |
19 | 17 |
import sequin |
20 | 18 |
import ssh_agent_client |
21 | 19 |
|
22 |
-__author__ = "Marco Ricci <m@the13thletter.info>" |
|
23 |
-__version__ = "0.1.1" |
|
20 |
+__author__ = 'Marco Ricci <m@the13thletter.info>' |
|
21 |
+__version__ = '0.1.1' |
|
22 |
+ |
|
24 | 23 |
|
25 | 24 |
class AmbiguousByteRepresentationError(ValueError): |
26 | 25 |
"""The object has an ambiguous byte representation.""" |
... | ... |
@@ -40,8 +40,10 @@ _CHARSETS = collections.OrderedDict([ |
40 | 40 |
]) |
41 | 41 |
_CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper'] |
42 | 42 |
_CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number'] |
43 |
-_CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space'] |
|
44 |
- + _CHARSETS['symbol']) |
|
43 |
+_CHARSETS['all'] = ( |
|
44 |
+ _CHARSETS['alphanum'] + _CHARSETS['space'] + _CHARSETS['symbol'] |
|
45 |
+) |
|
46 |
+ |
|
45 | 47 |
|
46 | 48 |
class Vault: |
47 | 49 |
"""A work-alike of James Coglan's vault. |
... | ... |
@@ -75,10 +78,16 @@ class Vault: |
75 | 78 |
""" |
76 | 79 |
|
77 | 80 |
def __init__( |
78 |
- self, *, phrase: bytes | bytearray | str = b'', |
|
79 |
- length: int = 20, repeat: int = 0, lower: int | None = None, |
|
80 |
- upper: int | None = None, number: int | None = None, |
|
81 |
- space: int | None = None, dash: int | None = None, |
|
81 |
+ self, |
|
82 |
+ *, |
|
83 |
+ phrase: bytes | bytearray | str = b'', |
|
84 |
+ length: int = 20, |
|
85 |
+ repeat: int = 0, |
|
86 |
+ lower: int | None = None, |
|
87 |
+ upper: int | None = None, |
|
88 |
+ number: int | None = None, |
|
89 |
+ space: int | None = None, |
|
90 |
+ dash: int | None = None, |
|
82 | 91 |
symbol: int | None = None, |
83 | 92 |
) -> None: |
84 | 93 |
"""Initialize the Vault object. |
... | ... |
@@ -177,7 +188,8 @@ class Vault: |
177 | 188 |
return math.fsum(math.log2(f) for f in factors) |
178 | 189 |
|
179 | 190 |
def _estimate_sufficient_hash_length( |
180 |
- self, safety_factor: float = 2.0, |
|
191 |
+ self, |
|
192 |
+ safety_factor: float = 2.0, |
|
181 | 193 |
) -> int: |
182 | 194 |
"""Estimate the sufficient hash length, given the current settings. |
183 | 195 |
|
... | ... |
@@ -241,8 +253,11 @@ class Vault: |
241 | 253 |
|
242 | 254 |
@classmethod |
243 | 255 |
def create_hash( |
244 |
- cls, phrase: bytes | bytearray | str, |
|
245 |
- service: bytes | bytearray, *, length: int = 32, |
|
256 |
+ cls, |
|
257 |
+ phrase: bytes | bytearray | str, |
|
258 |
+ service: bytes | bytearray, |
|
259 |
+ *, |
|
260 |
+ length: int = 32, |
|
246 | 261 |
) -> bytes: |
247 | 262 |
r"""Create a pseudorandom byte stream from phrase and service. |
248 | 263 |
|
... | ... |
@@ -302,11 +317,19 @@ class Vault: |
302 | 317 |
phrase = cls._get_binary_string(phrase) |
303 | 318 |
assert not isinstance(phrase, str) |
304 | 319 |
salt = bytes(service) + cls._UUID |
305 |
- return hashlib.pbkdf2_hmac(hash_name='sha1', password=phrase, |
|
306 |
- salt=salt, iterations=8, dklen=length) |
|
320 |
+ return hashlib.pbkdf2_hmac( |
|
321 |
+ hash_name='sha1', |
|
322 |
+ password=phrase, |
|
323 |
+ salt=salt, |
|
324 |
+ iterations=8, |
|
325 |
+ dklen=length, |
|
326 |
+ ) |
|
307 | 327 |
|
308 | 328 |
def generate( |
309 |
- self, service_name: str | bytes | bytearray, /, *, |
|
329 |
+ self, |
|
330 |
+ service_name: str | bytes | bytearray, |
|
331 |
+ /, |
|
332 |
+ *, |
|
310 | 333 |
phrase: bytes | bytearray | str = b'', |
311 | 334 |
) -> bytes: |
312 | 335 |
r"""Generate a service passphrase. |
... | ... |
@@ -358,8 +381,11 @@ class Vault: |
358 | 381 |
while True: |
359 | 382 |
try: |
360 | 383 |
required = self._required[:] |
361 |
- seq = sequin.Sequin(self.create_hash( |
|
362 |
- phrase=phrase, service=service_name, length=hash_length)) |
|
384 |
+ seq = sequin.Sequin( |
|
385 |
+ self.create_hash( |
|
386 |
+ phrase=phrase, service=service_name, length=hash_length |
|
387 |
+ ) |
|
388 |
+ ) |
|
363 | 389 |
result = bytearray() |
364 | 390 |
while len(result) < self._length: |
365 | 391 |
pos = seq.generate(len(required)) |
... | ... |
@@ -374,8 +400,9 @@ class Vault: |
374 | 400 |
if self._repeat and result: |
375 | 401 |
bad_suffix = bytes(result[-1:]) * (self._repeat - 1) |
376 | 402 |
if result.endswith(bad_suffix): |
377 |
- charset = self._subtract(bytes(result[-1:]), |
|
378 |
- charset) |
|
403 |
+ charset = self._subtract( |
|
404 |
+ bytes(result[-1:]), charset |
|
405 |
+ ) |
|
379 | 406 |
pos = seq.generate(len(charset)) |
380 | 407 |
result.extend(charset[pos : pos + 1]) |
381 | 408 |
except sequin.SequinExhaustedError: |
... | ... |
@@ -399,19 +426,16 @@ class Vault: |
399 | 426 |
|
400 | 427 |
""" |
401 | 428 |
deterministic_signature_types = { |
402 |
- 'ssh-ed25519': |
|
403 |
- lambda k: k.startswith(b'\x00\x00\x00\x0bssh-ed25519'), |
|
404 |
- 'ssh-ed448': |
|
405 |
- lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'), |
|
406 |
- 'ssh-rsa': |
|
407 |
- lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'), |
|
429 |
+ 'ssh-ed25519': lambda k: k.startswith( |
|
430 |
+ b'\x00\x00\x00\x0bssh-ed25519' |
|
431 |
+ ), |
|
432 |
+ 'ssh-ed448': lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'), |
|
433 |
+ 'ssh-rsa': lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'), |
|
408 | 434 |
} |
409 | 435 |
return any(v(key) for v in deterministic_signature_types.values()) |
410 | 436 |
|
411 | 437 |
@classmethod |
412 |
- def phrase_from_key( |
|
413 |
- cls, key: bytes | bytearray, / |
|
414 |
- ) -> bytes: |
|
438 |
+ def phrase_from_key(cls, key: bytes | bytearray, /) -> bytes: |
|
415 | 439 |
"""Obtain the master passphrase from a configured SSH key. |
416 | 440 |
|
417 | 441 |
vault allows the usage of certain SSH keys to derive a master |
... | ... |
@@ -456,8 +480,10 @@ class Vault: |
456 | 480 |
|
457 | 481 |
""" |
458 | 482 |
if not cls._is_suitable_ssh_key(key): |
459 |
- msg = ('unsuitable SSH key: bad key, or ' |
|
460 |
- 'signature not deterministic') |
|
483 |
+ msg = ( |
|
484 |
+ 'unsuitable SSH key: bad key, or ' |
|
485 |
+ 'signature not deterministic' |
|
486 |
+ ) |
|
461 | 487 |
raise ValueError(msg) |
462 | 488 |
with ssh_agent_client.SSHAgentClient() as client: |
463 | 489 |
raw_sig = client.sign(key, cls._UUID) |
... | ... |
@@ -467,7 +493,8 @@ class Vault: |
467 | 493 |
|
468 | 494 |
@staticmethod |
469 | 495 |
def _subtract( |
470 |
- charset: bytes | bytearray, allowed: bytes | bytearray, |
|
496 |
+ charset: bytes | bytearray, |
|
497 |
+ allowed: bytes | bytearray, |
|
471 | 498 |
) -> bytearray: |
472 | 499 |
"""Remove the characters in charset from allowed. |
473 | 500 |
|
... | ... |
@@ -489,8 +516,9 @@ class Vault: |
489 | 516 |
`allowed` or `charset` contained duplicate characters. |
490 | 517 |
|
491 | 518 |
""" |
492 |
- allowed = (allowed if isinstance(allowed, bytearray) |
|
493 |
- else bytearray(allowed)) |
|
519 |
+ allowed = ( |
|
520 |
+ allowed if isinstance(allowed, bytearray) else bytearray(allowed) |
|
521 |
+ ) |
|
494 | 522 |
assert_type(allowed, bytearray) |
495 | 523 |
msg_dup_characters = 'duplicate characters in set' |
496 | 524 |
if len(frozenset(allowed)) != len(allowed): |
... | ... |
@@ -2,9 +2,7 @@ |
2 | 2 |
# |
3 | 3 |
# SPDX-License-Identifier: MIT |
4 | 4 |
|
5 |
-"""Command-line interface for derivepassphrase. |
|
6 |
- |
|
7 |
-""" |
|
5 |
+"""Command-line interface for derivepassphrase.""" |
|
8 | 6 |
|
9 | 7 |
from __future__ import annotations |
10 | 8 |
|
... | ... |
@@ -64,8 +62,9 @@ def _config_filename() -> str | bytes | pathlib.Path: |
64 | 62 |
|
65 | 63 |
""" |
66 | 64 |
path: str | bytes | pathlib.Path |
67 |
- path = (os.getenv(PROG_NAME.upper() + '_PATH') |
|
68 |
- or click.get_app_dir(PROG_NAME, force_posix=True)) |
|
65 |
+ path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir( |
|
66 |
+ PROG_NAME, force_posix=True |
|
67 |
+ ) |
|
69 | 68 |
return os.path.join(path, 'settings.json') |
70 | 69 |
|
71 | 70 |
|
... | ... |
@@ -122,8 +121,7 @@ def _save_config(config: dpp_types.VaultConfig, /) -> None: |
122 | 121 |
|
123 | 122 |
|
124 | 123 |
def _get_suitable_ssh_keys( |
125 |
- conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, |
|
126 |
- / |
|
124 |
+ conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, / |
|
127 | 125 |
) -> Iterator[ssh_agent_client.types.KeyCommentPair]: |
128 | 126 |
"""Yield all SSH keys suitable for passphrase derivation. |
129 | 127 |
|
... | ... |
@@ -188,7 +186,8 @@ def _get_suitable_ssh_keys( |
188 | 186 |
|
189 | 187 |
|
190 | 188 |
def _prompt_for_selection( |
191 |
- items: Sequence[str | bytes], heading: str = 'Possible choices:', |
|
189 |
+ items: Sequence[str | bytes], |
|
190 |
+ heading: str = 'Possible choices:', |
|
192 | 191 |
single_choice_prompt: str = 'Confirm this choice?', |
193 | 192 |
) -> int: |
194 | 193 |
"""Prompt user for a choice among the given items. |
... | ... |
@@ -229,26 +228,34 @@ def _prompt_for_selection( |
229 | 228 |
choices = click.Choice([''] + [str(i) for i in range(1, n + 1)]) |
230 | 229 |
choice = click.prompt( |
231 | 230 |
f'Your selection? (1-{n}, leave empty to abort)', |
232 |
- err=True, type=choices, show_choices=False, |
|
233 |
- show_default=False, default='') |
|
231 |
+ err=True, |
|
232 |
+ type=choices, |
|
233 |
+ show_choices=False, |
|
234 |
+ show_default=False, |
|
235 |
+ default='', |
|
236 |
+ ) |
|
234 | 237 |
if not choice: |
235 | 238 |
raise IndexError(_EMPTY_SELECTION) |
236 | 239 |
return int(choice) - 1 |
237 |
- prompt_suffix = (' ' |
|
238 |
- if single_choice_prompt.endswith(tuple('?.!')) |
|
239 |
- else ': ') |
|
240 |
+ prompt_suffix = ( |
|
241 |
+ ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': ' |
|
242 |
+ ) |
|
240 | 243 |
try: |
241 |
- click.confirm(single_choice_prompt, |
|
242 |
- prompt_suffix=prompt_suffix, err=True, |
|
243 |
- abort=True, default=False, show_default=False) |
|
244 |
+ click.confirm( |
|
245 |
+ single_choice_prompt, |
|
246 |
+ prompt_suffix=prompt_suffix, |
|
247 |
+ err=True, |
|
248 |
+ abort=True, |
|
249 |
+ default=False, |
|
250 |
+ show_default=False, |
|
251 |
+ ) |
|
244 | 252 |
except click.Abort: |
245 | 253 |
raise IndexError(_EMPTY_SELECTION) from None |
246 | 254 |
return 0 |
247 | 255 |
|
248 | 256 |
|
249 | 257 |
def _select_ssh_key( |
250 |
- conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, |
|
251 |
- / |
|
258 |
+ conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, / |
|
252 | 259 |
) -> bytes | bytearray: |
253 | 260 |
"""Interactively select an SSH key for passphrase derivation. |
254 | 261 |
|
... | ... |
@@ -292,14 +299,18 @@ def _select_ssh_key( |
292 | 299 |
for key, comment in suitable_keys: |
293 | 300 |
keytype = unstring_prefix(key)[0].decode('ASCII') |
294 | 301 |
key_str = base64.standard_b64encode(key).decode('ASCII') |
295 |
- key_prefix = (key_str |
|
302 |
+ key_prefix = ( |
|
303 |
+ key_str |
|
296 | 304 |
if len(key_str) < KEY_DISPLAY_LENGTH + len('...') |
297 |
- else key_str[:KEY_DISPLAY_LENGTH] + '...') |
|
305 |
+ else key_str[:KEY_DISPLAY_LENGTH] + '...' |
|
306 |
+ ) |
|
298 | 307 |
comment_str = comment.decode('UTF-8', errors='replace') |
299 | 308 |
key_listing.append(f'{keytype} {key_prefix} {comment_str}') |
300 | 309 |
choice = _prompt_for_selection( |
301 |
- key_listing, heading='Suitable SSH keys:', |
|
302 |
- single_choice_prompt='Use this key?') |
|
310 |
+ key_listing, |
|
311 |
+ heading='Suitable SSH keys:', |
|
312 |
+ single_choice_prompt='Use this key?', |
|
313 |
+ ) |
|
303 | 314 |
return suitable_keys[choice].key |
304 | 315 |
|
305 | 316 |
|
... | ... |
@@ -313,8 +324,9 @@ def _prompt_for_passphrase() -> str: |
313 | 324 |
The user input. |
314 | 325 |
|
315 | 326 |
""" |
316 |
- return click.prompt('Passphrase', default='', hide_input=True, |
|
317 |
- show_default=False, err=True) |
|
327 |
+ return click.prompt( |
|
328 |
+ 'Passphrase', default='', hide_input=True, show_default=False, err=True |
|
329 |
+ ) |
|
318 | 330 |
|
319 | 331 |
|
320 | 332 |
class OptionGroupOption(click.Option): |
... | ... |
@@ -353,7 +366,9 @@ class CommandWithHelpGroups(click.Command): |
353 | 366 |
""" |
354 | 367 |
|
355 | 368 |
def format_options( |
356 |
- self, ctx: click.Context, formatter: click.HelpFormatter, |
|
369 |
+ self, |
|
370 |
+ ctx: click.Context, |
|
371 |
+ formatter: click.HelpFormatter, |
|
357 | 372 |
) -> None: |
358 | 373 |
r"""Format options on the help listing, grouped into sections. |
359 | 374 |
|
... | ... |
@@ -398,8 +413,9 @@ class CommandWithHelpGroups(click.Command): |
398 | 413 |
group_name = '' |
399 | 414 |
help_records.setdefault(group_name, []).append(rec) |
400 | 415 |
default_group = help_records.pop('') |
401 |
- default_group_name = ('Other Options' if len(default_group) > 1 |
|
402 |
- else 'Options') |
|
416 |
+ default_group_name = ( |
|
417 |
+ 'Other Options' if len(default_group) > 1 else 'Options' |
|
418 |
+ ) |
|
403 | 419 |
help_records[default_group_name] = default_group |
404 | 420 |
for group_name, records in help_records.items(): |
405 | 421 |
with formatter.section(group_name): |
... | ... |
@@ -414,31 +430,37 @@ class CommandWithHelpGroups(click.Command): |
414 | 430 |
# Concrete option groups used by this command-line interface. |
415 | 431 |
class PasswordGenerationOption(OptionGroupOption): |
416 | 432 |
"""Password generation options for the CLI.""" |
433 |
+ |
|
417 | 434 |
option_group_name = 'Password generation' |
418 |
- epilog = ''' |
|
435 |
+ epilog = """ |
|
419 | 436 |
Use NUMBER=0, e.g. "--symbol 0", to exclude a character type |
420 | 437 |
from the output. |
421 |
- ''' |
|
438 |
+ """ |
|
422 | 439 |
|
423 | 440 |
|
424 | 441 |
class ConfigurationOption(OptionGroupOption): |
425 | 442 |
"""Configuration options for the CLI.""" |
443 |
+ |
|
426 | 444 |
option_group_name = 'Configuration' |
427 |
- epilog = ''' |
|
445 |
+ epilog = """ |
|
428 | 446 |
Use $VISUAL or $EDITOR to configure the spawned editor. |
429 |
- ''' |
|
447 |
+ """ |
|
430 | 448 |
|
431 | 449 |
|
432 | 450 |
class StorageManagementOption(OptionGroupOption): |
433 | 451 |
"""Storage management options for the CLI.""" |
452 |
+ |
|
434 | 453 |
option_group_name = 'Storage management' |
435 |
- epilog = ''' |
|
454 |
+ epilog = """ |
|
436 | 455 |
Using "-" as PATH for standard input/standard output is |
437 | 456 |
supported. |
438 |
- ''' |
|
457 |
+ """ |
|
458 |
+ |
|
439 | 459 |
|
440 | 460 |
def _validate_occurrence_constraint( |
441 |
- ctx: click.Context, param: click.Parameter, value: Any, |
|
461 |
+ ctx: click.Context, |
|
462 |
+ param: click.Parameter, |
|
463 |
+ value: Any, |
|
442 | 464 |
) -> int | None: |
443 | 465 |
"""Check that the occurrence constraint is valid (int, 0 or larger).""" |
444 | 466 |
del ctx # Unused. |
... | ... |
@@ -460,7 +482,9 @@ def _validate_occurrence_constraint( |
460 | 482 |
|
461 | 483 |
|
462 | 484 |
def _validate_length( |
463 |
- ctx: click.Context, param: click.Parameter, value: Any, |
|
485 |
+ ctx: click.Context, |
|
486 |
+ param: click.Parameter, |
|
487 |
+ value: Any, |
|
464 | 488 |
) -> int | None: |
465 | 489 |
"""Check that the length is valid (int, 1 or larger).""" |
466 | 490 |
del ctx # Unused. |
... | ... |
@@ -480,7 +504,8 @@ def _validate_length( |
480 | 504 |
raise click.BadParameter(msg) |
481 | 505 |
return int_value |
482 | 506 |
|
483 |
-DEFAULT_NOTES_TEMPLATE = '''\ |
|
507 |
+ |
|
508 |
+DEFAULT_NOTES_TEMPLATE = """\ |
|
484 | 509 |
# Enter notes below the line with the cut mark (ASCII scissors and |
485 | 510 |
# dashes). Lines above the cut mark (such as this one) will be ignored. |
486 | 511 |
# |
... | ... |
@@ -490,14 +515,14 @@ DEFAULT_NOTES_TEMPLATE = '''\ |
490 | 515 |
# retained. |
491 | 516 |
# |
492 | 517 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
493 |
-''' |
|
518 |
+""" |
|
494 | 519 |
DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
495 | 520 |
|
496 | 521 |
|
497 | 522 |
@click.command( |
498 |
- context_settings={"help_option_names": ["-h", "--help"]}, |
|
523 |
+ context_settings={'help_option_names': ['-h', '--help']}, |
|
499 | 524 |
cls=CommandWithHelpGroups, |
500 |
- epilog=r''' |
|
525 |
+ epilog=r""" |
|
501 | 526 |
WARNING: There is NO WAY to retrieve the generated passphrases |
502 | 527 |
if the master passphrase, the SSH key, or the exact passphrase |
503 | 528 |
settings are lost, short of trying out all possible |
... | ... |
@@ -510,74 +535,145 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
510 | 535 |
`C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows. |
511 | 536 |
The configuration is NOT encrypted, and you are STRONGLY |
512 | 537 |
discouraged from using a stored passphrase. |
513 |
- ''', |
|
538 |
+ """, |
|
514 | 539 |
) |
515 |
-@click.option('-p', '--phrase', 'use_phrase', is_flag=True, |
|
540 |
+@click.option( |
|
541 |
+ '-p', |
|
542 |
+ '--phrase', |
|
543 |
+ 'use_phrase', |
|
544 |
+ is_flag=True, |
|
516 | 545 |
help='prompts you for your passphrase', |
517 |
- cls=PasswordGenerationOption) |
|
518 |
-@click.option('-k', '--key', 'use_key', is_flag=True, |
|
546 |
+ cls=PasswordGenerationOption, |
|
547 |
+) |
|
548 |
+@click.option( |
|
549 |
+ '-k', |
|
550 |
+ '--key', |
|
551 |
+ 'use_key', |
|
552 |
+ is_flag=True, |
|
519 | 553 |
help='uses your SSH private key to generate passwords', |
520 |
- cls=PasswordGenerationOption) |
|
521 |
-@click.option('-l', '--length', metavar='NUMBER', |
|
554 |
+ cls=PasswordGenerationOption, |
|
555 |
+) |
|
556 |
+@click.option( |
|
557 |
+ '-l', |
|
558 |
+ '--length', |
|
559 |
+ metavar='NUMBER', |
|
522 | 560 |
callback=_validate_length, |
523 | 561 |
help='emits password of length NUMBER', |
524 |
- cls=PasswordGenerationOption) |
|
525 |
-@click.option('-r', '--repeat', metavar='NUMBER', |
|
562 |
+ cls=PasswordGenerationOption, |
|
563 |
+) |
|
564 |
+@click.option( |
|
565 |
+ '-r', |
|
566 |
+ '--repeat', |
|
567 |
+ metavar='NUMBER', |
|
526 | 568 |
callback=_validate_occurrence_constraint, |
527 | 569 |
help='allows maximum of NUMBER repeated adjacent chars', |
528 |
- cls=PasswordGenerationOption) |
|
529 |
-@click.option('--lower', metavar='NUMBER', |
|
570 |
+ cls=PasswordGenerationOption, |
|
571 |
+) |
|
572 |
+@click.option( |
|
573 |
+ '--lower', |
|
574 |
+ metavar='NUMBER', |
|
530 | 575 |
callback=_validate_occurrence_constraint, |
531 | 576 |
help='includes at least NUMBER lowercase letters', |
532 |
- cls=PasswordGenerationOption) |
|
533 |
-@click.option('--upper', metavar='NUMBER', |
|
577 |
+ cls=PasswordGenerationOption, |
|
578 |
+) |
|
579 |
+@click.option( |
|
580 |
+ '--upper', |
|
581 |
+ metavar='NUMBER', |
|
534 | 582 |
callback=_validate_occurrence_constraint, |
535 | 583 |
help='includes at least NUMBER uppercase letters', |
536 |
- cls=PasswordGenerationOption) |
|
537 |
-@click.option('--number', metavar='NUMBER', |
|
584 |
+ cls=PasswordGenerationOption, |
|
585 |
+) |
|
586 |
+@click.option( |
|
587 |
+ '--number', |
|
588 |
+ metavar='NUMBER', |
|
538 | 589 |
callback=_validate_occurrence_constraint, |
539 | 590 |
help='includes at least NUMBER digits', |
540 |
- cls=PasswordGenerationOption) |
|
541 |
-@click.option('--space', metavar='NUMBER', |
|
591 |
+ cls=PasswordGenerationOption, |
|
592 |
+) |
|
593 |
+@click.option( |
|
594 |
+ '--space', |
|
595 |
+ metavar='NUMBER', |
|
542 | 596 |
callback=_validate_occurrence_constraint, |
543 | 597 |
help='includes at least NUMBER spaces', |
544 |
- cls=PasswordGenerationOption) |
|
545 |
-@click.option('--dash', metavar='NUMBER', |
|
598 |
+ cls=PasswordGenerationOption, |
|
599 |
+) |
|
600 |
+@click.option( |
|
601 |
+ '--dash', |
|
602 |
+ metavar='NUMBER', |
|
546 | 603 |
callback=_validate_occurrence_constraint, |
547 | 604 |
help='includes at least NUMBER "-" or "_"', |
548 |
- cls=PasswordGenerationOption) |
|
549 |
-@click.option('--symbol', metavar='NUMBER', |
|
605 |
+ cls=PasswordGenerationOption, |
|
606 |
+) |
|
607 |
+@click.option( |
|
608 |
+ '--symbol', |
|
609 |
+ metavar='NUMBER', |
|
550 | 610 |
callback=_validate_occurrence_constraint, |
551 | 611 |
help='includes at least NUMBER symbol chars', |
552 |
- cls=PasswordGenerationOption) |
|
553 |
-@click.option('-n', '--notes', 'edit_notes', is_flag=True, |
|
612 |
+ cls=PasswordGenerationOption, |
|
613 |
+) |
|
614 |
+@click.option( |
|
615 |
+ '-n', |
|
616 |
+ '--notes', |
|
617 |
+ 'edit_notes', |
|
618 |
+ is_flag=True, |
|
554 | 619 |
help='spawn an editor to edit notes for SERVICE', |
555 |
- cls=ConfigurationOption) |
|
556 |
-@click.option('-c', '--config', 'store_config_only', is_flag=True, |
|
620 |
+ cls=ConfigurationOption, |
|
621 |
+) |
|
622 |
+@click.option( |
|
623 |
+ '-c', |
|
624 |
+ '--config', |
|
625 |
+ 'store_config_only', |
|
626 |
+ is_flag=True, |
|
557 | 627 |
help='saves the given settings for SERVICE or global', |
558 |
- cls=ConfigurationOption) |
|
559 |
-@click.option('-x', '--delete', 'delete_service_settings', is_flag=True, |
|
628 |
+ cls=ConfigurationOption, |
|
629 |
+) |
|
630 |
+@click.option( |
|
631 |
+ '-x', |
|
632 |
+ '--delete', |
|
633 |
+ 'delete_service_settings', |
|
634 |
+ is_flag=True, |
|
560 | 635 |
help='deletes settings for SERVICE', |
561 |
- cls=ConfigurationOption) |
|
562 |
-@click.option('--delete-globals', is_flag=True, |
|
636 |
+ cls=ConfigurationOption, |
|
637 |
+) |
|
638 |
+@click.option( |
|
639 |
+ '--delete-globals', |
|
640 |
+ is_flag=True, |
|
563 | 641 |
help='deletes the global shared settings', |
564 |
- cls=ConfigurationOption) |
|
565 |
-@click.option('-X', '--clear', 'clear_all_settings', is_flag=True, |
|
642 |
+ cls=ConfigurationOption, |
|
643 |
+) |
|
644 |
+@click.option( |
|
645 |
+ '-X', |
|
646 |
+ '--clear', |
|
647 |
+ 'clear_all_settings', |
|
648 |
+ is_flag=True, |
|
566 | 649 |
help='deletes all settings', |
567 |
- cls=ConfigurationOption) |
|
568 |
-@click.option('-e', '--export', 'export_settings', metavar='PATH', |
|
650 |
+ cls=ConfigurationOption, |
|
651 |
+) |
|
652 |
+@click.option( |
|
653 |
+ '-e', |
|
654 |
+ '--export', |
|
655 |
+ 'export_settings', |
|
656 |
+ metavar='PATH', |
|
569 | 657 |
type=click.Path(file_okay=True, allow_dash=True, exists=False), |
570 | 658 |
help='export all saved settings into file PATH', |
571 |
- cls=StorageManagementOption) |
|
572 |
-@click.option('-i', '--import', 'import_settings', metavar='PATH', |
|
659 |
+ cls=StorageManagementOption, |
|
660 |
+) |
|
661 |
+@click.option( |
|
662 |
+ '-i', |
|
663 |
+ '--import', |
|
664 |
+ 'import_settings', |
|
665 |
+ metavar='PATH', |
|
573 | 666 |
type=click.Path(file_okay=True, allow_dash=True, exists=False), |
574 | 667 |
help='import saved settings from file PATH', |
575 |
- cls=StorageManagementOption) |
|
668 |
+ cls=StorageManagementOption, |
|
669 |
+) |
|
576 | 670 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
577 | 671 |
@click.argument('service', required=False) |
578 | 672 |
@click.pass_context |
579 | 673 |
def derivepassphrase( |
580 |
- ctx: click.Context, /, *, |
|
674 |
+ ctx: click.Context, |
|
675 |
+ /, |
|
676 |
+ *, |
|
581 | 677 |
service: str | None = None, |
582 | 678 |
use_phrase: bool = False, |
583 | 679 |
use_key: bool = False, |
... | ... |
@@ -704,7 +800,8 @@ def derivepassphrase( |
704 | 800 |
group = StorageManagementOption |
705 | 801 |
case OptionGroupOption(): |
706 | 802 |
raise AssertionError( # noqa: TRY003 |
707 |
- f'Unknown option group for {param!r}') # noqa: EM102 |
|
803 |
+ f'Unknown option group for {param!r}' # noqa: EM102 |
|
804 |
+ ) |
|
708 | 805 |
case _: |
709 | 806 |
group = click.Option |
710 | 807 |
options_in_group.setdefault(group, []).append(param) |
... | ... |
@@ -716,7 +813,8 @@ def derivepassphrase( |
716 | 813 |
return bool(ctx.params.get(param.human_readable_name)) |
717 | 814 |
|
718 | 815 |
def check_incompatible_options( |
719 |
- param: click.Parameter | str, *incompatible: click.Parameter | str, |
|
816 |
+ param: click.Parameter | str, |
|
817 |
+ *incompatible: click.Parameter | str, |
|
720 | 818 |
) -> None: |
721 | 819 |
if isinstance(param, str): |
722 | 820 |
param = params_by_str[param] |
... | ... |
@@ -731,7 +829,8 @@ def derivepassphrase( |
731 | 829 |
opt_str = param.opts[0] |
732 | 830 |
other_str = other.opts[0] |
733 | 831 |
raise click.BadOptionUsage( |
734 |
- opt_str, f'mutually exclusive with {other_str}', ctx=ctx) |
|
832 |
+ opt_str, f'mutually exclusive with {other_str}', ctx=ctx |
|
833 |
+ ) |
|
735 | 834 |
|
736 | 835 |
def get_config() -> dpp_types.VaultConfig: |
737 | 836 |
try: |
... | ... |
@@ -748,15 +847,20 @@ def derivepassphrase( |
748 | 847 |
for opt in options_in_group[group]: |
749 | 848 |
if opt != params_by_str['--config']: |
750 | 849 |
check_incompatible_options( |
751 |
- opt, *options_in_group[PasswordGenerationOption]) |
|
850 |
+ opt, *options_in_group[PasswordGenerationOption] |
|
851 |
+ ) |
|
752 | 852 |
|
753 | 853 |
for group in (ConfigurationOption, StorageManagementOption): |
754 | 854 |
for opt in options_in_group[group]: |
755 | 855 |
check_incompatible_options( |
756 |
- opt, *options_in_group[ConfigurationOption], |
|
757 |
- *options_in_group[StorageManagementOption]) |
|
758 |
- sv_options = (options_in_group[PasswordGenerationOption] + |
|
759 |
- [params_by_str['--notes'], params_by_str['--delete']]) |
|
856 |
+ opt, |
|
857 |
+ *options_in_group[ConfigurationOption], |
|
858 |
+ *options_in_group[StorageManagementOption], |
|
859 |
+ ) |
|
860 |
+ sv_options = options_in_group[PasswordGenerationOption] + [ |
|
861 |
+ params_by_str['--notes'], |
|
862 |
+ params_by_str['--delete'], |
|
863 |
+ ] |
|
760 | 864 |
sv_options.remove(params_by_str['--key']) |
761 | 865 |
sv_options.remove(params_by_str['--phrase']) |
762 | 866 |
for param in sv_options: |
... | ... |
@@ -765,16 +869,17 @@ def derivepassphrase( |
765 | 869 |
msg = f'{opt_str} requires a SERVICE' |
766 | 870 |
raise click.UsageError(msg) |
767 | 871 |
for param in [params_by_str['--key'], params_by_str['--phrase']]: |
768 |
- if ( |
|
769 |
- is_param_set(param) |
|
770 |
- and not (service or is_param_set(params_by_str['--config'])) |
|
872 |
+ if is_param_set(param) and not ( |
|
873 |
+ service or is_param_set(params_by_str['--config']) |
|
771 | 874 |
): |
772 | 875 |
opt_str = param.opts[0] |
773 | 876 |
msg = f'{opt_str} requires a SERVICE or --config' |
774 | 877 |
raise click.UsageError(msg) |
775 |
- no_sv_options = [params_by_str['--delete-globals'], |
|
878 |
+ no_sv_options = [ |
|
879 |
+ params_by_str['--delete-globals'], |
|
776 | 880 |
params_by_str['--clear'], |
777 |
- *options_in_group[StorageManagementOption]] |
|
881 |
+ *options_in_group[StorageManagementOption], |
|
882 |
+ ] |
|
778 | 883 |
for param in no_sv_options: |
779 | 884 |
if is_param_set(param) and service: |
780 | 885 |
opt_str = param.opts[0] |
... | ... |
@@ -784,10 +889,9 @@ def derivepassphrase( |
784 | 889 |
if edit_notes: |
785 | 890 |
assert service is not None |
786 | 891 |
configuration = get_config() |
787 |
- text = (DEFAULT_NOTES_TEMPLATE + |
|
788 |
- configuration['services'] |
|
789 |
- .get(service, cast(dpp_types.VaultConfigServicesSettings, {})) |
|
790 |
- .get('notes', '')) |
|
892 |
+ text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get( |
|
893 |
+ service, cast(dpp_types.VaultConfigServicesSettings, {}) |
|
894 |
+ ).get('notes', '') |
|
791 | 895 |
notes_value = click.edit(text=text) |
792 | 896 |
if notes_value is not None: |
793 | 897 |
notes_lines = collections.deque(notes_value.splitlines(True)) |
... | ... |
@@ -800,7 +904,8 @@ def derivepassphrase( |
800 | 904 |
if not notes_value.strip(): |
801 | 905 |
ctx.fail('not saving new notes: user aborted request') |
802 | 906 |
configuration['services'].setdefault(service, {})['notes'] = ( |
803 |
- notes_value.strip('\n')) |
|
907 |
+ notes_value.strip('\n') |
|
908 |
+ ) |
|
804 | 909 |
_save_config(configuration) |
805 | 910 |
elif delete_service_settings: |
806 | 911 |
assert service is not None |
... | ... |
@@ -818,9 +923,11 @@ def derivepassphrase( |
818 | 923 |
elif import_settings: |
819 | 924 |
try: |
820 | 925 |
# TODO: keep track of auto-close; try os.dup if feasible |
821 |
- infile = (cast(TextIO, import_settings) |
|
926 |
+ infile = ( |
|
927 |
+ cast(TextIO, import_settings) |
|
822 | 928 |
if hasattr(import_settings, 'close') |
823 |
- else click.open_file(os.fspath(import_settings), 'rt')) |
|
929 |
+ else click.open_file(os.fspath(import_settings), 'rt') |
|
930 |
+ ) |
|
824 | 931 |
with infile: |
825 | 932 |
maybe_config = json.load(infile) |
826 | 933 |
except json.JSONDecodeError as e: |
... | ... |
@@ -835,9 +942,11 @@ def derivepassphrase( |
835 | 942 |
configuration = get_config() |
836 | 943 |
try: |
837 | 944 |
# TODO: keep track of auto-close; try os.dup if feasible |
838 |
- outfile = (cast(TextIO, export_settings) |
|
945 |
+ outfile = ( |
|
946 |
+ cast(TextIO, export_settings) |
|
839 | 947 |
if hasattr(export_settings, 'close') |
840 |
- else click.open_file(os.fspath(export_settings), 'wt')) |
|
948 |
+ else click.open_file(os.fspath(export_settings), 'wt') |
|
949 |
+ ) |
|
841 | 950 |
with outfile: |
842 | 951 |
json.dump(configuration, outfile) |
843 | 952 |
except OSError as e: |
... | ... |
@@ -849,20 +958,36 @@ def derivepassphrase( |
849 | 958 |
# have a type guarding function anyway, assert that we didn't |
850 | 959 |
# make any mistakes at the end instead. |
851 | 960 |
global_keys = {'key', 'phrase'} |
852 |
- service_keys = {'key', 'phrase', 'length', 'repeat', 'lower', |
|
853 |
- 'upper', 'number', 'space', 'dash', 'symbol'} |
|
961 |
+ service_keys = { |
|
962 |
+ 'key', |
|
963 |
+ 'phrase', |
|
964 |
+ 'length', |
|
965 |
+ 'repeat', |
|
966 |
+ 'lower', |
|
967 |
+ 'upper', |
|
968 |
+ 'number', |
|
969 |
+ 'space', |
|
970 |
+ 'dash', |
|
971 |
+ 'symbol', |
|
972 |
+ } |
|
854 | 973 |
settings: collections.ChainMap[str, Any] = collections.ChainMap( |
855 |
- {k: v for k, v in locals().items() |
|
856 |
- if k in service_keys and v is not None}, |
|
857 |
- cast(dict[str, Any], |
|
858 |
- configuration['services'].get(service or '', {})), |
|
974 |
+ { |
|
975 |
+ k: v |
|
976 |
+ for k, v in locals().items() |
|
977 |
+ if k in service_keys and v is not None |
|
978 |
+ }, |
|
979 |
+ cast( |
|
980 |
+ dict[str, Any], |
|
981 |
+ configuration['services'].get(service or '', {}), |
|
982 |
+ ), |
|
859 | 983 |
{}, |
860 |
- cast(dict[str, Any], configuration.get('global', {})) |
|
984 |
+ cast(dict[str, Any], configuration.get('global', {})), |
|
861 | 985 |
) |
862 | 986 |
if use_key: |
863 | 987 |
try: |
864 |
- key = base64.standard_b64encode( |
|
865 |
- _select_ssh_key()).decode('ASCII') |
|
988 |
+ key = base64.standard_b64encode(_select_ssh_key()).decode( |
|
989 |
+ 'ASCII' |
|
990 |
+ ) |
|
866 | 991 |
except IndexError: |
867 | 992 |
ctx.fail('no valid SSH key selected') |
868 | 993 |
except (LookupError, RuntimeError) as e: |
... | ... |
@@ -875,8 +1000,11 @@ def derivepassphrase( |
875 | 1000 |
phrase = maybe_phrase |
876 | 1001 |
if store_config_only: |
877 | 1002 |
view: collections.ChainMap[str, Any] |
878 |
- view = (collections.ChainMap(*settings.maps[:2]) if service |
|
879 |
- else settings.parents.parents) |
|
1003 |
+ view = ( |
|
1004 |
+ collections.ChainMap(*settings.maps[:2]) |
|
1005 |
+ if service |
|
1006 |
+ else settings.parents.parents |
|
1007 |
+ ) |
|
880 | 1008 |
if use_key: |
881 | 1009 |
view['key'] = key |
882 | 1010 |
for m in view.maps: |
... | ... |
@@ -887,25 +1015,29 @@ def derivepassphrase( |
887 | 1015 |
m.pop('key', '') |
888 | 1016 |
if not view.maps[0]: |
889 | 1017 |
settings_type = 'service' if service else 'global' |
890 |
- msg = (f'cannot update {settings_type} settings without ' |
|
891 |
- f'actual settings') |
|
1018 |
+ msg = ( |
|
1019 |
+ f'cannot update {settings_type} settings without ' |
|
1020 |
+ f'actual settings' |
|
1021 |
+ ) |
|
892 | 1022 |
raise click.UsageError(msg) |
893 | 1023 |
if service: |
894 |
- configuration['services'].setdefault( |
|
895 |
- service, {}).update(view) # type: ignore[typeddict-item] |
|
1024 |
+ configuration['services'].setdefault(service, {}).update(view) # type: ignore[typeddict-item] |
|
896 | 1025 |
else: |
897 |
- configuration.setdefault( |
|
898 |
- 'global', {}).update(view) # type: ignore[typeddict-item] |
|
899 |
- assert dpp_types.is_vault_config(configuration), ( |
|
900 |
- f'invalid vault configuration: {configuration!r}' |
|
901 |
- ) |
|
1026 |
+ configuration.setdefault('global', {}).update(view) # type: ignore[typeddict-item] |
|
1027 |
+ assert dpp_types.is_vault_config( |
|
1028 |
+ configuration |
|
1029 |
+ ), f'invalid vault configuration: {configuration!r}' |
|
902 | 1030 |
_save_config(configuration) |
903 | 1031 |
else: |
904 | 1032 |
if not service: |
905 | 1033 |
msg = 'SERVICE is required' |
906 | 1034 |
raise click.UsageError(msg) |
907 |
- kwargs: dict[str, Any] = {k: v for k, v in settings.items() |
|
908 |
- if k in service_keys and v is not None} |
|
1035 |
+ kwargs: dict[str, Any] = { |
|
1036 |
+ k: v |
|
1037 |
+ for k, v in settings.items() |
|
1038 |
+ if k in service_keys and v is not None |
|
1039 |
+ } |
|
1040 |
+ |
|
909 | 1041 |
# If either --key or --phrase are given, use that setting. |
910 | 1042 |
# Otherwise, if both key and phrase are set in the config, |
911 | 1043 |
# one must be global (ignore it) and one must be |
... | ... |
@@ -915,10 +1047,12 @@ def derivepassphrase( |
915 | 1047 |
# derivepassphrase.Vault.phrase_from_key if a key is |
916 | 1048 |
# given. Finally, if nothing is set, error out. |
917 | 1049 |
def key_to_phrase( |
918 |
- key: str | bytes | bytearray |
|
1050 |
+ key: str | bytes | bytearray, |
|
919 | 1051 |
) -> bytes | bytearray: |
920 | 1052 |
return dpp.Vault.phrase_from_key( |
921 |
- base64.standard_b64decode(key)) |
|
1053 |
+ base64.standard_b64decode(key) |
|
1054 |
+ ) |
|
1055 |
+ |
|
922 | 1056 |
if use_key or use_phrase: |
923 | 1057 |
if use_key: |
924 | 1058 |
kwargs['phrase'] = key_to_phrase(key) |
... | ... |
@@ -935,8 +1069,10 @@ def derivepassphrase( |
935 | 1069 |
elif kwargs.get('phrase'): |
936 | 1070 |
pass |
937 | 1071 |
else: |
938 |
- msg = ('no passphrase or key given on command-line ' |
|
939 |
- 'or in configuration') |
|
1072 |
+ msg = ( |
|
1073 |
+ 'no passphrase or key given on command-line ' |
|
1074 |
+ 'or in configuration' |
|
1075 |
+ ) |
|
940 | 1076 |
raise click.UsageError(msg) |
941 | 1077 |
vault = dpp.Vault(**kwargs) |
942 | 1078 |
result = vault.generate(service) |
... | ... |
@@ -2,9 +2,7 @@ |
2 | 2 |
# |
3 | 3 |
# SPDX-License-Identifier: MIT |
4 | 4 |
|
5 |
-"""Common typing declarations for the parent module. |
|
6 |
- |
|
7 |
-""" |
|
5 |
+"""Common typing declarations for the parent module.""" |
|
8 | 6 |
|
9 | 7 |
from __future__ import annotations |
10 | 8 |
|
... | ... |
@@ -79,9 +80,13 @@ class VaultConfigServicesSettings(VaultConfigGlobalSettings, total=False): |
79 | 80 |
symbol: NotRequired[int] |
80 | 81 |
|
81 | 82 |
|
82 |
-_VaultConfig = TypedDict('_VaultConfig', |
|
83 |
+_VaultConfig = TypedDict( |
|
84 |
+ '_VaultConfig', |
|
83 | 85 |
{'global': NotRequired[VaultConfigGlobalSettings]}, |
84 |
- total=False) |
|
86 |
+ total=False, |
|
87 |
+) |
|
88 |
+ |
|
89 |
+ |
|
85 | 90 |
class VaultConfig(TypedDict, _VaultConfig, total=False): |
86 | 91 |
r"""Configuration for vault. |
87 | 92 |
|
... | ... |
@@ -32,8 +32,9 @@ if TYPE_CHECKING: |
32 | 32 |
from collections.abc import Iterator, Sequence |
33 | 33 |
|
34 | 34 |
__all__ = ('Sequin', 'SequinExhaustedError') |
35 |
-__author__ = "Marco Ricci <m@the13thletter.info>" |
|
36 |
-__version__ = "0.1.1" |
|
35 |
+__author__ = 'Marco Ricci <m@the13thletter.info>' |
|
36 |
+__version__ = '0.1.1' |
|
37 |
+ |
|
37 | 38 |
|
38 | 39 |
class Sequin: |
39 | 40 |
"""Generate pseudorandom non-negative numbers in different ranges. |
... | ... |
@@ -60,7 +62,9 @@ class Sequin: |
60 | 62 |
def __init__( |
61 | 63 |
self, |
62 | 64 |
sequence: str | bytes | bytearray | Sequence[int], |
63 |
- /, *, is_bitstring: bool = False |
|
65 |
+ /, |
|
66 |
+ *, |
|
67 |
+ is_bitstring: bool = False, |
|
64 | 68 |
): |
65 | 69 |
"""Initialize the Sequin. |
66 | 70 |
|
... | ... |
@@ -126,8 +134,7 @@ class Sequin: |
126 | 134 |
sequences. |
127 | 135 |
|
128 | 136 |
Examples: |
129 |
- >>> seq = Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], |
|
130 |
- ... is_bitstring=True) |
|
137 |
+ >>> seq = Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
|
131 | 138 |
>>> seq.bases |
132 | 139 |
{2: deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])} |
133 | 140 |
>>> seq._all_or_nothing_shift(3) |
... | ... |
@@ -163,9 +170,7 @@ class Sequin: |
163 | 170 |
return tuple(stash) |
164 | 171 |
|
165 | 172 |
@staticmethod |
166 |
- def _big_endian_number( |
|
167 |
- digits: Sequence[int], /, *, base: int = 2 |
|
168 |
- ) -> int: |
|
173 |
+ def _big_endian_number(digits: Sequence[int], /, *, base: int = 2) -> int: |
|
169 | 174 |
"""Evaluate the given integer sequence as a big endian number. |
170 | 175 |
|
171 | 176 |
Args: |
... | ... |
@@ -231,10 +236,14 @@ class Sequin: |
231 | 236 |
The sequin is exhausted. |
232 | 237 |
|
233 | 238 |
Examples: |
234 |
- >>> seq = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
235 |
- ... is_bitstring=True) |
|
236 |
- >>> seq2 = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
237 |
- ... is_bitstring=True) |
|
239 |
+ >>> seq = Sequin( |
|
240 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
241 |
+ ... is_bitstring=True, |
|
242 |
+ ... ) |
|
243 |
+ >>> seq2 = Sequin( |
|
244 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
245 |
+ ... is_bitstring=True, |
|
246 |
+ ... ) |
|
238 | 247 |
>>> seq.generate(5) |
239 | 248 |
3 |
240 | 249 |
>>> seq.generate(5) |
... | ... |
@@ -266,9 +275,7 @@ class Sequin: |
266 | 275 |
raise SequinExhaustedError |
267 | 276 |
return value |
268 | 277 |
|
269 |
- def _generate_inner( |
|
270 |
- self, n: int, /, *, base: int = 2 |
|
271 |
- ) -> int: |
|
278 |
+ def _generate_inner(self, n: int, /, *, base: int = 2) -> int: |
|
272 | 279 |
"""Recursive call to generate a base `n` non-negative integer. |
273 | 280 |
|
274 | 281 |
We first determine the correct exponent `k` to generate base `n` |
... | ... |
@@ -298,10 +305,14 @@ class Sequin: |
298 | 305 |
The range is empty. |
299 | 306 |
|
300 | 307 |
Examples: |
301 |
- >>> seq = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
302 |
- ... is_bitstring=True) |
|
303 |
- >>> seq2 = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
304 |
- ... is_bitstring=True) |
|
308 |
+ >>> seq = Sequin( |
|
309 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
310 |
+ ... is_bitstring=True, |
|
311 |
+ ... ) |
|
312 |
+ >>> seq2 = Sequin( |
|
313 |
+ ... [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
314 |
+ ... is_bitstring=True, |
|
315 |
+ ... ) |
|
305 | 316 |
>>> seq._generate_inner(5) |
306 | 317 |
3 |
307 | 318 |
>>> seq._generate_inner(5) |
... | ... |
@@ -21,8 +21,8 @@ if TYPE_CHECKING: |
21 | 21 |
from collections.abc import Sequence |
22 | 22 |
|
23 | 23 |
__all__ = ('SSHAgentClient',) |
24 |
-__author__ = "Marco Ricci <m@the13thletter.info>" |
|
25 |
-__version__ = "0.1.1" |
|
24 |
+__author__ = 'Marco Ricci <m@the13thletter.info>' |
|
25 |
+__version__ = '0.1.1' |
|
26 | 26 |
|
27 | 27 |
# In SSH bytestrings, the "length" of the byte string is stored as |
28 | 28 |
# a 4-byte/32-bit unsigned integer at the beginning. |
... | ... |
@@ -105,8 +110,7 @@ class SSHAgentClient: |
105 | 110 |
) -> bool: |
106 | 111 |
"""Close socket connection upon context manager completion.""" |
107 | 112 |
return bool( |
108 |
- self._connection.__exit__( |
|
109 |
- exc_type, exc_val, exc_tb) # type: ignore[func-returns-value] |
|
113 |
+ self._connection.__exit__(exc_type, exc_val, exc_tb) # type: ignore[func-returns-value] |
|
110 | 114 |
) |
111 | 115 |
|
112 | 116 |
@staticmethod |
... | ... |
@@ -172,17 +176,16 @@ class SSHAgentClient: |
172 | 176 |
Examples: |
173 | 177 |
>>> bytes(SSHAgentClient.unstring(b'\x00\x00\x00\x07ssh-rsa')) |
174 | 178 |
b'ssh-rsa' |
175 |
- >>> bytes(SSHAgentClient.unstring( |
|
176 |
- ... SSHAgentClient.string(b'ssh-ed25519'))) |
|
179 |
+ >>> bytes( |
|
180 |
+ ... SSHAgentClient.unstring(SSHAgentClient.string(b'ssh-ed25519')) |
|
181 |
+ ... ) |
|
177 | 182 |
b'ssh-ed25519' |
178 | 183 |
|
179 |
- """ |
|
184 |
+ """ # noqa: E501 |
|
180 | 185 |
n = len(bytestring) |
181 | 186 |
msg = 'malformed SSH byte string' |
182 |
- if ( |
|
183 |
- n < HEAD_LEN |
|
184 |
- or n != HEAD_LEN + int.from_bytes(bytestring[:HEAD_LEN], 'big', |
|
185 |
- signed=False) |
|
187 |
+ if n < HEAD_LEN or n != HEAD_LEN + int.from_bytes( |
|
188 |
+ bytestring[:HEAD_LEN], 'big', signed=False |
|
186 | 189 |
): |
187 | 190 |
raise ValueError(msg) |
188 | 191 |
return bytestring[HEAD_LEN:] |
... | ... |
@@ -209,11 +212,13 @@ class SSHAgentClient: |
209 | 212 |
|
210 | 213 |
Examples: |
211 | 214 |
>>> a, b = SSHAgentClient.unstring_prefix( |
212 |
- ... b'\x00\x00\x00\x07ssh-rsa____trailing data') |
|
215 |
+ ... b'\x00\x00\x00\x07ssh-rsa____trailing data' |
|
216 |
+ ... ) |
|
213 | 217 |
>>> (bytes(a), bytes(b)) |
214 | 218 |
(b'ssh-rsa', b'____trailing data') |
215 | 219 |
>>> a, b = SSHAgentClient.unstring_prefix( |
216 |
- ... SSHAgentClient.string(b'ssh-ed25519')) |
|
220 |
+ ... SSHAgentClient.string(b'ssh-ed25519') |
|
221 |
+ ... ) |
|
217 | 222 |
>>> (bytes(a), bytes(b)) |
218 | 223 |
(b'ssh-ed25519', b'') |
219 | 224 |
|
... | ... |
@@ -225,7 +230,10 @@ class SSHAgentClient: |
225 | 230 |
m = int.from_bytes(bytestring[:HEAD_LEN], 'big', signed=False) |
226 | 231 |
if m + HEAD_LEN > n: |
227 | 232 |
raise ValueError(msg) |
228 |
- return (bytestring[HEAD_LEN:m + HEAD_LEN], bytestring[m + HEAD_LEN:]) |
|
233 |
+ return ( |
|
234 |
+ bytestring[HEAD_LEN : m + HEAD_LEN], |
|
235 |
+ bytestring[m + HEAD_LEN :], |
|
236 |
+ ) |
|
229 | 237 |
|
230 | 238 |
def request( |
231 | 239 |
self, code: int, payload: bytes | bytearray, / |
... | ... |
@@ -282,12 +290,16 @@ class SSHAgentClient: |
282 | 290 |
|
283 | 291 |
""" |
284 | 292 |
response_code, response = self.request( |
285 |
- ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'') |
|
293 |
+ ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'' |
|
294 |
+ ) |
|
286 | 295 |
if response_code != ssh_types.SSH_AGENT.IDENTITIES_ANSWER.value: |
287 |
- msg = (f'error return from SSH agent: ' |
|
288 |
- f'{response_code = }, {response = }') |
|
296 |
+ msg = ( |
|
297 |
+ f'error return from SSH agent: ' |
|
298 |
+ f'{response_code = }, {response = }' |
|
299 |
+ ) |
|
289 | 300 |
raise RuntimeError(msg) |
290 | 301 |
response_stream = collections.deque(response) |
302 |
+ |
|
291 | 303 |
def shift(num: int) -> bytes: |
292 | 304 |
buf = collections.deque(b'') |
293 | 305 |
for _ in range(num): |
... | ... |
@@ -314,8 +327,13 @@ class SSHAgentClient: |
314 | 327 |
return keys |
315 | 328 |
|
316 | 329 |
def sign( |
317 |
- self, /, key: bytes | bytearray, payload: bytes | bytearray, |
|
318 |
- *, flags: int = 0, check_if_key_loaded: bool = False, |
|
330 |
+ self, |
|
331 |
+ /, |
|
332 |
+ key: bytes | bytearray, |
|
333 |
+ payload: bytes | bytearray, |
|
334 |
+ *, |
|
335 |
+ flags: int = 0, |
|
336 |
+ check_if_key_loaded: bool = False, |
|
319 | 337 |
) -> bytes | bytearray: |
320 | 338 |
"""Request the SSH agent sign the payload with the key. |
321 | 339 |
|
... | ... |
@@ -361,7 +379,8 @@ class SSHAgentClient: |
361 | 379 |
request_data.extend(self.string(payload)) |
362 | 380 |
request_data.extend(self.uint32(flags)) |
363 | 381 |
response_code, response = self.request( |
364 |
- ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data) |
|
382 |
+ ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data |
|
383 |
+ ) |
|
365 | 384 |
if response_code != ssh_types.SSH_AGENT.SIGN_RESPONSE.value: |
366 | 385 |
msg = f'signing data failed: {response_code = }, {response = }' |
367 | 386 |
raise RuntimeError(msg) |
... | ... |
@@ -33,38 +33,39 @@ if TYPE_CHECKING: |
33 | 33 |
expected_signature: bytes | None |
34 | 34 |
derived_passphrase: bytes | str | None |
35 | 35 |
|
36 |
+ |
|
36 | 37 |
SUPPORTED_KEYS: Mapping[str, SSHTestKey] = { |
37 | 38 |
'ed25519': { |
38 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
39 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
39 | 40 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW |
40 | 41 |
QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju |
41 | 42 |
xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg |
42 | 43 |
AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw |
43 | 44 |
idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
44 | 45 |
-----END OPENSSH PRIVATE KEY----- |
45 |
-''', |
|
46 |
- 'public_key': rb'''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
47 |
-''', # noqa: E501 |
|
48 |
- 'public_key_data': bytes.fromhex(''' |
|
46 |
+""", |
|
47 |
+ 'public_key': rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
48 |
+""", # noqa: E501 |
|
49 |
+ 'public_key_data': bytes.fromhex(""" |
|
49 | 50 |
00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
50 | 51 |
00 00 00 20 |
51 | 52 |
81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
52 | 53 |
30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
53 |
-'''), |
|
54 |
- 'expected_signature': bytes.fromhex(''' |
|
54 |
+"""), |
|
55 |
+ 'expected_signature': bytes.fromhex(""" |
|
55 | 56 |
00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
56 | 57 |
00 00 00 40 |
57 | 58 |
f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86 |
58 | 59 |
66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd |
59 | 60 |
0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c |
60 | 61 |
1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02 |
61 |
- '''), |
|
62 |
+ """), |
|
62 | 63 |
'derived_passphrase': rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==', # noqa: E501 |
63 | 64 |
}, |
64 | 65 |
# Currently only supported by PuTTY (which is deficient in other |
65 | 66 |
# niceties of the SSH agent and the agent's client). |
66 | 67 |
'ed448': { |
67 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
68 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
68 | 69 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz |
69 | 70 |
c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT |
70 | 71 |
zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA |
... | ... |
@@ -74,19 +75,18 @@ ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM |
74 | 75 |
GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp |
75 | 76 |
dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
76 | 77 |
-----END OPENSSH PRIVATE KEY----- |
77 |
-''', |
|
78 |
- 'public_key': rb'''ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
79 |
-''', # noqa: E501 |
|
80 |
- 'public_key_data': bytes.fromhex(''' |
|
78 |
+""", |
|
79 |
+ 'public_key': rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
80 |
+""", # noqa: E501 |
|
81 |
+ 'public_key_data': bytes.fromhex(""" |
|
81 | 82 |
00 00 00 09 73 73 68 2d 65 64 34 34 38 |
82 | 83 |
00 00 00 39 |
83 | 84 |
e2 f6 72 d3 4f 56 bb cc 04 c6 3b c4 6f 78 6a b4 |
84 | 85 |
bc f5 18 ef fe 77 9b e6 19 46 c4 ad 64 38 01 43 |
85 | 86 |
bd 99 82 d3 cc 72 47 73 69 b8 b3 ec 96 cc fd bd |
86 | 87 |
09 cd c4 73 18 b3 2c 6f 00 |
87 |
- '''), |
|
88 |
- |
|
89 |
- 'expected_signature': bytes.fromhex(''' |
|
88 |
+ """), |
|
89 |
+ 'expected_signature': bytes.fromhex(""" |
|
90 | 90 |
00 00 00 09 73 73 68 2d 65 64 34 34 38 |
91 | 91 |
00 00 00 72 06 86 |
92 | 92 |
f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67 |
... | ... |
@@ -96,11 +96,11 @@ dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
96 | 96 |
db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80 |
97 | 97 |
db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53 |
98 | 98 |
7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00 |
99 |
- '''), |
|
99 |
+ """), |
|
100 | 100 |
'derived_passphrase': rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA', # noqa: E501 |
101 | 101 |
}, |
102 | 102 |
'rsa': { |
103 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
103 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
104 | 104 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn |
105 | 105 |
NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7 |
106 | 106 |
Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs |
... | ... |
@@ -138,10 +138,10 @@ vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi |
138 | 138 |
btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg |
139 | 139 |
Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
140 | 140 |
-----END OPENSSH PRIVATE KEY----- |
141 |
-''', |
|
142 |
- '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 |
|
143 |
-''', # noqa: E501 |
|
144 |
- 'public_key_data': bytes.fromhex(''' |
|
141 |
+""", |
|
142 |
+ '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 |
|
143 |
+""", # noqa: E501 |
|
144 |
+ 'public_key_data': bytes.fromhex(""" |
|
145 | 145 |
00 00 00 07 73 73 68 2d 72 73 61 |
146 | 146 |
00 00 00 03 01 00 01 |
147 | 147 |
00 00 01 81 00 |
... | ... |
@@ -169,8 +169,8 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
169 | 169 |
b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f |
170 | 170 |
0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
171 | 171 |
d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
172 |
-'''), |
|
173 |
- 'expected_signature': bytes.fromhex(''' |
|
172 |
+"""), |
|
173 |
+ 'expected_signature': bytes.fromhex(""" |
|
174 | 174 |
00 00 00 07 73 73 68 2d 72 73 61 |
175 | 175 |
00 00 01 80 |
176 | 176 |
a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be |
... | ... |
@@ -197,14 +197,14 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
197 | 197 |
a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec |
198 | 198 |
de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38 |
199 | 199 |
e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce |
200 |
-'''), |
|
200 |
+"""), |
|
201 | 201 |
'derived_passphrase': rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O', # noqa: E501 |
202 | 202 |
}, |
203 | 203 |
} |
204 | 204 |
|
205 | 205 |
UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = { |
206 | 206 |
'dsa1024': { |
207 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
207 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
208 | 208 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH |
209 | 209 |
NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa |
210 | 210 |
0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9 |
... | ... |
@@ -225,10 +225,10 @@ mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH |
225 | 225 |
u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
226 | 226 |
0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH |
227 | 227 |
-----END OPENSSH PRIVATE KEY----- |
228 |
-''', |
|
229 |
- 'public_key': rb'''ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
230 |
-''', # noqa: E501 |
|
231 |
- 'public_key_data': bytes.fromhex(''' |
|
228 |
+""", |
|
229 |
+ 'public_key': rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
230 |
+""", # noqa: E501 |
|
231 |
+ 'public_key_data': bytes.fromhex(""" |
|
232 | 232 |
00 00 00 07 73 73 68 2d 64 73 73 |
233 | 233 |
00 00 00 81 00 |
234 | 234 |
bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d |
... | ... |
@@ -259,12 +259,12 @@ u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
259 | 259 |
a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a |
260 | 260 |
a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
261 | 261 |
40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
262 |
-'''), |
|
262 |
+"""), |
|
263 | 263 |
'expected_signature': None, |
264 | 264 |
'derived_passphrase': None, |
265 | 265 |
}, |
266 | 266 |
'ecdsa256': { |
267 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
267 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
268 | 268 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS |
269 | 269 |
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S |
270 | 270 |
3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP |
... | ... |
@@ -273,10 +273,10 @@ Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb |
273 | 273 |
oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp |
274 | 274 |
dGhvdXQgcGFzc3BocmFzZQECAwQ= |
275 | 275 |
-----END OPENSSH PRIVATE KEY----- |
276 |
-''', |
|
277 |
- 'public_key': rb'''ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
278 |
-''', # noqa: E501 |
|
279 |
- 'public_key_data': bytes.fromhex(''' |
|
276 |
+""", |
|
277 |
+ 'public_key': rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
278 |
+""", # noqa: E501 |
|
279 |
+ 'public_key_data': bytes.fromhex(""" |
|
280 | 280 |
00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e |
281 | 281 |
69 73 74 70 32 35 36 |
282 | 282 |
00 00 00 08 6e 69 73 74 70 32 35 36 |
... | ... |
@@ -285,12 +285,12 @@ dGhvdXQgcGFzc3BocmFzZQECAwQ= |
285 | 285 |
7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c |
286 | 286 |
64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
287 | 287 |
49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
288 |
-'''), |
|
288 |
+"""), |
|
289 | 289 |
'expected_signature': None, |
290 | 290 |
'derived_passphrase': None, |
291 | 291 |
}, |
292 | 292 |
'ecdsa384': { |
293 |
- 'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY----- |
|
293 |
+ 'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
294 | 294 |
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS |
295 | 295 |
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ |
296 | 296 |
DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N |
... | ... |
@@ -300,10 +300,10 @@ au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx |
300 | 300 |
JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
301 | 301 |
2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA== |
302 | 302 |
-----END OPENSSH PRIVATE KEY----- |
303 |
-''', |
|
304 |
- 'public_key': rb'''ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
305 |
-''', # noqa: E501 |
|
306 |
- 'public_key_data': bytes.fromhex(''' |
|
303 |
+""", |
|
304 |
+ 'public_key': rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
305 |
+""", # noqa: E501 |
|
306 |
+ 'public_key_data': bytes.fromhex(""" |
|
307 | 307 |
00 00 00 13 |
308 | 308 |
65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 |
309 | 309 |
33 38 34 |
... | ... |
@@ -315,7 +315,7 @@ JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
315 | 315 |
a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd |
316 | 316 |
7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
317 | 317 |
79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
318 |
-'''), |
|
318 |
+"""), |
|
319 | 319 |
'expected_signature': None, |
320 | 320 |
'derived_passphrase': None, |
321 | 321 |
}, |
... | ... |
@@ -327,8 +327,16 @@ DUMMY_KEY1 = SUPPORTED_KEYS['ed25519']['public_key_data'] |
327 | 327 |
DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII') |
328 | 328 |
DUMMY_KEY2 = SUPPORTED_KEYS['rsa']['public_key_data'] |
329 | 329 |
DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII') |
330 |
-DUMMY_CONFIG_SETTINGS = {"length": 10, "upper": 1, "lower": 1, "repeat": 5, |
|
331 |
- "number": 1, "space": 1, "dash": 1, "symbol": 1} |
|
330 |
+DUMMY_CONFIG_SETTINGS = { |
|
331 |
+ 'length': 10, |
|
332 |
+ 'upper': 1, |
|
333 |
+ 'lower': 1, |
|
334 |
+ 'repeat': 5, |
|
335 |
+ 'number': 1, |
|
336 |
+ 'space': 1, |
|
337 |
+ 'dash': 1, |
|
338 |
+ 'symbol': 1, |
|
339 |
+} |
|
332 | 340 |
DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o' |
333 | 341 |
DUMMY_RESULT_KEY1 = b'E<b<{ -7iG' |
334 | 342 |
DUMMY_PHRASE_FROM_KEY1_RAW = ( |
... | ... |
@@ -341,30 +349,40 @@ DUMMY_PHRASE_FROM_KEY1_RAW = ( |
341 | 349 |
DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==' # noqa: E501 |
342 | 350 |
|
343 | 351 |
skip_if_no_agent = pytest.mark.skipif( |
344 |
- not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required') |
|
352 |
+ not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required' |
|
353 |
+) |
|
354 |
+ |
|
345 | 355 |
|
346 | 356 |
def list_keys( |
347 | 357 |
self: Any = None, |
348 | 358 |
) -> list[ssh_agent_client.types.KeyCommentPair]: |
349 | 359 |
del self # Unused. |
350 | 360 |
Pair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
351 |
- list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
352 |
- for key, value in SUPPORTED_KEYS.items()] |
|
353 |
- list2 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
354 |
- for key, value in UNSUITABLE_KEYS.items()] |
|
361 |
+ list1 = [ |
|
362 |
+ Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
363 |
+ for key, value in SUPPORTED_KEYS.items() |
|
364 |
+ ] |
|
365 |
+ list2 = [ |
|
366 |
+ Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
367 |
+ for key, value in UNSUITABLE_KEYS.items() |
|
368 |
+ ] |
|
355 | 369 |
return list1 + list2 |
356 | 370 |
|
371 |
+ |
|
357 | 372 |
def list_keys_singleton( |
358 | 373 |
self: Any = None, |
359 | 374 |
) -> list[ssh_agent_client.types.KeyCommentPair]: |
360 | 375 |
del self # Unused. |
361 | 376 |
Pair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
362 |
- list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
363 |
- for key, value in SUPPORTED_KEYS.items()] |
|
377 |
+ list1 = [ |
|
378 |
+ Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
|
379 |
+ for key, value in SUPPORTED_KEYS.items() |
|
380 |
+ ] |
|
364 | 381 |
return list1[:1] |
365 | 382 |
|
383 |
+ |
|
366 | 384 |
def suitable_ssh_keys( |
367 |
- conn: Any |
|
385 |
+ conn: Any, |
|
368 | 386 |
) -> Iterator[ssh_agent_client.types.KeyCommentPair]: |
369 | 387 |
del conn # Unused. |
370 | 388 |
yield from [ |
... | ... |
@@ -377,9 +396,12 @@ def phrase_from_key(key: bytes) -> bytes: |
377 | 396 |
return DUMMY_PHRASE_FROM_KEY1 |
378 | 397 |
raise KeyError(key) # pragma: no cover |
379 | 398 |
|
399 |
+ |
|
380 | 400 |
@contextlib.contextmanager |
381 | 401 |
def isolated_config( |
382 |
- monkeypatch: Any, runner: click.testing.CliRunner, config: Any, |
|
402 |
+ monkeypatch: Any, |
|
403 |
+ runner: click.testing.CliRunner, |
|
404 |
+ config: Any, |
|
383 | 405 |
): |
384 | 406 |
prog_name = derivepassphrase.cli.PROG_NAME |
385 | 407 |
env_name = prog_name.replace(' ', '_').upper() + '_PATH' |
... | ... |
@@ -393,7 +415,8 @@ def isolated_config( |
393 | 415 |
) |
394 | 416 |
with open( |
395 | 417 |
derivepassphrase.cli._config_filename(), |
396 |
- 'w', encoding='UTF-8', |
|
418 |
+ 'w', |
|
419 |
+ encoding='UTF-8', |
|
397 | 420 |
) as outfile: |
398 | 421 |
json.dump(config, outfile) |
399 | 422 |
yield |
... | ... |
@@ -15,115 +15,149 @@ import derivepassphrase |
15 | 15 |
|
16 | 16 |
Vault = derivepassphrase.Vault |
17 | 17 |
|
18 |
-class TestVault: |
|
19 | 18 |
|
19 |
+class TestVault: |
|
20 | 20 |
phrase = b'She cells C shells bye the sea shoars' |
21 | 21 |
google_phrase = rb': 4TVH#5:aZl8LueOT\{' |
22 | 22 |
twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9" |
23 | 23 |
|
24 |
- @pytest.mark.parametrize(['service', 'expected'], [ |
|
24 |
+ @pytest.mark.parametrize( |
|
25 |
+ ['service', 'expected'], |
|
26 |
+ [ |
|
25 | 27 |
(b'google', google_phrase), |
26 | 28 |
('twitter', twitter_phrase), |
27 |
- ]) |
|
29 |
+ ], |
|
30 |
+ ) |
|
28 | 31 |
def test_200_basic_configuration(self, service, expected): |
29 | 32 |
assert Vault(phrase=self.phrase).generate(service) == expected |
30 | 33 |
|
31 | 34 |
def test_201_phrase_dependence(self): |
32 | 35 |
assert ( |
33 |
- Vault(phrase=(self.phrase + b'X')).generate('google') == |
|
34 |
- b'n+oIz6sL>K*lTEWYRO%7' |
|
36 |
+ Vault(phrase=(self.phrase + b'X')).generate('google') |
|
37 |
+ == b'n+oIz6sL>K*lTEWYRO%7' |
|
35 | 38 |
) |
36 | 39 |
|
37 | 40 |
def test_202_reproducibility_and_bytes_service_name(self): |
38 |
- assert ( |
|
39 |
- Vault(phrase=self.phrase).generate(b'google') == |
|
40 |
- Vault(phrase=self.phrase).generate('google') |
|
41 |
- ) |
|
41 |
+ assert Vault(phrase=self.phrase).generate(b'google') == Vault( |
|
42 |
+ phrase=self.phrase |
|
43 |
+ ).generate('google') |
|
42 | 44 |
|
43 | 45 |
def test_203_reproducibility_and_bytearray_service_name(self): |
44 |
- assert ( |
|
45 |
- Vault(phrase=self.phrase).generate(b'google') == |
|
46 |
- Vault(phrase=self.phrase).generate(bytearray(b'google')) |
|
47 |
- ) |
|
46 |
+ assert Vault(phrase=self.phrase).generate(b'google') == Vault( |
|
47 |
+ phrase=self.phrase |
|
48 |
+ ).generate(bytearray(b'google')) |
|
48 | 49 |
|
49 | 50 |
def test_210_nonstandard_length(self): |
50 | 51 |
assert ( |
51 |
- Vault(phrase=self.phrase, length=4).generate('google') |
|
52 |
- == b'xDFu' |
|
52 |
+ Vault(phrase=self.phrase, length=4).generate('google') == b'xDFu' |
|
53 | 53 |
) |
54 | 54 |
|
55 | 55 |
def test_211_repetition_limit(self): |
56 | 56 |
assert ( |
57 |
- Vault(phrase=b'', length=24, symbol=0, number=0, |
|
58 |
- repeat=1).generate('asd') == |
|
59 |
- b'IVTDzACftqopUXqDHPkuCIhV' |
|
57 |
+ Vault( |
|
58 |
+ phrase=b'', length=24, symbol=0, number=0, repeat=1 |
|
59 |
+ ).generate('asd') |
|
60 |
+ == b'IVTDzACftqopUXqDHPkuCIhV' |
|
60 | 61 |
) |
61 | 62 |
|
62 | 63 |
def test_212_without_symbols(self): |
63 | 64 |
assert ( |
64 |
- Vault(phrase=self.phrase, symbol=0).generate('google') == |
|
65 |
- b'XZ4wRe0bZCazbljCaMqR' |
|
65 |
+ Vault(phrase=self.phrase, symbol=0).generate('google') |
|
66 |
+ == b'XZ4wRe0bZCazbljCaMqR' |
|
66 | 67 |
) |
67 | 68 |
|
68 | 69 |
def test_213_no_numbers(self): |
69 | 70 |
assert ( |
70 |
- Vault(phrase=self.phrase, number=0).generate('google') == |
|
71 |
- b'_*$TVH.%^aZl(LUeOT?>' |
|
71 |
+ Vault(phrase=self.phrase, number=0).generate('google') |
|
72 |
+ == b'_*$TVH.%^aZl(LUeOT?>' |
|
72 | 73 |
) |
73 | 74 |
|
74 | 75 |
def test_214_no_lowercase_letters(self): |
75 | 76 |
assert ( |
76 |
- Vault(phrase=self.phrase, lower=0).generate('google') == |
|
77 |
- b':{?)+7~@OA:L]!0E$)(+' |
|
77 |
+ Vault(phrase=self.phrase, lower=0).generate('google') |
|
78 |
+ == b':{?)+7~@OA:L]!0E$)(+' |
|
78 | 79 |
) |
79 | 80 |
|
80 | 81 |
def test_215_at_least_5_digits(self): |
81 | 82 |
assert ( |
82 |
- Vault(phrase=self.phrase, length=8, number=5) |
|
83 |
- .generate('songkick') == b'i0908.7[' |
|
83 |
+ Vault(phrase=self.phrase, length=8, number=5).generate('songkick') |
|
84 |
+ == b'i0908.7[' |
|
84 | 85 |
) |
85 | 86 |
|
86 | 87 |
def test_216_lots_of_spaces(self): |
87 | 88 |
assert ( |
88 |
- Vault(phrase=self.phrase, space=12) |
|
89 |
- .generate('songkick') == b' c 6 Bq % 5fR ' |
|
89 |
+ Vault(phrase=self.phrase, space=12).generate('songkick') |
|
90 |
+ == b' c 6 Bq % 5fR ' |
|
90 | 91 |
) |
91 | 92 |
|
92 | 93 |
def test_217_all_character_classes(self): |
93 | 94 |
assert ( |
94 |
- Vault(phrase=self.phrase, lower=2, upper=2, number=1, |
|
95 |
- space=3, dash=2, symbol=1) |
|
96 |
- .generate('google') == b': : fv_wqt>a-4w1S R' |
|
95 |
+ Vault( |
|
96 |
+ phrase=self.phrase, |
|
97 |
+ lower=2, |
|
98 |
+ upper=2, |
|
99 |
+ number=1, |
|
100 |
+ space=3, |
|
101 |
+ dash=2, |
|
102 |
+ symbol=1, |
|
103 |
+ ).generate('google') |
|
104 |
+ == b': : fv_wqt>a-4w1S R' |
|
97 | 105 |
) |
98 | 106 |
|
99 | 107 |
def test_218_only_numbers_and_very_high_repetition_limit(self): |
100 |
- generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0, |
|
101 |
- dash=0, symbol=0, repeat=4).generate('abcdef') |
|
102 |
- forbidden_substrings = {b'0000', b'1111', b'2222', b'3333', b'4444', |
|
103 |
- b'5555', b'6666', b'7777', b'8888', b'9999'} |
|
108 |
+ generated = Vault( |
|
109 |
+ phrase=b'', |
|
110 |
+ length=40, |
|
111 |
+ lower=0, |
|
112 |
+ upper=0, |
|
113 |
+ space=0, |
|
114 |
+ dash=0, |
|
115 |
+ symbol=0, |
|
116 |
+ repeat=4, |
|
117 |
+ ).generate('abcdef') |
|
118 |
+ forbidden_substrings = { |
|
119 |
+ b'0000', |
|
120 |
+ b'1111', |
|
121 |
+ b'2222', |
|
122 |
+ b'3333', |
|
123 |
+ b'4444', |
|
124 |
+ b'5555', |
|
125 |
+ b'6666', |
|
126 |
+ b'7777', |
|
127 |
+ b'8888', |
|
128 |
+ b'9999', |
|
129 |
+ } |
|
104 | 130 |
for substring in forbidden_substrings: |
105 | 131 |
assert substring not in generated |
106 | 132 |
|
107 | 133 |
def test_219_very_limited_character_set(self): |
108 |
- generated = Vault(phrase=b'', length=24, lower=0, upper=0, |
|
109 |
- space=0, symbol=0).generate('testing') |
|
134 |
+ generated = Vault( |
|
135 |
+ phrase=b'', length=24, lower=0, upper=0, space=0, symbol=0 |
|
136 |
+ ).generate('testing') |
|
110 | 137 |
assert generated == b'763252593304946694588866' |
111 | 138 |
|
112 | 139 |
def test_220_character_set_subtraction(self): |
113 | 140 |
assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') |
114 | 141 |
|
115 |
- @pytest.mark.parametrize(['length', 'settings', 'entropy'], [ |
|
142 |
+ @pytest.mark.parametrize( |
|
143 |
+ ['length', 'settings', 'entropy'], |
|
144 |
+ [ |
|
116 | 145 |
(20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)), |
117 | 146 |
( |
118 | 147 |
20, |
119 | 148 |
{'upper': 0, 'number': 0, 'space': 0, 'symbol': 0}, |
120 |
- math.log2(math.factorial(20)) + 20 * math.log2(26) |
|
149 |
+ math.log2(math.factorial(20)) + 20 * math.log2(26), |
|
121 | 150 |
), |
122 | 151 |
(0, {}, float('-inf')), |
123 |
- (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')), |
|
152 |
+ ( |
|
153 |
+ 0, |
|
154 |
+ {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, |
|
155 |
+ float('-inf'), |
|
156 |
+ ), |
|
124 | 157 |
(1, {}, math.log2(94)), |
125 | 158 |
(1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0), |
126 |
- ]) |
|
159 |
+ ], |
|
160 |
+ ) |
|
127 | 161 |
def test_221_entropy( |
128 | 162 |
self, length: int, settings: dict[str, int], entropy: int |
129 | 163 |
) -> None: |
... | ... |
@@ -131,38 +165,54 @@ class TestVault: |
131 | 165 |
assert math.isclose(v._entropy(), entropy) |
132 | 166 |
assert v._estimate_sufficient_hash_length() > 0 |
133 | 167 |
if math.isfinite(entropy) and entropy: |
134 |
- assert ( |
|
135 |
- v._estimate_sufficient_hash_length(1.0) == |
|
136 |
- math.ceil(entropy / 8) |
|
168 |
+ assert v._estimate_sufficient_hash_length(1.0) == math.ceil( |
|
169 |
+ entropy / 8 |
|
137 | 170 |
) |
138 | 171 |
assert v._estimate_sufficient_hash_length(8.0) >= entropy |
139 | 172 |
|
140 | 173 |
def test_222_hash_length_estimation(self) -> None: |
141 |
- v = Vault(phrase=self.phrase, lower=0, upper=0, number=0, |
|
142 |
- symbol=0, space=1, length=1) |
|
174 |
+ v = Vault( |
|
175 |
+ phrase=self.phrase, |
|
176 |
+ lower=0, |
|
177 |
+ upper=0, |
|
178 |
+ number=0, |
|
179 |
+ symbol=0, |
|
180 |
+ space=1, |
|
181 |
+ length=1, |
|
182 |
+ ) |
|
143 | 183 |
assert v._entropy() == 0.0 |
144 | 184 |
assert v._estimate_sufficient_hash_length() > 0 |
145 | 185 |
|
146 |
- @pytest.mark.parametrize(['service', 'expected'], [ |
|
186 |
+ @pytest.mark.parametrize( |
|
187 |
+ ['service', 'expected'], |
|
188 |
+ [ |
|
147 | 189 |
(b'google', google_phrase), |
148 | 190 |
('twitter', twitter_phrase), |
149 |
- ]) |
|
191 |
+ ], |
|
192 |
+ ) |
|
150 | 193 |
def test_223_hash_length_expansion( |
151 | 194 |
self, monkeypatch: Any, service: str | bytes, expected: bytes |
152 | 195 |
) -> None: |
153 | 196 |
v = Vault(phrase=self.phrase) |
154 |
- monkeypatch.setattr(v, |
|
197 |
+ monkeypatch.setattr( |
|
198 |
+ v, |
|
155 | 199 |
'_estimate_sufficient_hash_length', |
156 |
- lambda *args, **kwargs: 1) # noqa: ARG005 |
|
200 |
+ lambda *args, **kwargs: 1, # noqa: ARG005 |
|
201 |
+ ) |
|
157 | 202 |
assert v._estimate_sufficient_hash_length() < len(self.phrase) |
158 | 203 |
assert v.generate(service) == expected |
159 | 204 |
|
160 |
- @pytest.mark.parametrize(['s', 'raises'], [ |
|
161 |
- ('ñ', True), ('Düsseldorf', True), |
|
162 |
- ('liberté, egalité, fraternité', True), ('ASCII', False), |
|
205 |
+ @pytest.mark.parametrize( |
|
206 |
+ ['s', 'raises'], |
|
207 |
+ [ |
|
208 |
+ ('ñ', True), |
|
209 |
+ ('Düsseldorf', True), |
|
210 |
+ ('liberté, egalité, fraternité', True), |
|
211 |
+ ('ASCII', False), |
|
163 | 212 |
(b'D\xc3\xbcsseldorf', False), |
164 | 213 |
(bytearray([2, 3, 5, 7, 11, 13]), False), |
165 |
- ]) |
|
214 |
+ ], |
|
215 |
+ ) |
|
166 | 216 |
def test_224_binary_strings( |
167 | 217 |
self, s: str | bytes | bytearray, raises: bool |
168 | 218 |
) -> None: |
... | ... |
@@ -181,15 +231,22 @@ class TestVault: |
181 | 231 |
assert binstr(binstr(s)) == bytes(s) |
182 | 232 |
|
183 | 233 |
def test_310_too_many_symbols(self): |
184 |
- with pytest.raises(ValueError, |
|
185 |
- match='requested passphrase length too short'): |
|
234 |
+ with pytest.raises( |
|
235 |
+ ValueError, match='requested passphrase length too short' |
|
236 |
+ ): |
|
186 | 237 |
Vault(phrase=self.phrase, symbol=100) |
187 | 238 |
|
188 | 239 |
def test_311_no_viable_characters(self): |
189 |
- with pytest.raises(ValueError, |
|
190 |
- match='no allowed characters left'): |
|
191 |
- Vault(phrase=self.phrase, lower=0, upper=0, number=0, |
|
192 |
- space=0, dash=0, symbol=0) |
|
240 |
+ with pytest.raises(ValueError, match='no allowed characters left'): |
|
241 |
+ Vault( |
|
242 |
+ phrase=self.phrase, |
|
243 |
+ lower=0, |
|
244 |
+ upper=0, |
|
245 |
+ number=0, |
|
246 |
+ space=0, |
|
247 |
+ dash=0, |
|
248 |
+ symbol=0, |
|
249 |
+ ) |
|
193 | 250 |
|
194 | 251 |
def test_320_character_set_subtraction_duplicate(self): |
195 | 252 |
with pytest.raises(ValueError, match='duplicate characters'): |
... | ... |
@@ -199,9 +256,9 @@ class TestVault: |
199 | 256 |
|
200 | 257 |
def test_322_hash_length_estimation(self) -> None: |
201 | 258 |
v = Vault(phrase=self.phrase) |
202 |
- with pytest.raises(ValueError, |
|
203 |
- match='invalid safety factor'): |
|
259 |
+ with pytest.raises(ValueError, match='invalid safety factor'): |
|
204 | 260 |
assert v._estimate_sufficient_hash_length(-1.0) |
205 |
- with pytest.raises(TypeError, |
|
206 |
- match='invalid safety factor: not a float'): |
|
261 |
+ with pytest.raises( |
|
262 |
+ TypeError, match='invalid safety factor: not a float' |
|
263 |
+ ): |
|
207 | 264 |
assert v._estimate_sufficient_hash_length(None) # type: ignore |
... | ... |
@@ -56,68 +58,91 @@ class OptionCombination(NamedTuple): |
56 | 58 |
|
57 | 59 |
|
58 | 60 |
PASSWORD_GENERATION_OPTIONS: list[tuple[str, ...]] = [ |
59 |
- ('--phrase',), ('--key',), ('--length', '20'), ('--repeat', '20'), |
|
60 |
- ('--lower', '1'), ('--upper', '1'), ('--number', '1'), |
|
61 |
- ('--space', '1'), ('--dash', '1'), ('--symbol', '1') |
|
61 |
+ ('--phrase',), |
|
62 |
+ ('--key',), |
|
63 |
+ ('--length', '20'), |
|
64 |
+ ('--repeat', '20'), |
|
65 |
+ ('--lower', '1'), |
|
66 |
+ ('--upper', '1'), |
|
67 |
+ ('--number', '1'), |
|
68 |
+ ('--space', '1'), |
|
69 |
+ ('--dash', '1'), |
|
70 |
+ ('--symbol', '1'), |
|
62 | 71 |
] |
63 | 72 |
CONFIGURATION_OPTIONS: list[tuple[str, ...]] = [ |
64 |
- ('--notes',), ('--config',), ('--delete',), ('--delete-globals',), |
|
65 |
- ('--clear',) |
|
73 |
+ ('--notes',), |
|
74 |
+ ('--config',), |
|
75 |
+ ('--delete',), |
|
76 |
+ ('--delete-globals',), |
|
77 |
+ ('--clear',), |
|
66 | 78 |
] |
67 | 79 |
CONFIGURATION_COMMANDS: list[tuple[str, ...]] = [ |
68 |
- ('--notes',), ('--delete',), ('--delete-globals',), ('--clear',) |
|
69 |
-] |
|
70 |
-STORAGE_OPTIONS: list[tuple[str, ...]] = [ |
|
71 |
- ('--export', '-'), ('--import', '-') |
|
80 |
+ ('--notes',), |
|
81 |
+ ('--delete',), |
|
82 |
+ ('--delete-globals',), |
|
83 |
+ ('--clear',), |
|
72 | 84 |
] |
85 |
+STORAGE_OPTIONS: list[tuple[str, ...]] = [('--export', '-'), ('--import', '-')] |
|
73 | 86 |
INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = { |
74 | 87 |
('--phrase',): IncompatibleConfiguration( |
75 | 88 |
[('--key',), *CONFIGURATION_COMMANDS, *STORAGE_OPTIONS], |
76 |
- True, DUMMY_PASSPHRASE), |
|
89 |
+ True, |
|
90 |
+ DUMMY_PASSPHRASE, |
|
91 |
+ ), |
|
77 | 92 |
('--key',): IncompatibleConfiguration( |
78 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
79 |
- True, DUMMY_PASSPHRASE), |
|
93 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
94 |
+ ), |
|
80 | 95 |
('--length', '20'): IncompatibleConfiguration( |
81 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
82 |
- True, DUMMY_PASSPHRASE), |
|
96 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
97 |
+ ), |
|
83 | 98 |
('--repeat', '20'): IncompatibleConfiguration( |
84 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
85 |
- True, DUMMY_PASSPHRASE), |
|
99 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
100 |
+ ), |
|
86 | 101 |
('--lower', '1'): IncompatibleConfiguration( |
87 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
88 |
- True, DUMMY_PASSPHRASE), |
|
102 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
103 |
+ ), |
|
89 | 104 |
('--upper', '1'): IncompatibleConfiguration( |
90 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
91 |
- True, DUMMY_PASSPHRASE), |
|
105 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
106 |
+ ), |
|
92 | 107 |
('--number', '1'): IncompatibleConfiguration( |
93 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
94 |
- True, DUMMY_PASSPHRASE), |
|
108 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
109 |
+ ), |
|
95 | 110 |
('--space', '1'): IncompatibleConfiguration( |
96 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
97 |
- True, DUMMY_PASSPHRASE), |
|
111 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
112 |
+ ), |
|
98 | 113 |
('--dash', '1'): IncompatibleConfiguration( |
99 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
100 |
- True, DUMMY_PASSPHRASE), |
|
114 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
115 |
+ ), |
|
101 | 116 |
('--symbol', '1'): IncompatibleConfiguration( |
102 |
- CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
103 |
- True, DUMMY_PASSPHRASE), |
|
117 |
+ CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE |
|
118 |
+ ), |
|
104 | 119 |
('--notes',): IncompatibleConfiguration( |
105 |
- [('--config',), ('--delete',), ('--delete-globals',), |
|
106 |
- ('--clear',), *STORAGE_OPTIONS], |
|
107 |
- True, None), |
|
120 |
+ [ |
|
121 |
+ ('--config',), |
|
122 |
+ ('--delete',), |
|
123 |
+ ('--delete-globals',), |
|
124 |
+ ('--clear',), |
|
125 |
+ *STORAGE_OPTIONS, |
|
126 |
+ ], |
|
127 |
+ True, |
|
128 |
+ None, |
|
129 |
+ ), |
|
108 | 130 |
('--config', '-p'): IncompatibleConfiguration( |
109 |
- [('--delete',), ('--delete-globals',), |
|
110 |
- ('--clear',), *STORAGE_OPTIONS], |
|
111 |
- None, DUMMY_PASSPHRASE), |
|
131 |
+ [('--delete',), ('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], |
|
132 |
+ None, |
|
133 |
+ DUMMY_PASSPHRASE, |
|
134 |
+ ), |
|
112 | 135 |
('--delete',): IncompatibleConfiguration( |
113 |
- [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None), |
|
136 |
+ [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None |
|
137 |
+ ), |
|
114 | 138 |
('--delete-globals',): IncompatibleConfiguration( |
115 |
- [('--clear',), *STORAGE_OPTIONS], False, None), |
|
139 |
+ [('--clear',), *STORAGE_OPTIONS], False, None |
|
140 |
+ ), |
|
116 | 141 |
('--clear',): IncompatibleConfiguration(STORAGE_OPTIONS, False, None), |
117 | 142 |
('--export', '-'): IncompatibleConfiguration( |
118 |
- [('--import', '-')], False, None), |
|
119 |
- ('--import', '-'): IncompatibleConfiguration( |
|
120 |
- [], False, None), |
|
143 |
+ [('--import', '-')], False, None |
|
144 |
+ ), |
|
145 |
+ ('--import', '-'): IncompatibleConfiguration([], False, None), |
|
121 | 146 |
} |
122 | 147 |
SINGLES: dict[tuple[str, ...], SingleConfiguration] = { |
123 | 148 |
('--phrase',): SingleConfiguration(True, DUMMY_PASSPHRASE, True), |
... | ... |
@@ -143,37 +168,50 @@ config: IncompatibleConfiguration | SingleConfiguration |
143 | 168 |
for opt, config in INCOMPATIBLE.items(): |
144 | 169 |
for opt2 in config.other_options: |
145 | 170 |
INTERESTING_OPTION_COMBINATIONS.extend([ |
146 |
- OptionCombination(options=list(opt + opt2), incompatible=True, |
|
171 |
+ OptionCombination( |
|
172 |
+ options=list(opt + opt2), |
|
173 |
+ incompatible=True, |
|
147 | 174 |
needs_service=config.needs_service, |
148 |
- input=config.input, check_success=False), |
|
149 |
- OptionCombination(options=list(opt2 + opt), incompatible=True, |
|
175 |
+ input=config.input, |
|
176 |
+ check_success=False, |
|
177 |
+ ), |
|
178 |
+ OptionCombination( |
|
179 |
+ options=list(opt2 + opt), |
|
180 |
+ incompatible=True, |
|
150 | 181 |
needs_service=config.needs_service, |
151 |
- input=config.input, check_success=False) |
|
182 |
+ input=config.input, |
|
183 |
+ check_success=False, |
|
184 |
+ ), |
|
152 | 185 |
]) |
153 | 186 |
for opt, config in SINGLES.items(): |
154 | 187 |
INTERESTING_OPTION_COMBINATIONS.append( |
155 |
- OptionCombination(options=list(opt), incompatible=False, |
|
188 |
+ OptionCombination( |
|
189 |
+ options=list(opt), |
|
190 |
+ incompatible=False, |
|
156 | 191 |
needs_service=config.needs_service, |
157 | 192 |
input=config.input, |
158 |
- check_success=config.check_success)) |
|
193 |
+ check_success=config.check_success, |
|
194 |
+ ) |
|
195 |
+ ) |
|
159 | 196 |
|
160 | 197 |
|
161 | 198 |
class TestCLI: |
162 | 199 |
def test_200_help_output(self): |
163 | 200 |
runner = click.testing.CliRunner(mix_stderr=False) |
164 |
- result = runner.invoke(cli.derivepassphrase, ['--help'], |
|
165 |
- catch_exceptions=False) |
|
166 |
- assert result.exit_code == 0 |
|
167 |
- assert 'Password generation:\n' in result.output, ( |
|
168 |
- 'Option groups not respected in help text.' |
|
169 |
- ) |
|
170 |
- assert 'Use NUMBER=0, e.g. "--symbol 0"' in result.output, ( |
|
171 |
- 'Option group epilog not printed.' |
|
201 |
+ result = runner.invoke( |
|
202 |
+ cli.derivepassphrase, ['--help'], catch_exceptions=False |
|
172 | 203 |
) |
204 |
+ assert result.exit_code == 0 |
|
205 |
+ assert ( |
|
206 |
+ 'Password generation:\n' in result.output |
|
207 |
+ ), 'Option groups not respected in help text.' |
|
208 |
+ assert ( |
|
209 |
+ 'Use NUMBER=0, e.g. "--symbol 0"' in result.output |
|
210 |
+ ), 'Option group epilog not printed.' |
|
173 | 211 |
|
174 |
- @pytest.mark.parametrize('charset_name', |
|
175 |
- ['lower', 'upper', 'number', 'space', |
|
176 |
- 'dash', 'symbol']) |
|
212 |
+ @pytest.mark.parametrize( |
|
213 |
+ 'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol'] |
|
214 |
+ ) |
|
177 | 215 |
def test_201_disable_character_set( |
178 | 216 |
self, monkeypatch: Any, charset_name: str |
179 | 217 |
) -> None: |
... | ... |
@@ -181,15 +219,18 @@ class TestCLI: |
181 | 219 |
option = f'--{charset_name}' |
182 | 220 |
charset = dpp.Vault._CHARSETS[charset_name].decode('ascii') |
183 | 221 |
runner = click.testing.CliRunner(mix_stderr=False) |
184 |
- result = runner.invoke(cli.derivepassphrase, |
|
222 |
+ result = runner.invoke( |
|
223 |
+ cli.derivepassphrase, |
|
185 | 224 |
[option, '0', '-p', DUMMY_SERVICE], |
186 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
187 |
- assert result.exit_code == 0, ( |
|
188 |
- f'program died unexpectedly with exit code {result.exit_code}' |
|
189 |
- ) |
|
190 |
- assert not result.stderr_bytes, ( |
|
191 |
- f'program barfed on stderr: {result.stderr_bytes!r}' |
|
225 |
+ input=DUMMY_PASSPHRASE, |
|
226 |
+ catch_exceptions=False, |
|
192 | 227 |
) |
228 |
+ assert ( |
|
229 |
+ result.exit_code == 0 |
|
230 |
+ ), f'program died unexpectedly with exit code {result.exit_code}' |
|
231 |
+ assert ( |
|
232 |
+ not result.stderr_bytes |
|
233 |
+ ), f'program barfed on stderr: {result.stderr_bytes!r}' |
|
193 | 234 |
for c in charset: |
194 | 235 |
assert c not in result.stdout, ( |
195 | 236 |
f'derived password contains forbidden character {c!r}: ' |
... | ... |
@@ -199,15 +240,18 @@ class TestCLI: |
199 | 240 |
def test_202_disable_repetition(self, monkeypatch: Any) -> None: |
200 | 241 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
201 | 242 |
runner = click.testing.CliRunner(mix_stderr=False) |
202 |
- result = runner.invoke(cli.derivepassphrase, |
|
243 |
+ result = runner.invoke( |
|
244 |
+ cli.derivepassphrase, |
|
203 | 245 |
['--repeat', '0', '-p', DUMMY_SERVICE], |
204 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
205 |
- assert result.exit_code == 0, ( |
|
206 |
- f'program died unexpectedly with exit code {result.exit_code}' |
|
207 |
- ) |
|
208 |
- assert not result.stderr_bytes, ( |
|
209 |
- f'program barfed on stderr: {result.stderr_bytes!r}' |
|
246 |
+ input=DUMMY_PASSPHRASE, |
|
247 |
+ catch_exceptions=False, |
|
210 | 248 |
) |
249 |
+ assert ( |
|
250 |
+ result.exit_code == 0 |
|
251 |
+ ), f'program died unexpectedly with exit code {result.exit_code}' |
|
252 |
+ assert ( |
|
253 |
+ not result.stderr_bytes |
|
254 |
+ ), f'program barfed on stderr: {result.stderr_bytes!r}' |
|
211 | 255 |
passphrase = result.stdout.rstrip('\r\n') |
212 | 256 |
for i in range(len(passphrase) - 1): |
213 | 257 |
assert passphrase[i : i + 1] != passphrase[i + 1 : i + 2], ( |
... | ... |
@@ -215,391 +259,497 @@ class TestCLI: |
215 | 259 |
f'at position {i}: {result.stdout!r}' |
216 | 260 |
) |
217 | 261 |
|
218 |
- @pytest.mark.parametrize('config', [ |
|
219 |
- pytest.param({'global': {'key': DUMMY_KEY1_B64}, |
|
220 |
- 'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
|
221 |
- id='global'), |
|
222 |
- pytest.param({'global': {'phrase': DUMMY_PASSPHRASE.rstrip(b'\n') |
|
223 |
- .decode('ASCII')}, |
|
224 |
- 'services': {DUMMY_SERVICE: {'key': DUMMY_KEY1_B64, |
|
225 |
- **DUMMY_CONFIG_SETTINGS}}}, |
|
226 |
- id='service'), |
|
227 |
- ]) |
|
262 |
+ @pytest.mark.parametrize( |
|
263 |
+ 'config', |
|
264 |
+ [ |
|
265 |
+ pytest.param( |
|
266 |
+ { |
|
267 |
+ 'global': {'key': DUMMY_KEY1_B64}, |
|
268 |
+ 'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}, |
|
269 |
+ }, |
|
270 |
+ id='global', |
|
271 |
+ ), |
|
272 |
+ pytest.param( |
|
273 |
+ { |
|
274 |
+ 'global': { |
|
275 |
+ 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode( |
|
276 |
+ 'ASCII' |
|
277 |
+ ) |
|
278 |
+ }, |
|
279 |
+ 'services': { |
|
280 |
+ DUMMY_SERVICE: { |
|
281 |
+ 'key': DUMMY_KEY1_B64, |
|
282 |
+ **DUMMY_CONFIG_SETTINGS, |
|
283 |
+ } |
|
284 |
+ }, |
|
285 |
+ }, |
|
286 |
+ id='service', |
|
287 |
+ ), |
|
288 |
+ ], |
|
289 |
+ ) |
|
228 | 290 |
def test_204a_key_from_config( |
229 |
- self, monkeypatch: Any, config: dpp.types.VaultConfig, |
|
291 |
+ self, |
|
292 |
+ monkeypatch: Any, |
|
293 |
+ config: dpp.types.VaultConfig, |
|
230 | 294 |
) -> None: |
231 | 295 |
runner = click.testing.CliRunner(mix_stderr=False) |
232 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
233 |
- config=config): |
|
234 |
- monkeypatch.setattr(dpp.Vault, 'phrase_from_key', |
|
235 |
- tests.phrase_from_key) |
|
236 |
- result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
|
237 |
- catch_exceptions=False) |
|
238 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
239 |
- 'program exited with failure' |
|
296 |
+ with tests.isolated_config( |
|
297 |
+ monkeypatch=monkeypatch, runner=runner, config=config |
|
298 |
+ ): |
|
299 |
+ monkeypatch.setattr( |
|
300 |
+ dpp.Vault, 'phrase_from_key', tests.phrase_from_key |
|
301 |
+ ) |
|
302 |
+ result = runner.invoke( |
|
303 |
+ cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False |
|
240 | 304 |
) |
305 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
306 |
+ 0, |
|
307 |
+ b'', |
|
308 |
+ ), 'program exited with failure' |
|
241 | 309 |
assert ( |
242 | 310 |
result.stdout_bytes.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE |
243 |
- ), ( |
|
244 |
- 'program generated unexpected result (phrase instead of key)' |
|
245 |
- ) |
|
246 |
- assert result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1, ( |
|
247 |
- 'program generated unexpected result (wrong settings?)' |
|
248 |
- ) |
|
311 |
+ ), 'program generated unexpected result (phrase instead of key)' |
|
312 |
+ assert ( |
|
313 |
+ result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1 |
|
314 |
+ ), 'program generated unexpected result (wrong settings?)' |
|
249 | 315 |
|
250 | 316 |
def test_204b_key_from_command_line(self, monkeypatch: Any) -> None: |
251 | 317 |
runner = click.testing.CliRunner(mix_stderr=False) |
252 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
253 |
- config={'services': {DUMMY_SERVICE: |
|
254 |
- DUMMY_CONFIG_SETTINGS}}): |
|
255 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
256 |
- tests.suitable_ssh_keys) |
|
257 |
- monkeypatch.setattr(dpp.Vault, 'phrase_from_key', |
|
258 |
- tests.phrase_from_key) |
|
259 |
- result = runner.invoke(cli.derivepassphrase, |
|
318 |
+ with tests.isolated_config( |
|
319 |
+ monkeypatch=monkeypatch, |
|
320 |
+ runner=runner, |
|
321 |
+ config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
|
322 |
+ ): |
|
323 |
+ monkeypatch.setattr( |
|
324 |
+ cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
|
325 |
+ ) |
|
326 |
+ monkeypatch.setattr( |
|
327 |
+ dpp.Vault, 'phrase_from_key', tests.phrase_from_key |
|
328 |
+ ) |
|
329 |
+ result = runner.invoke( |
|
330 |
+ cli.derivepassphrase, |
|
260 | 331 |
['-k', DUMMY_SERVICE], |
261 |
- input=b'1\n', catch_exceptions=False) |
|
332 |
+ input=b'1\n', |
|
333 |
+ catch_exceptions=False, |
|
334 |
+ ) |
|
262 | 335 |
assert result.exit_code == 0, 'program exited with failure' |
263 | 336 |
assert result.stdout_bytes, 'program output expected' |
264 | 337 |
last_line = result.stdout_bytes.splitlines(True)[-1] |
265 |
- assert last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE, ( |
|
266 |
- 'program generated unexpected result (phrase instead of key)' |
|
267 |
- ) |
|
268 |
- assert last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1, ( |
|
269 |
- 'program generated unexpected result (wrong settings?)' |
|
270 |
- ) |
|
338 |
+ assert ( |
|
339 |
+ last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE |
|
340 |
+ ), 'program generated unexpected result (phrase instead of key)' |
|
341 |
+ assert ( |
|
342 |
+ last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1 |
|
343 |
+ ), 'program generated unexpected result (wrong settings?)' |
|
271 | 344 |
|
272 | 345 |
def test_205_service_phrase_if_key_in_global_config( |
273 |
- self, monkeypatch: Any, |
|
346 |
+ self, |
|
347 |
+ monkeypatch: Any, |
|
274 | 348 |
) -> None: |
275 | 349 |
runner = click.testing.CliRunner(mix_stderr=False) |
276 | 350 |
with tests.isolated_config( |
277 |
- monkeypatch=monkeypatch, runner=runner, |
|
351 |
+ monkeypatch=monkeypatch, |
|
352 |
+ runner=runner, |
|
278 | 353 |
config={ |
279 | 354 |
'global': {'key': DUMMY_KEY1_B64}, |
280 | 355 |
'services': { |
281 | 356 |
DUMMY_SERVICE: { |
282 |
- 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n') |
|
283 |
- .decode('ASCII'), |
|
284 |
- **DUMMY_CONFIG_SETTINGS}}} |
|
357 |
+ 'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode( |
|
358 |
+ 'ASCII' |
|
359 |
+ ), |
|
360 |
+ **DUMMY_CONFIG_SETTINGS, |
|
361 |
+ } |
|
362 |
+ }, |
|
363 |
+ }, |
|
285 | 364 |
): |
286 |
- result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
|
287 |
- catch_exceptions=False) |
|
365 |
+ result = runner.invoke( |
|
366 |
+ cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False |
|
367 |
+ ) |
|
288 | 368 |
assert result.exit_code == 0, 'program exited with failure' |
289 | 369 |
assert result.stdout_bytes, 'program output expected' |
290 | 370 |
last_line = result.stdout_bytes.splitlines(True)[-1] |
291 |
- assert last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1, ( |
|
292 |
- 'program generated unexpected result (key instead of phrase)' |
|
293 |
- ) |
|
294 |
- assert last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE, ( |
|
295 |
- 'program generated unexpected result (wrong settings?)' |
|
296 |
- ) |
|
371 |
+ assert ( |
|
372 |
+ last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1 |
|
373 |
+ ), 'program generated unexpected result (key instead of phrase)' |
|
374 |
+ assert ( |
|
375 |
+ last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE |
|
376 |
+ ), 'program generated unexpected result (wrong settings?)' |
|
297 | 377 |
|
298 |
- @pytest.mark.parametrize('option', |
|
299 |
- ['--lower', '--upper', '--number', |
|
300 |
- '--space', '--dash', '--symbol', |
|
301 |
- '--repeat', '--length']) |
|
378 |
+ @pytest.mark.parametrize( |
|
379 |
+ 'option', |
|
380 |
+ [ |
|
381 |
+ '--lower', |
|
382 |
+ '--upper', |
|
383 |
+ '--number', |
|
384 |
+ '--space', |
|
385 |
+ '--dash', |
|
386 |
+ '--symbol', |
|
387 |
+ '--repeat', |
|
388 |
+ '--length', |
|
389 |
+ ], |
|
390 |
+ ) |
|
302 | 391 |
def test_210_invalid_argument_range(self, option: str) -> None: |
303 | 392 |
runner = click.testing.CliRunner(mix_stderr=False) |
304 | 393 |
value: str | int |
305 | 394 |
for value in '-42', 'invalid': |
306 |
- result = runner.invoke(cli.derivepassphrase, |
|
307 |
- [option, cast(str, value), '-p', |
|
308 |
- DUMMY_SERVICE], |
|
395 |
+ result = runner.invoke( |
|
396 |
+ cli.derivepassphrase, |
|
397 |
+ [option, cast(str, value), '-p', DUMMY_SERVICE], |
|
309 | 398 |
input=DUMMY_PASSPHRASE, |
310 |
- catch_exceptions=False) |
|
311 |
- assert result.exit_code > 0, ( |
|
312 |
- 'program unexpectedly succeeded' |
|
313 |
- ) |
|
314 |
- assert result.stderr_bytes, ( |
|
315 |
- 'program did not print any error message' |
|
316 |
- ) |
|
317 |
- assert b'Error: Invalid value' in result.stderr_bytes, ( |
|
318 |
- 'program did not print the expected error message' |
|
399 |
+ catch_exceptions=False, |
|
319 | 400 |
) |
401 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
402 |
+ assert ( |
|
403 |
+ result.stderr_bytes |
|
404 |
+ ), 'program did not print any error message' |
|
405 |
+ assert ( |
|
406 |
+ b'Error: Invalid value' in result.stderr_bytes |
|
407 |
+ ), 'program did not print the expected error message' |
|
320 | 408 |
|
321 | 409 |
@pytest.mark.parametrize( |
322 | 410 |
['options', 'service', 'input', 'check_success'], |
323 |
- [(o.options, o.needs_service, o.input, o.check_success) |
|
324 |
- for o in INTERESTING_OPTION_COMBINATIONS if not o.incompatible], |
|
411 |
+ [ |
|
412 |
+ (o.options, o.needs_service, o.input, o.check_success) |
|
413 |
+ for o in INTERESTING_OPTION_COMBINATIONS |
|
414 |
+ if not o.incompatible |
|
415 |
+ ], |
|
325 | 416 |
) |
326 | 417 |
def test_211_service_needed( |
327 |
- self, monkeypatch: Any, options: list[str], |
|
328 |
- service: bool | None, input: bytes | None, check_success: bool, |
|
418 |
+ self, |
|
419 |
+ monkeypatch: Any, |
|
420 |
+ options: list[str], |
|
421 |
+ service: bool | None, |
|
422 |
+ input: bytes | None, |
|
423 |
+ check_success: bool, |
|
329 | 424 |
) -> None: |
330 | 425 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt) |
331 | 426 |
runner = click.testing.CliRunner(mix_stderr=False) |
332 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
333 |
- config={'global': {'phrase': 'abc'}, |
|
334 |
- 'services': {}}): |
|
335 |
- result = runner.invoke(cli.derivepassphrase, |
|
336 |
- options if service |
|
337 |
- else [*options, DUMMY_SERVICE], |
|
338 |
- input=input, catch_exceptions=False) |
|
339 |
- if service is not None: |
|
340 |
- assert result.exit_code > 0, ( |
|
341 |
- 'program unexpectedly succeeded' |
|
342 |
- ) |
|
343 |
- assert result.stderr_bytes, ( |
|
344 |
- 'program did not print any error message' |
|
427 |
+ with tests.isolated_config( |
|
428 |
+ monkeypatch=monkeypatch, |
|
429 |
+ runner=runner, |
|
430 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
431 |
+ ): |
|
432 |
+ result = runner.invoke( |
|
433 |
+ cli.derivepassphrase, |
|
434 |
+ options if service else [*options, DUMMY_SERVICE], |
|
435 |
+ input=input, |
|
436 |
+ catch_exceptions=False, |
|
345 | 437 |
) |
346 |
- err_msg = (b' requires a SERVICE' if service |
|
347 |
- else b' does not take a SERVICE argument') |
|
348 |
- assert err_msg in result.stderr_bytes, ( |
|
349 |
- 'program did not print the expected error message' |
|
438 |
+ if service is not None: |
|
439 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
440 |
+ assert ( |
|
441 |
+ result.stderr_bytes |
|
442 |
+ ), 'program did not print any error message' |
|
443 |
+ err_msg = ( |
|
444 |
+ b' requires a SERVICE' |
|
445 |
+ if service |
|
446 |
+ else b' does not take a SERVICE argument' |
|
350 | 447 |
) |
448 |
+ assert ( |
|
449 |
+ err_msg in result.stderr_bytes |
|
450 |
+ ), 'program did not print the expected error message' |
|
351 | 451 |
else: |
352 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
353 |
- 'program unexpectedly failed' |
|
354 |
- ) |
|
452 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
453 |
+ 0, |
|
454 |
+ b'', |
|
455 |
+ ), 'program unexpectedly failed' |
|
355 | 456 |
if check_success: |
356 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
357 |
- config={'global': {'phrase': 'abc'}, |
|
358 |
- 'services': {}}): |
|
359 |
- monkeypatch.setattr(cli, '_prompt_for_passphrase', |
|
360 |
- tests.auto_prompt) |
|
361 |
- result = runner.invoke(cli.derivepassphrase, |
|
362 |
- [*options, DUMMY_SERVICE] |
|
363 |
- if service else options, |
|
364 |
- input=input, catch_exceptions=False) |
|
365 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
366 |
- 'program unexpectedly failed' |
|
457 |
+ with tests.isolated_config( |
|
458 |
+ monkeypatch=monkeypatch, |
|
459 |
+ runner=runner, |
|
460 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
461 |
+ ): |
|
462 |
+ monkeypatch.setattr( |
|
463 |
+ cli, '_prompt_for_passphrase', tests.auto_prompt |
|
464 |
+ ) |
|
465 |
+ result = runner.invoke( |
|
466 |
+ cli.derivepassphrase, |
|
467 |
+ [*options, DUMMY_SERVICE] if service else options, |
|
468 |
+ input=input, |
|
469 |
+ catch_exceptions=False, |
|
367 | 470 |
) |
471 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
472 |
+ 0, |
|
473 |
+ b'', |
|
474 |
+ ), 'program unexpectedly failed' |
|
368 | 475 |
|
369 | 476 |
@pytest.mark.parametrize( |
370 | 477 |
['options', 'service'], |
371 |
- [(o.options, o.needs_service) |
|
372 |
- for o in INTERESTING_OPTION_COMBINATIONS if o.incompatible], |
|
478 |
+ [ |
|
479 |
+ (o.options, o.needs_service) |
|
480 |
+ for o in INTERESTING_OPTION_COMBINATIONS |
|
481 |
+ if o.incompatible |
|
482 |
+ ], |
|
373 | 483 |
) |
374 | 484 |
def test_212_incompatible_options( |
375 |
- self, options: list[str], service: bool | None, |
|
485 |
+ self, |
|
486 |
+ options: list[str], |
|
487 |
+ service: bool | None, |
|
376 | 488 |
) -> None: |
377 | 489 |
runner = click.testing.CliRunner(mix_stderr=False) |
378 |
- result = runner.invoke(cli.derivepassphrase, |
|
379 |
- [*options, DUMMY_SERVICE] if service |
|
380 |
- else options, |
|
381 |
- input=DUMMY_PASSPHRASE, catch_exceptions=False) |
|
382 |
- assert result.exit_code > 0, ( |
|
383 |
- 'program unexpectedly succeeded' |
|
384 |
- ) |
|
385 |
- assert result.stderr_bytes, ( |
|
386 |
- 'program did not print any error message' |
|
387 |
- ) |
|
388 |
- assert b'mutually exclusive with ' in result.stderr_bytes, ( |
|
389 |
- 'program did not print the expected error message' |
|
490 |
+ result = runner.invoke( |
|
491 |
+ cli.derivepassphrase, |
|
492 |
+ [*options, DUMMY_SERVICE] if service else options, |
|
493 |
+ input=DUMMY_PASSPHRASE, |
|
494 |
+ catch_exceptions=False, |
|
390 | 495 |
) |
496 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
497 |
+ assert result.stderr_bytes, 'program did not print any error message' |
|
498 |
+ assert ( |
|
499 |
+ b'mutually exclusive with ' in result.stderr_bytes |
|
500 |
+ ), 'program did not print the expected error message' |
|
391 | 501 |
|
392 | 502 |
def test_213_import_bad_config_not_vault_config( |
393 |
- self, monkeypatch: Any, |
|
503 |
+ self, |
|
504 |
+ monkeypatch: Any, |
|
394 | 505 |
) -> None: |
395 | 506 |
runner = click.testing.CliRunner(mix_stderr=False) |
396 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
397 |
- config={'services': {}}): |
|
398 |
- result = runner.invoke(cli.derivepassphrase, ['--import', '-'], |
|
399 |
- input=b'null', catch_exceptions=False) |
|
400 |
- assert result.exit_code > 0, ( |
|
401 |
- 'program unexpectedly succeeded' |
|
402 |
- ) |
|
403 |
- assert result.stderr_bytes, ( |
|
404 |
- 'program did not print any error message' |
|
405 |
- ) |
|
406 |
- assert b'not a valid config' in result.stderr_bytes, ( |
|
407 |
- 'program did not print the expected error message' |
|
507 |
+ with tests.isolated_config( |
|
508 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
509 |
+ ): |
|
510 |
+ result = runner.invoke( |
|
511 |
+ cli.derivepassphrase, |
|
512 |
+ ['--import', '-'], |
|
513 |
+ input=b'null', |
|
514 |
+ catch_exceptions=False, |
|
408 | 515 |
) |
516 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
517 |
+ assert ( |
|
518 |
+ result.stderr_bytes |
|
519 |
+ ), 'program did not print any error message' |
|
520 |
+ assert ( |
|
521 |
+ b'not a valid config' in result.stderr_bytes |
|
522 |
+ ), 'program did not print the expected error message' |
|
409 | 523 |
|
410 | 524 |
def test_213a_import_bad_config_not_json_data( |
411 |
- self, monkeypatch: Any, |
|
525 |
+ self, |
|
526 |
+ monkeypatch: Any, |
|
412 | 527 |
) -> None: |
413 | 528 |
runner = click.testing.CliRunner(mix_stderr=False) |
414 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
415 |
- config={'services': {}}): |
|
416 |
- result = runner.invoke(cli.derivepassphrase, ['--import', '-'], |
|
529 |
+ with tests.isolated_config( |
|
530 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
531 |
+ ): |
|
532 |
+ result = runner.invoke( |
|
533 |
+ cli.derivepassphrase, |
|
534 |
+ ['--import', '-'], |
|
417 | 535 |
input=b'This string is not valid JSON.', |
418 |
- catch_exceptions=False) |
|
419 |
- assert result.exit_code > 0, ( |
|
420 |
- 'program unexpectedly succeeded' |
|
421 |
- ) |
|
422 |
- assert result.stderr_bytes, ( |
|
423 |
- 'program did not print any error message' |
|
424 |
- ) |
|
425 |
- assert b'cannot decode JSON' in result.stderr_bytes, ( |
|
426 |
- 'program did not print the expected error message' |
|
536 |
+ catch_exceptions=False, |
|
427 | 537 |
) |
538 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
539 |
+ assert ( |
|
540 |
+ result.stderr_bytes |
|
541 |
+ ), 'program did not print any error message' |
|
542 |
+ assert ( |
|
543 |
+ b'cannot decode JSON' in result.stderr_bytes |
|
544 |
+ ), 'program did not print the expected error message' |
|
428 | 545 |
|
429 | 546 |
def test_213b_import_bad_config_not_a_file( |
430 |
- self, monkeypatch: Any, |
|
547 |
+ self, |
|
548 |
+ monkeypatch: Any, |
|
431 | 549 |
) -> None: |
432 | 550 |
runner = click.testing.CliRunner(mix_stderr=False) |
433 | 551 |
# `isolated_config` validates the configuration. So, to pass an |
434 | 552 |
# actual broken configuration, we must open the configuration file |
435 | 553 |
# ourselves afterwards, inside the context. |
436 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
437 |
- config={'services': {}}): |
|
438 |
- with open(cli._config_filename(), 'w', |
|
439 |
- encoding='UTF-8') as outfile: |
|
554 |
+ with tests.isolated_config( |
|
555 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
556 |
+ ): |
|
557 |
+ with open( |
|
558 |
+ cli._config_filename(), 'w', encoding='UTF-8' |
|
559 |
+ ) as outfile: |
|
440 | 560 |
print('This string is not valid JSON.', file=outfile) |
441 | 561 |
dname = os.path.dirname(cli._config_filename()) |
442 | 562 |
result = runner.invoke( |
443 | 563 |
cli.derivepassphrase, |
444 | 564 |
['--import', os.fsdecode(dname)], |
445 |
- catch_exceptions=False) |
|
446 |
- assert result.exit_code > 0, ( |
|
447 |
- 'program unexpectedly succeeded' |
|
448 |
- ) |
|
449 |
- assert result.stderr_bytes, ( |
|
450 |
- 'program did not print any error message' |
|
565 |
+ catch_exceptions=False, |
|
451 | 566 |
) |
567 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
568 |
+ assert ( |
|
569 |
+ result.stderr_bytes |
|
570 |
+ ), 'program did not print any error message' |
|
452 | 571 |
# Don't test the actual error message, because it is subject to |
453 | 572 |
# locale settings. TODO: find a way anyway. |
454 | 573 |
|
455 | 574 |
def test_214_export_settings_no_stored_settings( |
456 |
- self, monkeypatch: Any, |
|
575 |
+ self, |
|
576 |
+ monkeypatch: Any, |
|
457 | 577 |
) -> None: |
458 | 578 |
runner = click.testing.CliRunner(mix_stderr=False) |
459 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
460 |
- config={'services': {}}): |
|
579 |
+ with tests.isolated_config( |
|
580 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
581 |
+ ): |
|
461 | 582 |
with contextlib.suppress(FileNotFoundError): |
462 | 583 |
os.remove(cli._config_filename()) |
463 |
- result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
|
464 |
- catch_exceptions=False) |
|
465 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
466 |
- 'program exited with failure' |
|
584 |
+ result = runner.invoke( |
|
585 |
+ cli.derivepassphrase, ['--export', '-'], catch_exceptions=False |
|
467 | 586 |
) |
587 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
588 |
+ 0, |
|
589 |
+ b'', |
|
590 |
+ ), 'program exited with failure' |
|
468 | 591 |
|
469 | 592 |
def test_214a_export_settings_bad_stored_config( |
470 |
- self, monkeypatch: Any, |
|
593 |
+ self, |
|
594 |
+ monkeypatch: Any, |
|
471 | 595 |
) -> None: |
472 | 596 |
runner = click.testing.CliRunner(mix_stderr=False) |
473 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
474 |
- config={}): |
|
475 |
- result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
|
476 |
- input=b'null', catch_exceptions=False) |
|
477 |
- assert result.exit_code > 0, ( |
|
478 |
- 'program unexpectedly succeeded' |
|
479 |
- ) |
|
480 |
- assert result.stderr_bytes, ( |
|
481 |
- 'program did not print any error message' |
|
482 |
- ) |
|
483 |
- assert b'cannot load config' in result.stderr_bytes, ( |
|
484 |
- 'program did not print the expected error message' |
|
597 |
+ with tests.isolated_config( |
|
598 |
+ monkeypatch=monkeypatch, runner=runner, config={} |
|
599 |
+ ): |
|
600 |
+ result = runner.invoke( |
|
601 |
+ cli.derivepassphrase, |
|
602 |
+ ['--export', '-'], |
|
603 |
+ input=b'null', |
|
604 |
+ catch_exceptions=False, |
|
485 | 605 |
) |
606 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
607 |
+ assert ( |
|
608 |
+ result.stderr_bytes |
|
609 |
+ ), 'program did not print any error message' |
|
610 |
+ assert ( |
|
611 |
+ b'cannot load config' in result.stderr_bytes |
|
612 |
+ ), 'program did not print the expected error message' |
|
486 | 613 |
|
487 | 614 |
def test_214b_export_settings_not_a_file( |
488 |
- self, monkeypatch: Any, |
|
615 |
+ self, |
|
616 |
+ monkeypatch: Any, |
|
489 | 617 |
) -> None: |
490 | 618 |
runner = click.testing.CliRunner(mix_stderr=False) |
491 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
492 |
- config={'services': {}}): |
|
619 |
+ with tests.isolated_config( |
|
620 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
621 |
+ ): |
|
493 | 622 |
with contextlib.suppress(FileNotFoundError): |
494 | 623 |
os.remove(cli._config_filename()) |
495 | 624 |
os.makedirs(cli._config_filename()) |
496 |
- result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
|
497 |
- input=b'null', catch_exceptions=False) |
|
498 |
- assert result.exit_code > 0, ( |
|
499 |
- 'program unexpectedly succeeded' |
|
500 |
- ) |
|
501 |
- assert result.stderr_bytes, ( |
|
502 |
- 'program did not print any error message' |
|
503 |
- ) |
|
504 |
- assert b'cannot load config' in result.stderr_bytes, ( |
|
505 |
- 'program did not print the expected error message' |
|
625 |
+ result = runner.invoke( |
|
626 |
+ cli.derivepassphrase, |
|
627 |
+ ['--export', '-'], |
|
628 |
+ input=b'null', |
|
629 |
+ catch_exceptions=False, |
|
506 | 630 |
) |
631 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
632 |
+ assert ( |
|
633 |
+ result.stderr_bytes |
|
634 |
+ ), 'program did not print any error message' |
|
635 |
+ assert ( |
|
636 |
+ b'cannot load config' in result.stderr_bytes |
|
637 |
+ ), 'program did not print the expected error message' |
|
507 | 638 |
|
508 | 639 |
def test_214c_export_settings_target_not_a_file( |
509 |
- self, monkeypatch: Any, |
|
640 |
+ self, |
|
641 |
+ monkeypatch: Any, |
|
510 | 642 |
) -> None: |
511 | 643 |
runner = click.testing.CliRunner(mix_stderr=False) |
512 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
513 |
- config={'services': {}}): |
|
644 |
+ with tests.isolated_config( |
|
645 |
+ monkeypatch=monkeypatch, runner=runner, config={'services': {}} |
|
646 |
+ ): |
|
514 | 647 |
dname = os.path.dirname(cli._config_filename()) |
515 |
- result = runner.invoke(cli.derivepassphrase, |
|
648 |
+ result = runner.invoke( |
|
649 |
+ cli.derivepassphrase, |
|
516 | 650 |
['--export', os.fsdecode(dname)], |
517 |
- input=b'null', catch_exceptions=False) |
|
518 |
- assert result.exit_code > 0, ( |
|
519 |
- 'program unexpectedly succeeded' |
|
520 |
- ) |
|
521 |
- assert result.stderr_bytes, ( |
|
522 |
- 'program did not print any error message' |
|
523 |
- ) |
|
524 |
- assert b'cannot write config' in result.stderr_bytes, ( |
|
525 |
- 'program did not print the expected error message' |
|
651 |
+ input=b'null', |
|
652 |
+ catch_exceptions=False, |
|
526 | 653 |
) |
654 |
+ assert result.exit_code > 0, 'program unexpectedly succeeded' |
|
655 |
+ assert ( |
|
656 |
+ result.stderr_bytes |
|
657 |
+ ), 'program did not print any error message' |
|
658 |
+ assert ( |
|
659 |
+ b'cannot write config' in result.stderr_bytes |
|
660 |
+ ), 'program did not print the expected error message' |
|
527 | 661 |
|
528 | 662 |
def test_220_edit_notes_successfully(self, monkeypatch: Any) -> None: |
529 |
- edit_result = ''' |
|
663 |
+ edit_result = """ |
|
530 | 664 |
|
531 | 665 |
# - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - - |
532 | 666 |
contents go here |
533 |
-''' |
|
667 |
+""" |
|
534 | 668 |
runner = click.testing.CliRunner(mix_stderr=False) |
535 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
536 |
- config={'global': {'phrase': 'abc'}, |
|
537 |
- 'services': {}}): |
|
538 |
- monkeypatch.setattr(click, 'edit', |
|
539 |
- lambda *a, **kw: edit_result) # noqa: ARG005 |
|
540 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
541 |
- catch_exceptions=False) |
|
542 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
543 |
- 'program exited with failure' |
|
669 |
+ with tests.isolated_config( |
|
670 |
+ monkeypatch=monkeypatch, |
|
671 |
+ runner=runner, |
|
672 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
673 |
+ ): |
|
674 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: edit_result) # noqa: ARG005 |
|
675 |
+ result = runner.invoke( |
|
676 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
544 | 677 |
) |
678 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
679 |
+ 0, |
|
680 |
+ b'', |
|
681 |
+ ), 'program exited with failure' |
|
545 | 682 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
546 | 683 |
config = json.load(infile) |
547 |
- assert config == {'global': {'phrase': 'abc'}, |
|
548 |
- 'services': {'sv': {'notes': |
|
549 |
- 'contents go here'}}} |
|
684 |
+ assert config == { |
|
685 |
+ 'global': {'phrase': 'abc'}, |
|
686 |
+ 'services': {'sv': {'notes': 'contents go here'}}, |
|
687 |
+ } |
|
550 | 688 |
|
551 | 689 |
def test_221_edit_notes_noop(self, monkeypatch: Any) -> None: |
552 | 690 |
runner = click.testing.CliRunner(mix_stderr=False) |
553 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
554 |
- config={'global': {'phrase': 'abc'}, |
|
555 |
- 'services': {}}): |
|
556 |
- monkeypatch.setattr(click, 'edit', |
|
557 |
- lambda *a, **kw: None) # noqa: ARG005 |
|
558 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
559 |
- catch_exceptions=False) |
|
560 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
561 |
- 'program exited with failure' |
|
691 |
+ with tests.isolated_config( |
|
692 |
+ monkeypatch=monkeypatch, |
|
693 |
+ runner=runner, |
|
694 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
695 |
+ ): |
|
696 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: None) # noqa: ARG005 |
|
697 |
+ result = runner.invoke( |
|
698 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
562 | 699 |
) |
700 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
701 |
+ 0, |
|
702 |
+ b'', |
|
703 |
+ ), 'program exited with failure' |
|
563 | 704 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
564 | 705 |
config = json.load(infile) |
565 | 706 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
566 | 707 |
|
567 | 708 |
def test_222_edit_notes_marker_removed(self, monkeypatch: Any) -> None: |
568 | 709 |
runner = click.testing.CliRunner(mix_stderr=False) |
569 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
570 |
- config={'global': {'phrase': 'abc'}, |
|
571 |
- 'services': {}}): |
|
572 |
- monkeypatch.setattr(click, 'edit', |
|
573 |
- lambda *a, **kw: 'long\ntext') # noqa: ARG005 |
|
574 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
575 |
- catch_exceptions=False) |
|
576 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
577 |
- 'program exited with failure' |
|
710 |
+ with tests.isolated_config( |
|
711 |
+ monkeypatch=monkeypatch, |
|
712 |
+ runner=runner, |
|
713 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
714 |
+ ): |
|
715 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext') # noqa: ARG005 |
|
716 |
+ result = runner.invoke( |
|
717 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
578 | 718 |
) |
719 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
720 |
+ 0, |
|
721 |
+ b'', |
|
722 |
+ ), 'program exited with failure' |
|
579 | 723 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
580 | 724 |
config = json.load(infile) |
581 |
- assert config == {'global': {'phrase': 'abc'}, |
|
582 |
- 'services': {'sv': {'notes': 'long\ntext'}}} |
|
725 |
+ assert config == { |
|
726 |
+ 'global': {'phrase': 'abc'}, |
|
727 |
+ 'services': {'sv': {'notes': 'long\ntext'}}, |
|
728 |
+ } |
|
583 | 729 |
|
584 | 730 |
def test_223_edit_notes_abort(self, monkeypatch: Any) -> None: |
585 | 731 |
runner = click.testing.CliRunner(mix_stderr=False) |
586 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
587 |
- config={'global': {'phrase': 'abc'}, |
|
588 |
- 'services': {}}): |
|
589 |
- monkeypatch.setattr(click, 'edit', |
|
590 |
- lambda *a, **kw: '\n\n') # noqa: ARG005 |
|
591 |
- result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
|
592 |
- catch_exceptions=False) |
|
732 |
+ with tests.isolated_config( |
|
733 |
+ monkeypatch=monkeypatch, |
|
734 |
+ runner=runner, |
|
735 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
736 |
+ ): |
|
737 |
+ monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n') # noqa: ARG005 |
|
738 |
+ result = runner.invoke( |
|
739 |
+ cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False |
|
740 |
+ ) |
|
593 | 741 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
594 | 742 |
assert result.stderr_bytes is not None |
595 |
- assert b'user aborted request' in result.stderr_bytes, ( |
|
596 |
- 'expected error message missing' |
|
597 |
- ) |
|
743 |
+ assert ( |
|
744 |
+ b'user aborted request' in result.stderr_bytes |
|
745 |
+ ), 'expected error message missing' |
|
598 | 746 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
599 | 747 |
config = json.load(infile) |
600 | 748 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
601 | 749 |
|
602 |
- @pytest.mark.parametrize(['command_line', 'input', 'result_config'], [ |
|
750 |
+ @pytest.mark.parametrize( |
|
751 |
+ ['command_line', 'input', 'result_config'], |
|
752 |
+ [ |
|
603 | 753 |
( |
604 | 754 |
['--phrase'], |
605 | 755 |
b'my passphrase\n', |
... | ... |
@@ -613,44 +763,66 @@ contents go here |
613 | 763 |
( |
614 | 764 |
['--phrase', 'sv'], |
615 | 765 |
b'my passphrase\n', |
616 |
- {'global': {'phrase': 'abc'}, |
|
617 |
- 'services': {'sv': {'phrase': 'my passphrase'}}}, |
|
766 |
+ { |
|
767 |
+ 'global': {'phrase': 'abc'}, |
|
768 |
+ 'services': {'sv': {'phrase': 'my passphrase'}}, |
|
769 |
+ }, |
|
618 | 770 |
), |
619 | 771 |
( |
620 | 772 |
['--key', 'sv'], |
621 | 773 |
b'1\n', |
622 |
- {'global': {'phrase': 'abc'}, |
|
623 |
- 'services': {'sv': {'key': DUMMY_KEY1_B64}}}, |
|
774 |
+ { |
|
775 |
+ 'global': {'phrase': 'abc'}, |
|
776 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64}}, |
|
777 |
+ }, |
|
624 | 778 |
), |
625 | 779 |
( |
626 | 780 |
['--key', '--length', '15', 'sv'], |
627 | 781 |
b'1\n', |
628 |
- {'global': {'phrase': 'abc'}, |
|
629 |
- 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}}, |
|
782 |
+ { |
|
783 |
+ 'global': {'phrase': 'abc'}, |
|
784 |
+ 'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}, |
|
785 |
+ }, |
|
630 | 786 |
), |
631 |
- ]) |
|
787 |
+ ], |
|
788 |
+ ) |
|
632 | 789 |
def test_224_store_config_good( |
633 |
- self, monkeypatch: Any, command_line: list[str], input: bytes, |
|
790 |
+ self, |
|
791 |
+ monkeypatch: Any, |
|
792 |
+ command_line: list[str], |
|
793 |
+ input: bytes, |
|
634 | 794 |
result_config: Any, |
635 | 795 |
) -> None: |
636 | 796 |
runner = click.testing.CliRunner(mix_stderr=False) |
637 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
638 |
- config={'global': {'phrase': 'abc'}, |
|
639 |
- 'services': {}}): |
|
640 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
641 |
- tests.suitable_ssh_keys) |
|
642 |
- result = runner.invoke(cli.derivepassphrase, |
|
797 |
+ with tests.isolated_config( |
|
798 |
+ monkeypatch=monkeypatch, |
|
799 |
+ runner=runner, |
|
800 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
801 |
+ ): |
|
802 |
+ monkeypatch.setattr( |
|
803 |
+ cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
|
804 |
+ ) |
|
805 |
+ result = runner.invoke( |
|
806 |
+ cli.derivepassphrase, |
|
643 | 807 |
['--config', *command_line], |
644 |
- catch_exceptions=False, input=input) |
|
808 |
+ catch_exceptions=False, |
|
809 |
+ input=input, |
|
810 |
+ ) |
|
645 | 811 |
assert result.exit_code == 0, 'program exited with failure' |
646 | 812 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
647 | 813 |
config = json.load(infile) |
648 |
- assert config == result_config, ( |
|
649 |
- 'stored config does not match expectation' |
|
650 |
- ) |
|
814 |
+ assert ( |
|
815 |
+ config == result_config |
|
816 |
+ ), 'stored config does not match expectation' |
|
651 | 817 |
|
652 |
- @pytest.mark.parametrize(['command_line', 'input', 'err_text'], [ |
|
653 |
- ([], b'', b'cannot update global settings without actual settings'), |
|
818 |
+ @pytest.mark.parametrize( |
|
819 |
+ ['command_line', 'input', 'err_text'], |
|
820 |
+ [ |
|
821 |
+ ( |
|
822 |
+ [], |
|
823 |
+ b'', |
|
824 |
+ b'cannot update global settings without actual settings', |
|
825 |
+ ), |
|
654 | 826 |
( |
655 | 827 |
['sv'], |
656 | 828 |
b'', |
... | ... |
@@ -658,65 +830,84 @@ contents go here |
658 | 830 |
), |
659 | 831 |
(['--phrase', 'sv'], b'', b'no passphrase given'), |
660 | 832 |
(['--key'], b'', b'no valid SSH key selected'), |
661 |
- ]) |
|
833 |
+ ], |
|
834 |
+ ) |
|
662 | 835 |
def test_225_store_config_fail( |
663 |
- self, monkeypatch: Any, command_line: list[str], |
|
664 |
- input: bytes, err_text: bytes, |
|
836 |
+ self, |
|
837 |
+ monkeypatch: Any, |
|
838 |
+ command_line: list[str], |
|
839 |
+ input: bytes, |
|
840 |
+ err_text: bytes, |
|
665 | 841 |
) -> None: |
666 | 842 |
runner = click.testing.CliRunner(mix_stderr=False) |
667 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
668 |
- config={'global': {'phrase': 'abc'}, |
|
669 |
- 'services': {}}): |
|
670 |
- monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
|
671 |
- tests.suitable_ssh_keys) |
|
672 |
- result = runner.invoke(cli.derivepassphrase, |
|
843 |
+ with tests.isolated_config( |
|
844 |
+ monkeypatch=monkeypatch, |
|
845 |
+ runner=runner, |
|
846 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
847 |
+ ): |
|
848 |
+ monkeypatch.setattr( |
|
849 |
+ cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys |
|
850 |
+ ) |
|
851 |
+ result = runner.invoke( |
|
852 |
+ cli.derivepassphrase, |
|
673 | 853 |
['--config', *command_line], |
674 |
- catch_exceptions=False, input=input) |
|
854 |
+ catch_exceptions=False, |
|
855 |
+ input=input, |
|
856 |
+ ) |
|
675 | 857 |
assert result.exit_code != 0, 'program unexpectedly succeeded?!' |
676 | 858 |
assert result.stderr_bytes is not None |
677 |
- assert err_text in result.stderr_bytes, ( |
|
678 |
- 'expected error message missing' |
|
679 |
- ) |
|
859 |
+ assert ( |
|
860 |
+ err_text in result.stderr_bytes |
|
861 |
+ ), 'expected error message missing' |
|
680 | 862 |
|
681 | 863 |
def test_225a_store_config_fail_manual_no_ssh_key_selection( |
682 |
- self, monkeypatch: Any, |
|
864 |
+ self, |
|
865 |
+ monkeypatch: Any, |
|
683 | 866 |
) -> None: |
684 | 867 |
runner = click.testing.CliRunner(mix_stderr=False) |
685 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
686 |
- config={'global': {'phrase': 'abc'}, |
|
687 |
- 'services': {}}): |
|
868 |
+ with tests.isolated_config( |
|
869 |
+ monkeypatch=monkeypatch, |
|
870 |
+ runner=runner, |
|
871 |
+ config={'global': {'phrase': 'abc'}, 'services': {}}, |
|
872 |
+ ): |
|
688 | 873 |
custom_error = 'custom error message' |
874 |
+ |
|
689 | 875 |
def raiser(): |
690 | 876 |
raise RuntimeError(custom_error) |
877 |
+ |
|
691 | 878 |
monkeypatch.setattr(cli, '_select_ssh_key', raiser) |
692 |
- result = runner.invoke(cli.derivepassphrase, |
|
879 |
+ result = runner.invoke( |
|
880 |
+ cli.derivepassphrase, |
|
693 | 881 |
['--key', '--config'], |
694 |
- catch_exceptions=False) |
|
882 |
+ catch_exceptions=False, |
|
883 |
+ ) |
|
695 | 884 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
696 | 885 |
assert result.stderr_bytes is not None |
697 |
- assert custom_error.encode() in result.stderr_bytes, ( |
|
698 |
- 'expected error message missing' |
|
699 |
- ) |
|
886 |
+ assert ( |
|
887 |
+ custom_error.encode() in result.stderr_bytes |
|
888 |
+ ), 'expected error message missing' |
|
700 | 889 |
|
701 | 890 |
def test_226_no_arguments(self) -> None: |
702 | 891 |
runner = click.testing.CliRunner(mix_stderr=False) |
703 |
- result = runner.invoke(cli.derivepassphrase, [], |
|
704 |
- catch_exceptions=False) |
|
892 |
+ result = runner.invoke( |
|
893 |
+ cli.derivepassphrase, [], catch_exceptions=False |
|
894 |
+ ) |
|
705 | 895 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
706 | 896 |
assert result.stderr_bytes is not None |
707 |
- assert b'SERVICE is required' in result.stderr_bytes, ( |
|
708 |
- 'expected error message missing' |
|
709 |
- ) |
|
897 |
+ assert ( |
|
898 |
+ b'SERVICE is required' in result.stderr_bytes |
|
899 |
+ ), 'expected error message missing' |
|
710 | 900 |
|
711 | 901 |
def test_226a_no_passphrase_or_key(self) -> None: |
712 | 902 |
runner = click.testing.CliRunner(mix_stderr=False) |
713 |
- result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE], |
|
714 |
- catch_exceptions=False) |
|
903 |
+ result = runner.invoke( |
|
904 |
+ cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False |
|
905 |
+ ) |
|
715 | 906 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
716 | 907 |
assert result.stderr_bytes is not None |
717 |
- assert b'no passphrase or key given' in result.stderr_bytes, ( |
|
718 |
- 'expected error message missing' |
|
719 |
- ) |
|
908 |
+ assert ( |
|
909 |
+ b'no passphrase or key given' in result.stderr_bytes |
|
910 |
+ ), 'expected error message missing' |
|
720 | 911 |
|
721 | 912 |
|
722 | 913 |
class TestCLIUtils: |
... | ... |
@@ -724,9 +914,10 @@ class TestCLIUtils: |
724 | 914 |
def test_100_save_bad_config(self, monkeypatch: Any) -> None: |
725 | 915 |
runner = click.testing.CliRunner() |
726 | 916 |
with ( |
727 |
- tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
728 |
- config={}), |
|
729 |
- pytest.raises(ValueError, match='Invalid vault config') |
|
917 |
+ tests.isolated_config( |
|
918 |
+ monkeypatch=monkeypatch, runner=runner, config={} |
|
919 |
+ ), |
|
920 |
+ pytest.raises(ValueError, match='Invalid vault config'), |
|
730 | 921 |
): |
731 | 922 |
cli._save_config(None) # type: ignore |
732 | 923 |
|
... | ... |
@@ -746,11 +936,15 @@ class TestCLIUtils: |
746 | 936 |
'Spam, bacon, sausage and spam', |
747 | 937 |
'Spam, egg, spam, spam, bacon and spam', |
748 | 938 |
'Spam, spam, spam, egg and spam', |
749 |
- ('Spam, spam, spam, spam, spam, spam, baked beans, ' |
|
750 |
- 'spam, spam, spam and spam'), |
|
751 |
- ('Lobster thermidor aux crevettes with a mornay sauce ' |
|
939 |
+ ( |
|
940 |
+ 'Spam, spam, spam, spam, spam, spam, baked beans, ' |
|
941 |
+ 'spam, spam, spam and spam' |
|
942 |
+ ), |
|
943 |
+ ( |
|
944 |
+ 'Lobster thermidor aux crevettes with a mornay sauce ' |
|
752 | 945 |
'garnished with truffle paté, brandy ' |
753 |
- 'and a fried egg on top and spam'), |
|
946 |
+ 'and a fried egg on top and spam' |
|
947 |
+ ), |
|
754 | 948 |
] |
755 | 949 |
index = cli._prompt_for_selection(items, heading=heading) |
756 | 950 |
click.echo('A fine choice: ', nl=False) |
... | ... |
@@ -759,7 +954,9 @@ class TestCLIUtils: |
759 | 954 |
runner = click.testing.CliRunner(mix_stderr=True) |
760 | 955 |
result = runner.invoke(driver, [], input='9') |
761 | 956 |
assert result.exit_code == 0, 'driver program failed' |
762 |
- assert result.stdout == '''\ |
|
957 |
+ assert ( |
|
958 |
+ result.stdout |
|
959 |
+ == """\ |
|
763 | 960 |
Our menu: |
764 | 961 |
[1] Egg and bacon |
765 | 962 |
[2] Egg, sausage and bacon |
... | ... |
@@ -774,11 +971,15 @@ Our menu: |
774 | 971 |
Your selection? (1-10, leave empty to abort): 9 |
775 | 972 |
A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
776 | 973 |
(Note: Vikings strictly optional.) |
777 |
-''', 'driver program produced unexpected output' # noqa: E501 |
|
778 |
- result = runner.invoke(driver, ['--heading='], input='', |
|
779 |
- catch_exceptions=True) |
|
974 |
+""" # noqa: E501 |
|
975 |
+ ), 'driver program produced unexpected output' |
|
976 |
+ result = runner.invoke( |
|
977 |
+ driver, ['--heading='], input='', catch_exceptions=True |
|
978 |
+ ) |
|
780 | 979 |
assert result.exit_code > 0, 'driver program succeeded?!' |
781 |
- assert result.stdout == '''\ |
|
980 |
+ assert ( |
|
981 |
+ result.stdout |
|
982 |
+ == """\ |
|
782 | 983 |
[1] Egg and bacon |
783 | 984 |
[2] Egg, sausage and bacon |
784 | 985 |
[3] Egg and spam |
... | ... |
@@ -789,13 +990,12 @@ A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam |
789 | 990 |
[8] Spam, spam, spam, egg and spam |
790 | 991 |
[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
791 | 992 |
[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
792 |
-Your selection? (1-10, leave empty to abort): \n''', ( # noqa: E501 |
|
793 |
- 'driver program produced unexpected output' |
|
794 |
- ) |
|
795 |
- assert isinstance(result.exception, IndexError), ( |
|
796 |
- 'driver program did not raise IndexError?!' |
|
797 |
- ) |
|
798 |
- |
|
993 |
+Your selection? (1-10, leave empty to abort):\x20 |
|
994 |
+""" # noqa: E501 |
|
995 |
+ ), 'driver program produced unexpected output' |
|
996 |
+ assert isinstance( |
|
997 |
+ result.exception, IndexError |
|
998 |
+ ), 'driver program did not raise IndexError?!' |
|
799 | 999 |
|
800 | 1000 |
def test_102_prompt_for_selection_single(self) -> None: |
801 | 1001 |
@click.command() |
... | ... |
@@ -803,42 +1003,52 @@ Your selection? (1-10, leave empty to abort): \n''', ( # noqa: E501 |
803 | 1003 |
@click.argument('prompt') |
804 | 1004 |
def driver(item, prompt): |
805 | 1005 |
try: |
806 |
- cli._prompt_for_selection([item], heading='', |
|
807 |
- single_choice_prompt=prompt) |
|
1006 |
+ cli._prompt_for_selection( |
|
1007 |
+ [item], heading='', single_choice_prompt=prompt |
|
1008 |
+ ) |
|
808 | 1009 |
except IndexError: |
809 | 1010 |
click.echo('Boo.') |
810 | 1011 |
raise |
811 | 1012 |
else: |
812 | 1013 |
click.echo('Great!') |
1014 |
+ |
|
813 | 1015 |
runner = click.testing.CliRunner(mix_stderr=True) |
814 |
- result = runner.invoke(driver, |
|
815 |
- ['Will replace with spam. Confirm, y/n?'], |
|
816 |
- input='y') |
|
1016 |
+ result = runner.invoke( |
|
1017 |
+ driver, ['Will replace with spam. Confirm, y/n?'], input='y' |
|
1018 |
+ ) |
|
817 | 1019 |
assert result.exit_code == 0, 'driver program failed' |
818 |
- assert result.stdout == '''\ |
|
1020 |
+ assert ( |
|
1021 |
+ result.stdout |
|
1022 |
+ == """\ |
|
819 | 1023 |
[1] baked beans |
820 | 1024 |
Will replace with spam. Confirm, y/n? y |
821 | 1025 |
Great! |
822 |
-''', 'driver program produced unexpected output' |
|
823 |
- result = runner.invoke(driver, |
|
824 |
- ['Will replace with spam, okay? ' |
|
825 |
- '(Please say "y" or "n".)'], |
|
826 |
- input='') |
|
1026 |
+""" |
|
1027 |
+ ), 'driver program produced unexpected output' |
|
1028 |
+ result = runner.invoke( |
|
1029 |
+ driver, |
|
1030 |
+ ['Will replace with spam, okay? ' '(Please say "y" or "n".)'], |
|
1031 |
+ input='', |
|
1032 |
+ ) |
|
827 | 1033 |
assert result.exit_code > 0, 'driver program succeeded?!' |
828 |
- assert result.stdout == '''\ |
|
1034 |
+ assert ( |
|
1035 |
+ result.stdout |
|
1036 |
+ == """\ |
|
829 | 1037 |
[1] baked beans |
830 | 1038 |
Will replace with spam, okay? (Please say "y" or "n".):\x20 |
831 | 1039 |
Boo. |
832 |
-''', 'driver program produced unexpected output' |
|
833 |
- assert isinstance(result.exception, IndexError), ( |
|
834 |
- 'driver program did not raise IndexError?!' |
|
835 |
- ) |
|
836 |
- |
|
1040 |
+""" |
|
1041 |
+ ), 'driver program produced unexpected output' |
|
1042 |
+ assert isinstance( |
|
1043 |
+ result.exception, IndexError |
|
1044 |
+ ), 'driver program did not raise IndexError?!' |
|
837 | 1045 |
|
838 | 1046 |
def test_103_prompt_for_passphrase(self, monkeypatch: Any) -> None: |
839 |
- monkeypatch.setattr(click, 'prompt', |
|
840 |
- lambda *a, **kw: |
|
841 |
- json.dumps({'args': a, 'kwargs': kw})) |
|
1047 |
+ monkeypatch.setattr( |
|
1048 |
+ click, |
|
1049 |
+ 'prompt', |
|
1050 |
+ lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw}), |
|
1051 |
+ ) |
|
842 | 1052 |
res = json.loads(cli._prompt_for_passphrase()) |
843 | 1053 |
err_msg = 'missing arguments to passphrase prompt' |
844 | 1054 |
assert 'args' in res, err_msg |
... | ... |
@@ -849,48 +1059,70 @@ Boo. |
849 | 1059 |
assert res['kwargs'].get('err'), err_msg |
850 | 1060 |
assert res['kwargs'].get('hide_input'), err_msg |
851 | 1061 |
|
852 |
- |
|
853 |
- @pytest.mark.parametrize(['command_line', 'config', 'result_config'], [ |
|
854 |
- (['--delete-globals'], |
|
855 |
- {'global': {'phrase': 'abc'}, 'services': {}}, {'services': {}}), |
|
856 |
- (['--delete', DUMMY_SERVICE], |
|
857 |
- {'global': {'phrase': 'abc'}, |
|
858 |
- 'services': {DUMMY_SERVICE: {'notes': '...'}}}, |
|
859 |
- {'global': {'phrase': 'abc'}, 'services': {}}), |
|
860 |
- (['--clear'], |
|
861 |
- {'global': {'phrase': 'abc'}, |
|
862 |
- 'services': {DUMMY_SERVICE: {'notes': '...'}}}, |
|
863 |
- {'services': {}}), |
|
864 |
- ]) |
|
1062 |
+ @pytest.mark.parametrize( |
|
1063 |
+ ['command_line', 'config', 'result_config'], |
|
1064 |
+ [ |
|
1065 |
+ ( |
|
1066 |
+ ['--delete-globals'], |
|
1067 |
+ {'global': {'phrase': 'abc'}, 'services': {}}, |
|
1068 |
+ {'services': {}}, |
|
1069 |
+ ), |
|
1070 |
+ ( |
|
1071 |
+ ['--delete', DUMMY_SERVICE], |
|
1072 |
+ { |
|
1073 |
+ 'global': {'phrase': 'abc'}, |
|
1074 |
+ 'services': {DUMMY_SERVICE: {'notes': '...'}}, |
|
1075 |
+ }, |
|
1076 |
+ {'global': {'phrase': 'abc'}, 'services': {}}, |
|
1077 |
+ ), |
|
1078 |
+ ( |
|
1079 |
+ ['--clear'], |
|
1080 |
+ { |
|
1081 |
+ 'global': {'phrase': 'abc'}, |
|
1082 |
+ 'services': {DUMMY_SERVICE: {'notes': '...'}}, |
|
1083 |
+ }, |
|
1084 |
+ {'services': {}}, |
|
1085 |
+ ), |
|
1086 |
+ ], |
|
1087 |
+ ) |
|
865 | 1088 |
def test_203_repeated_config_deletion( |
866 |
- self, monkeypatch: Any, command_line: list[str], |
|
867 |
- config: dpp.types.VaultConfig, result_config: dpp.types.VaultConfig, |
|
1089 |
+ self, |
|
1090 |
+ monkeypatch: Any, |
|
1091 |
+ command_line: list[str], |
|
1092 |
+ config: dpp.types.VaultConfig, |
|
1093 |
+ result_config: dpp.types.VaultConfig, |
|
868 | 1094 |
) -> None: |
869 | 1095 |
runner = click.testing.CliRunner(mix_stderr=False) |
870 | 1096 |
for start_config in [config, result_config]: |
871 |
- with tests.isolated_config(monkeypatch=monkeypatch, |
|
872 |
- runner=runner, config=start_config): |
|
873 |
- result = runner.invoke(cli.derivepassphrase, command_line, |
|
874 |
- catch_exceptions=False) |
|
875 |
- assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
|
876 |
- 'program exited with failure' |
|
1097 |
+ with tests.isolated_config( |
|
1098 |
+ monkeypatch=monkeypatch, runner=runner, config=start_config |
|
1099 |
+ ): |
|
1100 |
+ result = runner.invoke( |
|
1101 |
+ cli.derivepassphrase, command_line, catch_exceptions=False |
|
877 | 1102 |
) |
1103 |
+ assert (result.exit_code, result.stderr_bytes) == ( |
|
1104 |
+ 0, |
|
1105 |
+ b'', |
|
1106 |
+ ), 'program exited with failure' |
|
878 | 1107 |
with open(cli._config_filename(), encoding='UTF-8') as infile: |
879 | 1108 |
config_readback = json.load(infile) |
880 | 1109 |
assert config_readback == result_config |
881 | 1110 |
|
882 |
- |
|
883 | 1111 |
def test_204_phrase_from_key_manually(self) -> None: |
884 | 1112 |
assert ( |
885 |
- dpp.Vault(phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS) |
|
886 |
- .generate(DUMMY_SERVICE) == DUMMY_RESULT_KEY1 |
|
1113 |
+ dpp.Vault( |
|
1114 |
+ phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS |
|
1115 |
+ ).generate(DUMMY_SERVICE) |
|
1116 |
+ == DUMMY_RESULT_KEY1 |
|
887 | 1117 |
) |
888 | 1118 |
|
889 |
- |
|
890 |
- @pytest.mark.parametrize(['vfunc', 'input'], [ |
|
1119 |
+ @pytest.mark.parametrize( |
|
1120 |
+ ['vfunc', 'input'], |
|
1121 |
+ [ |
|
891 | 1122 |
(cli._validate_occurrence_constraint, 20), |
892 | 1123 |
(cli._validate_length, 20), |
893 |
- ]) |
|
1124 |
+ ], |
|
1125 |
+ ) |
|
894 | 1126 |
def test_210a_validate_constraints_manually( |
895 | 1127 |
self, |
896 | 1128 |
vfunc: Callable[[click.Context, click.Parameter, Any], int | None], |
... | ... |
@@ -904,10 +1135,13 @@ Boo. |
904 | 1135 |
@tests.skip_if_no_agent |
905 | 1136 |
@pytest.mark.parametrize('conn_hint', ['none', 'socket', 'client']) |
906 | 1137 |
def test_227_get_suitable_ssh_keys( |
907 |
- self, monkeypatch: Any, conn_hint: str, |
|
1138 |
+ self, |
|
1139 |
+ monkeypatch: Any, |
|
1140 |
+ conn_hint: str, |
|
908 | 1141 |
) -> None: |
909 |
- monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
910 |
- 'list_keys', tests.list_keys) |
|
1142 |
+ monkeypatch.setattr( |
|
1143 |
+ ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys |
|
1144 |
+ ) |
|
911 | 1145 |
hint: ssh_agent_client.SSHAgentClient | socket.socket | None |
912 | 1146 |
match conn_hint: |
913 | 1147 |
case 'client': |
... | ... |
@@ -10,47 +10,88 @@ from typing_extensions import Any |
10 | 10 |
import derivepassphrase.types |
11 | 11 |
|
12 | 12 |
|
13 |
-@pytest.mark.parametrize(['obj', 'comment'], [ |
|
13 |
+@pytest.mark.parametrize( |
|
14 |
+ ['obj', 'comment'], |
|
15 |
+ [ |
|
14 | 16 |
(None, 'not a dict'), |
15 | 17 |
({}, 'missing required keys'), |
16 | 18 |
({'global': None, 'services': {}}, 'bad config value: global'), |
17 |
- ({'global': {'key': 123}, 'services': {}}, |
|
18 |
- 'bad config value: global.key'), |
|
19 |
- ({'global': {'phrase': 'abc', 'key': '...'}, 'services': {}}, |
|
20 |
- 'incompatible config values: global.key and global.phrase'), |
|
19 |
+ ( |
|
20 |
+ {'global': {'key': 123}, 'services': {}}, |
|
21 |
+ 'bad config value: global.key', |
|
22 |
+ ), |
|
23 |
+ ( |
|
24 |
+ {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}}, |
|
25 |
+ 'incompatible config values: global.key and global.phrase', |
|
26 |
+ ), |
|
21 | 27 |
({'services': None}, 'bad config value: services'), |
22 | 28 |
({'services': {2: {}}}, 'bad config value: services."2"'), |
23 | 29 |
({'services': {'2': 2}}, 'bad config value: services."2"'), |
24 |
- ({'services': {'sv': {'notes': False}}}, |
|
25 |
- 'bad config value: services.sv.notes'), |
|
30 |
+ ( |
|
31 |
+ {'services': {'sv': {'notes': False}}}, |
|
32 |
+ 'bad config value: services.sv.notes', |
|
33 |
+ ), |
|
26 | 34 |
({'services': {'sv': {'notes': 'blah blah blah'}}}, ''), |
27 |
- ({'services': {'sv': {'length': '200'}}}, |
|
28 |
- 'bad config value: services.sv.length'), |
|
29 |
- ({'services': {'sv': {'length': 0.5}}}, |
|
30 |
- 'bad config value: services.sv.length'), |
|
31 |
- ({'services': {'sv': {'length': -10}}}, |
|
32 |
- 'bad config value: services.sv.length'), |
|
33 |
- ({'services': {'sv': {'upper': -10}}}, |
|
34 |
- 'bad config value: services.sv.upper'), |
|
35 |
- ({'global': {'phrase': 'my secret phrase'}, |
|
36 |
- 'services': {'sv': {'length': 10}}}, |
|
37 |
- ''), |
|
35 |
+ ( |
|
36 |
+ {'services': {'sv': {'length': '200'}}}, |
|
37 |
+ 'bad config value: services.sv.length', |
|
38 |
+ ), |
|
39 |
+ ( |
|
40 |
+ {'services': {'sv': {'length': 0.5}}}, |
|
41 |
+ 'bad config value: services.sv.length', |
|
42 |
+ ), |
|
43 |
+ ( |
|
44 |
+ {'services': {'sv': {'length': -10}}}, |
|
45 |
+ 'bad config value: services.sv.length', |
|
46 |
+ ), |
|
47 |
+ ( |
|
48 |
+ {'services': {'sv': {'upper': -10}}}, |
|
49 |
+ 'bad config value: services.sv.upper', |
|
50 |
+ ), |
|
51 |
+ ( |
|
52 |
+ { |
|
53 |
+ 'global': {'phrase': 'my secret phrase'}, |
|
54 |
+ 'services': {'sv': {'length': 10}}, |
|
55 |
+ }, |
|
56 |
+ '', |
|
57 |
+ ), |
|
38 | 58 |
({'services': {'sv': {'length': 10, 'phrase': '...'}}}, ''), |
39 | 59 |
({'services': {'sv': {'length': 10, 'key': '...'}}}, ''), |
40 | 60 |
({'services': {'sv': {'upper': 10, 'key': '...'}}}, ''), |
41 |
- ({'services': {'sv': {'phrase': 'abc', 'key': '...'}}}, |
|
42 |
- 'incompatible config values: services.sv.key and services.sv.phrase'), |
|
43 |
- ({'global': {'phrase': 'abc'}, |
|
44 |
- 'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''), |
|
45 |
- ({'global': {'key': '...'}, |
|
46 |
- 'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''), |
|
47 |
- ({'global': {'key': '...'}, |
|
48 |
- 'services': {'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
49 |
- 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}}}, ''), |
|
50 |
-]) |
|
61 |
+ ( |
|
62 |
+ {'services': {'sv': {'phrase': 'abc', 'key': '...'}}}, |
|
63 |
+ 'incompatible config values: services.sv.key and services.sv.phrase', # noqa: E501 |
|
64 |
+ ), |
|
65 |
+ ( |
|
66 |
+ { |
|
67 |
+ 'global': {'phrase': 'abc'}, |
|
68 |
+ 'services': {'sv': {'phrase': 'abc', 'length': 10}}, |
|
69 |
+ }, |
|
70 |
+ '', |
|
71 |
+ ), |
|
72 |
+ ( |
|
73 |
+ { |
|
74 |
+ 'global': {'key': '...'}, |
|
75 |
+ 'services': {'sv': {'phrase': 'abc', 'length': 10}}, |
|
76 |
+ }, |
|
77 |
+ '', |
|
78 |
+ ), |
|
79 |
+ ( |
|
80 |
+ { |
|
81 |
+ 'global': {'key': '...'}, |
|
82 |
+ 'services': { |
|
83 |
+ 'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1}, |
|
84 |
+ 'sv2': {'length': 10, 'repeat': 1, 'lower': 1}, |
|
85 |
+ }, |
|
86 |
+ }, |
|
87 |
+ '', |
|
88 |
+ ), |
|
89 |
+ ], |
|
90 |
+) |
|
51 | 91 |
def test_200_is_vault_config(obj: Any, comment: str) -> None: |
52 | 92 |
is_vault_config = derivepassphrase.types.is_vault_config |
53 | 93 |
assert is_vault_config(obj) == (not comment), ( |
54 |
- 'failed to complain about: ' + comment if comment |
|
94 |
+ 'failed to complain about: ' + comment |
|
95 |
+ if comment |
|
55 | 96 |
else 'failed on valid example' |
56 | 97 |
) |
... | ... |
@@ -12,53 +12,107 @@ import sequin |
12 | 12 |
|
13 | 13 |
|
14 | 14 |
class TestStaticFunctionality: |
15 |
- |
|
16 |
- @pytest.mark.parametrize(['sequence', 'base', 'expected'], [ |
|
15 |
+ @pytest.mark.parametrize( |
|
16 |
+ ['sequence', 'base', 'expected'], |
|
17 |
+ [ |
|
17 | 18 |
([1, 2, 3, 4, 5, 6], 10, 123456), |
18 | 19 |
([1, 2, 3, 4, 5, 6], 100, 10203040506), |
19 | 20 |
([0, 0, 1, 4, 9, 7], 10, 1497), |
20 | 21 |
([1, 0, 0, 1, 0, 0, 0, 0], 2, 144), |
21 | 22 |
([1, 7, 5, 5], 8, 0o1755), |
22 |
- ]) |
|
23 |
+ ], |
|
24 |
+ ) |
|
23 | 25 |
def test_200_big_endian_number(self, sequence, base, expected): |
24 | 26 |
assert ( |
25 | 27 |
sequin.Sequin._big_endian_number(sequence, base=base) |
26 | 28 |
) == expected |
27 | 29 |
|
28 | 30 |
@pytest.mark.parametrize( |
29 |
- ['exc_type', 'exc_pattern', 'sequence', 'base'], [ |
|
31 |
+ ['exc_type', 'exc_pattern', 'sequence', 'base'], |
|
32 |
+ [ |
|
30 | 33 |
(ValueError, 'invalid base 3 digit:', [-1], 3), |
31 | 34 |
(ValueError, 'invalid base:', [0], 1), |
32 | 35 |
(TypeError, 'not an integer:', [0.0, 1.0, 0.0, 1.0], 2), |
33 |
- ] |
|
36 |
+ ], |
|
34 | 37 |
) |
35 |
- def test_300_big_endian_number_exceptions(self, exc_type, exc_pattern, |
|
36 |
- sequence, base): |
|
38 |
+ def test_300_big_endian_number_exceptions( |
|
39 |
+ self, exc_type, exc_pattern, sequence, base |
|
40 |
+ ): |
|
37 | 41 |
with pytest.raises(exc_type, match=exc_pattern): |
38 | 42 |
sequin.Sequin._big_endian_number(sequence, base=base) |
39 | 43 |
|
40 |
-class TestSequin: |
|
41 | 44 |
|
42 |
- @pytest.mark.parametrize(['sequence', 'is_bitstring', 'expected'], [ |
|
43 |
- ([1, 0, 0, 1, 0, 1], False, [0, 0, 0, 0, 0, 0, 0, 1, |
|
44 |
- 0, 0, 0, 0, 0, 0, 0, 0, |
|
45 |
- 0, 0, 0, 0, 0, 0, 0, 0, |
|
46 |
- 0, 0, 0, 0, 0, 0, 0, 1, |
|
47 |
- 0, 0, 0, 0, 0, 0, 0, 0, |
|
48 |
- 0, 0, 0, 0, 0, 0, 0, 1]), |
|
45 |
+class TestSequin: |
|
46 |
+ @pytest.mark.parametrize( |
|
47 |
+ ['sequence', 'is_bitstring', 'expected'], |
|
48 |
+ [ |
|
49 |
+ ( |
|
50 |
+ [1, 0, 0, 1, 0, 1], |
|
51 |
+ False, |
|
52 |
+ [ |
|
53 |
+ 0, |
|
54 |
+ 0, |
|
55 |
+ 0, |
|
56 |
+ 0, |
|
57 |
+ 0, |
|
58 |
+ 0, |
|
59 |
+ 0, |
|
60 |
+ 1, |
|
61 |
+ 0, |
|
62 |
+ 0, |
|
63 |
+ 0, |
|
64 |
+ 0, |
|
65 |
+ 0, |
|
66 |
+ 0, |
|
67 |
+ 0, |
|
68 |
+ 0, |
|
69 |
+ 0, |
|
70 |
+ 0, |
|
71 |
+ 0, |
|
72 |
+ 0, |
|
73 |
+ 0, |
|
74 |
+ 0, |
|
75 |
+ 0, |
|
76 |
+ 0, |
|
77 |
+ 0, |
|
78 |
+ 0, |
|
79 |
+ 0, |
|
80 |
+ 0, |
|
81 |
+ 0, |
|
82 |
+ 0, |
|
83 |
+ 0, |
|
84 |
+ 1, |
|
85 |
+ 0, |
|
86 |
+ 0, |
|
87 |
+ 0, |
|
88 |
+ 0, |
|
89 |
+ 0, |
|
90 |
+ 0, |
|
91 |
+ 0, |
|
92 |
+ 0, |
|
93 |
+ 0, |
|
94 |
+ 0, |
|
95 |
+ 0, |
|
96 |
+ 0, |
|
97 |
+ 0, |
|
98 |
+ 0, |
|
99 |
+ 0, |
|
100 |
+ 1, |
|
101 |
+ ], |
|
102 |
+ ), |
|
49 | 103 |
([1, 0, 0, 1, 0, 1], True, [1, 0, 0, 1, 0, 1]), |
50 |
- (b'OK', False, [0, 1, 0, 0, 1, 1, 1, 1, |
|
51 |
- 0, 1, 0, 0, 1, 0, 1, 1]), |
|
52 |
- ('OK', False, [0, 1, 0, 0, 1, 1, 1, 1, |
|
53 |
- 0, 1, 0, 0, 1, 0, 1, 1]), |
|
54 |
- ]) |
|
104 |
+ (b'OK', False, [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1]), |
|
105 |
+ ('OK', False, [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1]), |
|
106 |
+ ], |
|
107 |
+ ) |
|
55 | 108 |
def test_200_constructor(self, sequence, is_bitstring, expected): |
56 | 109 |
seq = sequin.Sequin(sequence, is_bitstring=is_bitstring) |
57 | 110 |
assert seq.bases == {2: collections.deque(expected)} |
58 | 111 |
|
59 | 112 |
def test_201_generating(self): |
60 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
61 |
- is_bitstring=True) |
|
113 |
+ seq = sequin.Sequin( |
|
114 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
115 |
+ ) |
|
62 | 116 |
assert seq.generate(1) == 0 |
63 | 117 |
assert seq.generate(5) == 3 |
64 | 118 |
assert seq.generate(5) == 3 |
... | ... |
@@ -67,21 +121,24 @@ class TestSequin: |
67 | 121 |
seq.generate(5) |
68 | 122 |
with pytest.raises(sequin.SequinExhaustedError): |
69 | 123 |
seq.generate(1) |
70 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
71 |
- is_bitstring=True) |
|
124 |
+ seq = sequin.Sequin( |
|
125 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
126 |
+ ) |
|
72 | 127 |
with pytest.raises(ValueError, match='invalid target range'): |
73 | 128 |
seq.generate(0) |
74 | 129 |
|
75 | 130 |
def test_210_internal_generating(self): |
76 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
77 |
- is_bitstring=True) |
|
131 |
+ seq = sequin.Sequin( |
|
132 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
133 |
+ ) |
|
78 | 134 |
assert seq._generate_inner(5) == 3 |
79 | 135 |
assert seq._generate_inner(5) == 3 |
80 | 136 |
assert seq._generate_inner(5) == 1 |
81 | 137 |
assert seq._generate_inner(5) == 5 |
82 | 138 |
assert seq._generate_inner(1) == 0 |
83 |
- seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], |
|
84 |
- is_bitstring=True) |
|
139 |
+ seq = sequin.Sequin( |
|
140 |
+ [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True |
|
141 |
+ ) |
|
85 | 142 |
assert seq._generate_inner(1) == 0 |
86 | 143 |
with pytest.raises(ValueError, match='invalid target range'): |
87 | 144 |
seq._generate_inner(0) |
... | ... |
@@ -90,8 +147,9 @@ class TestSequin: |
90 | 147 |
|
91 | 148 |
def test_211_shifting(self): |
92 | 149 |
seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
93 |
- assert seq.bases == {2: collections.deque([ |
|
94 |
- 1, 0, 1, 0, 0, 1, 0, 0, 0, 1])} |
|
150 |
+ assert seq.bases == { |
|
151 |
+ 2: collections.deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1]) |
|
152 |
+ } |
|
95 | 153 |
|
96 | 154 |
assert seq._all_or_nothing_shift(3) == (1, 0, 1) |
97 | 155 |
assert seq._all_or_nothing_shift(3) == (0, 0, 1) |
... | ... |
@@ -106,13 +164,17 @@ class TestSequin: |
106 | 164 |
@pytest.mark.parametrize( |
107 | 165 |
['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'], |
108 | 166 |
[ |
109 |
- ([0, 1, 2, 3, 4, 5, 6, 7], True, |
|
110 |
- ValueError, 'sequence item out of range'), |
|
111 |
- ('こんにちは。', False, |
|
112 |
- ValueError, 'sequence item out of range'), |
|
113 |
- ] |
|
167 |
+ ( |
|
168 |
+ [0, 1, 2, 3, 4, 5, 6, 7], |
|
169 |
+ True, |
|
170 |
+ ValueError, |
|
171 |
+ 'sequence item out of range', |
|
172 |
+ ), |
|
173 |
+ ('こんにちは。', False, ValueError, 'sequence item out of range'), |
|
174 |
+ ], |
|
114 | 175 |
) |
115 |
- def test_300_constructor_exceptions(self, sequence, is_bitstring, |
|
116 |
- exc_type, exc_pattern): |
|
176 |
+ def test_300_constructor_exceptions( |
|
177 |
+ self, sequence, is_bitstring, exc_type, exc_pattern |
|
178 |
+ ): |
|
117 | 179 |
with pytest.raises(exc_type, match=exc_pattern): |
118 | 180 |
sequin.Sequin(sequence, is_bitstring=is_bitstring) |
... | ... |
@@ -24,10 +24,13 @@ import tests |
24 | 24 |
|
25 | 25 |
|
26 | 26 |
class TestStaticFunctionality: |
27 |
- |
|
28 |
- @pytest.mark.parametrize(['public_key', 'public_key_data'], |
|
29 |
- [(val['public_key'], val['public_key_data']) |
|
30 |
- for val in tests.SUPPORTED_KEYS.values()]) |
|
27 |
+ @pytest.mark.parametrize( |
|
28 |
+ ['public_key', 'public_key_data'], |
|
29 |
+ [ |
|
30 |
+ (val['public_key'], val['public_key_data']) |
|
31 |
+ for val in tests.SUPPORTED_KEYS.values() |
|
32 |
+ ], |
|
33 |
+ ) |
|
31 | 34 |
def test_100_key_decoding(self, public_key, public_key_data): |
32 | 35 |
keydata = base64.b64decode(public_key.split(None, 2)[1]) |
33 | 36 |
assert ( |
... | ... |
@@ -37,77 +40,101 @@ class TestStaticFunctionality: |
37 | 40 |
def test_200_constructor_no_running_agent(self, monkeypatch): |
38 | 41 |
monkeypatch.delenv('SSH_AUTH_SOCK', raising=False) |
39 | 42 |
sock = socket.socket(family=socket.AF_UNIX) |
40 |
- with pytest.raises(KeyError, |
|
41 |
- match='SSH_AUTH_SOCK environment variable'): |
|
43 |
+ with pytest.raises( |
|
44 |
+ KeyError, match='SSH_AUTH_SOCK environment variable' |
|
45 |
+ ): |
|
42 | 46 |
ssh_agent_client.SSHAgentClient(socket=sock) |
43 | 47 |
|
44 |
- @pytest.mark.parametrize(['input', 'expected'], [ |
|
48 |
+ @pytest.mark.parametrize( |
|
49 |
+ ['input', 'expected'], |
|
50 |
+ [ |
|
45 | 51 |
(16777216, b'\x01\x00\x00\x00'), |
46 |
- ]) |
|
52 |
+ ], |
|
53 |
+ ) |
|
47 | 54 |
def test_210_uint32(self, input, expected): |
48 | 55 |
uint32 = ssh_agent_client.SSHAgentClient.uint32 |
49 | 56 |
assert uint32(input) == expected |
50 | 57 |
|
51 |
- @pytest.mark.parametrize(['input', 'expected'], [ |
|
58 |
+ @pytest.mark.parametrize( |
|
59 |
+ ['input', 'expected'], |
|
60 |
+ [ |
|
52 | 61 |
(b'ssh-rsa', b'\x00\x00\x00\x07ssh-rsa'), |
53 | 62 |
(b'ssh-ed25519', b'\x00\x00\x00\x0bssh-ed25519'), |
54 | 63 |
( |
55 | 64 |
ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
56 | 65 |
b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519', |
57 | 66 |
), |
58 |
- ]) |
|
67 |
+ ], |
|
68 |
+ ) |
|
59 | 69 |
def test_211_string(self, input, expected): |
60 | 70 |
string = ssh_agent_client.SSHAgentClient.string |
61 | 71 |
assert bytes(string(input)) == expected |
62 | 72 |
|
63 |
- @pytest.mark.parametrize(['input', 'expected'], [ |
|
73 |
+ @pytest.mark.parametrize( |
|
74 |
+ ['input', 'expected'], |
|
75 |
+ [ |
|
64 | 76 |
(b'\x00\x00\x00\x07ssh-rsa', b'ssh-rsa'), |
65 | 77 |
( |
66 | 78 |
ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'), |
67 | 79 |
b'ssh-ed25519', |
68 | 80 |
), |
69 |
- ]) |
|
81 |
+ ], |
|
82 |
+ ) |
|
70 | 83 |
def test_212_unstring(self, input, expected): |
71 | 84 |
unstring = ssh_agent_client.SSHAgentClient.unstring |
72 | 85 |
unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
73 | 86 |
assert bytes(unstring(input)) == expected |
74 |
- assert tuple( |
|
75 |
- bytes(x) for x in unstring_prefix(input) |
|
76 |
- ) == (expected, b'') |
|
87 |
+ assert tuple(bytes(x) for x in unstring_prefix(input)) == ( |
|
88 |
+ expected, |
|
89 |
+ b'', |
|
90 |
+ ) |
|
77 | 91 |
|
78 |
- @pytest.mark.parametrize(['value', 'exc_type', 'exc_pattern'], [ |
|
92 |
+ @pytest.mark.parametrize( |
|
93 |
+ ['value', 'exc_type', 'exc_pattern'], |
|
94 |
+ [ |
|
79 | 95 |
(10000000000000000, OverflowError, 'int too big to convert'), |
80 | 96 |
(-1, OverflowError, "can't convert negative int to unsigned"), |
81 |
- ]) |
|
97 |
+ ], |
|
98 |
+ ) |
|
82 | 99 |
def test_310_uint32_exceptions(self, value, exc_type, exc_pattern): |
83 | 100 |
uint32 = ssh_agent_client.SSHAgentClient.uint32 |
84 | 101 |
with pytest.raises(exc_type, match=exc_pattern): |
85 | 102 |
uint32(value) |
86 | 103 |
|
87 |
- @pytest.mark.parametrize(['input', 'exc_type', 'exc_pattern'], [ |
|
104 |
+ @pytest.mark.parametrize( |
|
105 |
+ ['input', 'exc_type', 'exc_pattern'], |
|
106 |
+ [ |
|
88 | 107 |
('some string', TypeError, 'invalid payload type'), |
89 |
- ]) |
|
108 |
+ ], |
|
109 |
+ ) |
|
90 | 110 |
def test_311_string_exceptions(self, input, exc_type, exc_pattern): |
91 | 111 |
string = ssh_agent_client.SSHAgentClient.string |
92 | 112 |
with pytest.raises(exc_type, match=exc_pattern): |
93 | 113 |
string(input) |
94 | 114 |
|
95 | 115 |
@pytest.mark.parametrize( |
96 |
- ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], [ |
|
116 |
+ ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], |
|
117 |
+ [ |
|
97 | 118 |
(b'ssh', ValueError, 'malformed SSH byte string', False, None), |
98 | 119 |
( |
99 | 120 |
b'\x00\x00\x00\x08ssh-rsa', |
100 |
- ValueError, 'malformed SSH byte string', |
|
101 |
- False, None, |
|
121 |
+ ValueError, |
|
122 |
+ 'malformed SSH byte string', |
|
123 |
+ False, |
|
124 |
+ None, |
|
102 | 125 |
), |
103 | 126 |
( |
104 | 127 |
b'\x00\x00\x00\x04XXX trailing text', |
105 |
- ValueError, 'malformed SSH byte string', |
|
106 |
- True, (b'XXX ', b'trailing text'), |
|
128 |
+ ValueError, |
|
129 |
+ 'malformed SSH byte string', |
|
130 |
+ True, |
|
131 |
+ (b'XXX ', b'trailing text'), |
|
107 | 132 |
), |
108 |
- ]) |
|
109 |
- def test_312_unstring_exceptions(self, input, exc_type, exc_pattern, |
|
110 |
- has_trailer, parts): |
|
133 |
+ ], |
|
134 |
+ ) |
|
135 |
+ def test_312_unstring_exceptions( |
|
136 |
+ self, input, exc_type, exc_pattern, has_trailer, parts |
|
137 |
+ ): |
|
111 | 138 |
unstring = ssh_agent_client.SSHAgentClient.unstring |
112 | 139 |
unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix |
113 | 140 |
with pytest.raises(exc_type, match=exc_pattern): |
... | ... |
@@ -118,22 +145,26 @@ class TestStaticFunctionality: |
118 | 145 |
with pytest.raises(exc_type, match=exc_pattern): |
119 | 146 |
unstring_prefix(input) |
120 | 147 |
|
148 |
+ |
|
121 | 149 |
@tests.skip_if_no_agent |
122 | 150 |
class TestAgentInteraction: |
123 |
- |
|
124 |
- @pytest.mark.parametrize(['keytype', 'data_dict'], |
|
125 |
- list(tests.SUPPORTED_KEYS.items())) |
|
151 |
+ @pytest.mark.parametrize( |
|
152 |
+ ['keytype', 'data_dict'], list(tests.SUPPORTED_KEYS.items()) |
|
153 |
+ ) |
|
126 | 154 |
def test_200_sign_data_via_agent(self, keytype, data_dict): |
127 | 155 |
del keytype # Unused. |
128 | 156 |
private_key = data_dict['private_key'] |
129 | 157 |
try: |
130 |
- _ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
131 |
- input=private_key, check=True, |
|
132 |
- capture_output=True) |
|
158 |
+ _ = subprocess.run( |
|
159 |
+ ['ssh-add', '-t', '30', '-q', '-'], |
|
160 |
+ input=private_key, |
|
161 |
+ check=True, |
|
162 |
+ capture_output=True, |
|
163 |
+ ) |
|
133 | 164 |
except subprocess.CalledProcessError as e: |
134 | 165 |
pytest.skip( |
135 |
- f"uploading test key: {e!r}, stdout={e.stdout!r}, " |
|
136 |
- f"stderr={e.stderr!r}" |
|
166 |
+ f'uploading test key: {e!r}, stdout={e.stdout!r}, ' |
|
167 |
+ f'stderr={e.stderr!r}' |
|
137 | 168 |
) |
138 | 169 |
else: |
139 | 170 |
try: |
... | ... |
@@ -141,37 +172,48 @@ class TestAgentInteraction: |
141 | 172 |
except OSError: # pragma: no cover |
142 | 173 |
pytest.skip('communication error with the SSH agent') |
143 | 174 |
with client: |
144 |
- key_comment_pairs = {bytes(k): bytes(c) |
|
145 |
- for k, c in client.list_keys()} |
|
175 |
+ key_comment_pairs = { |
|
176 |
+ bytes(k): bytes(c) for k, c in client.list_keys() |
|
177 |
+ } |
|
146 | 178 |
public_key_data = data_dict['public_key_data'] |
147 | 179 |
expected_signature = data_dict['expected_signature'] |
148 | 180 |
derived_passphrase = data_dict['derived_passphrase'] |
149 | 181 |
if public_key_data not in key_comment_pairs: # pragma: no cover |
150 | 182 |
pytest.skip('prerequisite SSH key not loaded') |
151 |
- signature = bytes(client.sign( |
|
152 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
183 |
+ signature = bytes( |
|
184 |
+ client.sign( |
|
185 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
186 |
+ ) |
|
187 |
+ ) |
|
153 | 188 |
assert signature == expected_signature, 'SSH signature mismatch' |
154 |
- signature2 = bytes(client.sign( |
|
155 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
189 |
+ signature2 = bytes( |
|
190 |
+ client.sign( |
|
191 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
192 |
+ ) |
|
193 |
+ ) |
|
156 | 194 |
assert signature2 == expected_signature, 'SSH signature mismatch' |
157 | 195 |
assert ( |
158 |
- derivepassphrase.Vault.phrase_from_key(public_key_data) == |
|
159 |
- derived_passphrase |
|
196 |
+ derivepassphrase.Vault.phrase_from_key(public_key_data) |
|
197 |
+ == derived_passphrase |
|
160 | 198 |
), 'SSH signature mismatch' |
161 | 199 |
|
162 |
- @pytest.mark.parametrize(['keytype', 'data_dict'], |
|
163 |
- list(tests.UNSUITABLE_KEYS.items())) |
|
200 |
+ @pytest.mark.parametrize( |
|
201 |
+ ['keytype', 'data_dict'], list(tests.UNSUITABLE_KEYS.items()) |
|
202 |
+ ) |
|
164 | 203 |
def test_201_sign_data_via_agent_unsupported(self, keytype, data_dict): |
165 | 204 |
del keytype # Unused. |
166 | 205 |
private_key = data_dict['private_key'] |
167 | 206 |
try: |
168 |
- _ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
|
169 |
- input=private_key, check=True, |
|
170 |
- capture_output=True) |
|
207 |
+ _ = subprocess.run( |
|
208 |
+ ['ssh-add', '-t', '30', '-q', '-'], |
|
209 |
+ input=private_key, |
|
210 |
+ check=True, |
|
211 |
+ capture_output=True, |
|
212 |
+ ) |
|
171 | 213 |
except subprocess.CalledProcessError as e: # pragma: no cover |
172 | 214 |
pytest.skip( |
173 |
- f"uploading test key: {e!r}, stdout={e.stdout!r}, " |
|
174 |
- f"stderr={e.stderr!r}" |
|
215 |
+ f'uploading test key: {e!r}, stdout={e.stdout!r}, ' |
|
216 |
+ f'stderr={e.stderr!r}' |
|
175 | 217 |
) |
176 | 218 |
else: |
177 | 219 |
try: |
... | ... |
@@ -179,16 +221,23 @@ class TestAgentInteraction: |
179 | 221 |
except OSError: # pragma: no cover |
180 | 222 |
pytest.skip('communication error with the SSH agent') |
181 | 223 |
with client: |
182 |
- key_comment_pairs = {bytes(k): bytes(c) |
|
183 |
- for k, c in client.list_keys()} |
|
224 |
+ key_comment_pairs = { |
|
225 |
+ bytes(k): bytes(c) for k, c in client.list_keys() |
|
226 |
+ } |
|
184 | 227 |
public_key_data = data_dict['public_key_data'] |
185 | 228 |
_ = data_dict['expected_signature'] |
186 | 229 |
if public_key_data not in key_comment_pairs: # pragma: no cover |
187 | 230 |
pytest.skip('prerequisite SSH key not loaded') |
188 |
- signature = bytes(client.sign( |
|
189 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
190 |
- signature2 = bytes(client.sign( |
|
191 |
- payload=derivepassphrase.Vault._UUID, key=public_key_data)) |
|
231 |
+ signature = bytes( |
|
232 |
+ client.sign( |
|
233 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
234 |
+ ) |
|
235 |
+ ) |
|
236 |
+ signature2 = bytes( |
|
237 |
+ client.sign( |
|
238 |
+ payload=derivepassphrase.Vault._UUID, key=public_key_data |
|
239 |
+ ) |
|
240 |
+ ) |
|
192 | 241 |
assert signature != signature2, 'SSH signature repeatable?!' |
193 | 242 |
with pytest.raises(ValueError, match='unsuitable SSH key'): |
194 | 243 |
derivepassphrase.Vault.phrase_from_key(public_key_data) |
... | ... |
@@ -207,20 +256,32 @@ class TestAgentInteraction: |
207 | 256 |
@pytest.mark.parametrize(['key', 'single'], list(_params())) |
208 | 257 |
def test_210_ssh_key_selector(self, monkeypatch, key, single): |
209 | 258 |
def key_is_suitable(key: bytes): |
210 |
- return key in {v['public_key_data'] |
|
211 |
- for v in tests.SUPPORTED_KEYS.values()} |
|
259 |
+ return key in { |
|
260 |
+ v['public_key_data'] for v in tests.SUPPORTED_KEYS.values() |
|
261 |
+ } |
|
262 |
+ |
|
212 | 263 |
if single: |
213 |
- monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
214 |
- 'list_keys', tests.list_keys_singleton) |
|
215 |
- keys = [pair.key for pair in tests.list_keys_singleton() |
|
216 |
- if key_is_suitable(pair.key)] |
|
264 |
+ monkeypatch.setattr( |
|
265 |
+ ssh_agent_client.SSHAgentClient, |
|
266 |
+ 'list_keys', |
|
267 |
+ tests.list_keys_singleton, |
|
268 |
+ ) |
|
269 |
+ keys = [ |
|
270 |
+ pair.key |
|
271 |
+ for pair in tests.list_keys_singleton() |
|
272 |
+ if key_is_suitable(pair.key) |
|
273 |
+ ] |
|
217 | 274 |
index = '1' |
218 | 275 |
text = 'Use this key? yes\n' |
219 | 276 |
else: |
220 |
- monkeypatch.setattr(ssh_agent_client.SSHAgentClient, |
|
221 |
- 'list_keys', tests.list_keys) |
|
222 |
- keys = [pair.key for pair in tests.list_keys() |
|
223 |
- if key_is_suitable(pair.key)] |
|
277 |
+ monkeypatch.setattr( |
|
278 |
+ ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys |
|
279 |
+ ) |
|
280 |
+ keys = [ |
|
281 |
+ pair.key |
|
282 |
+ for pair in tests.list_keys() |
|
283 |
+ if key_is_suitable(pair.key) |
|
284 |
+ ] |
|
224 | 285 |
index = str(1 + keys.index(key)) |
225 | 286 |
n = len(keys) |
226 | 287 |
text = f'Your selection? (1-{n}, leave empty to abort): {index}\n' |
... | ... |
@@ -232,31 +293,36 @@ class TestAgentInteraction: |
232 | 293 |
click.echo(base64.standard_b64encode(key).decode('ASCII')) |
233 | 294 |
|
234 | 295 |
runner = click.testing.CliRunner(mix_stderr=True) |
235 |
- result = runner.invoke(driver, [], |
|
296 |
+ result = runner.invoke( |
|
297 |
+ driver, |
|
298 |
+ [], |
|
236 | 299 |
input=('yes\n' if single else f'{index}\n'), |
237 |
- catch_exceptions=True) |
|
238 |
- assert result.stdout.startswith('Suitable SSH keys:\n'), ( |
|
239 |
- 'missing expected output' |
|
300 |
+ catch_exceptions=True, |
|
240 | 301 |
) |
302 |
+ assert result.stdout.startswith( |
|
303 |
+ 'Suitable SSH keys:\n' |
|
304 |
+ ), 'missing expected output' |
|
241 | 305 |
assert text in result.stdout, 'missing expected output' |
242 |
- assert ( |
|
243 |
- result.stdout.endswith(f'\n{b64_key}\n') |
|
306 |
+ assert result.stdout.endswith( |
|
307 |
+ f'\n{b64_key}\n' |
|
244 | 308 |
), 'missing expected output' |
245 | 309 |
assert result.exit_code == 0, 'driver program failed?!' |
246 | 310 |
|
247 | 311 |
del _params |
248 | 312 |
|
249 | 313 |
def test_300_constructor_bad_running_agent(self, monkeypatch): |
250 |
- monkeypatch.setenv('SSH_AUTH_SOCK', |
|
251 |
- os.environ['SSH_AUTH_SOCK'] + '~') |
|
314 |
+ monkeypatch.setenv('SSH_AUTH_SOCK', os.environ['SSH_AUTH_SOCK'] + '~') |
|
252 | 315 |
sock = socket.socket(family=socket.AF_UNIX) |
253 | 316 |
with pytest.raises(OSError): # noqa: PT011 |
254 | 317 |
ssh_agent_client.SSHAgentClient(socket=sock) |
255 | 318 |
|
256 |
- @pytest.mark.parametrize('response', [ |
|
319 |
+ @pytest.mark.parametrize( |
|
320 |
+ 'response', |
|
321 |
+ [ |
|
257 | 322 |
b'\x00\x00', |
258 | 323 |
b'\x00\x00\x00\x1f some bytes missing', |
259 |
- ]) |
|
324 |
+ ], |
|
325 |
+ ) |
|
260 | 326 |
def test_310_truncated_server_response(self, monkeypatch, response): |
261 | 327 |
client = ssh_agent_client.SSHAgentClient() |
262 | 328 |
response_stream = io.BytesIO(response) |
... | ... |
@@ -282,13 +351,17 @@ class TestAgentInteraction: |
282 | 351 |
ssh_agent_client.TrailingDataError, |
283 | 352 |
'Overlong response', |
284 | 353 |
), |
285 |
- ] |
|
354 |
+ ], |
|
286 | 355 |
) |
287 |
- def test_320_list_keys_error_responses(self, monkeypatch, response_code, |
|
288 |
- response, exc_type, exc_pattern): |
|
356 |
+ def test_320_list_keys_error_responses( |
|
357 |
+ self, monkeypatch, response_code, response, exc_type, exc_pattern |
|
358 |
+ ): |
|
289 | 359 |
client = ssh_agent_client.SSHAgentClient() |
290 |
- monkeypatch.setattr(client, 'request', |
|
291 |
- lambda *a, **kw: (response_code, response)) # noqa: ARG005 |
|
360 |
+ monkeypatch.setattr( |
|
361 |
+ client, |
|
362 |
+ 'request', |
|
363 |
+ lambda *a, **kw: (response_code, response), # noqa: ARG005 |
|
364 |
+ ) |
|
292 | 365 |
with pytest.raises(exc_type, match=exc_pattern): |
293 | 366 |
client.list_keys() |
294 | 367 |
|
... | ... |
@@ -309,16 +382,19 @@ class TestAgentInteraction: |
309 | 382 |
(255, b''), |
310 | 383 |
RuntimeError, |
311 | 384 |
'signing data failed:', |
385 |
+ ), |
|
386 |
+ ], |
|
312 | 387 |
) |
313 |
- ] |
|
314 |
- ) |
|
315 |
- def test_330_sign_error_responses(self, monkeypatch, key, check, |
|
316 |
- response, exc_type, exc_pattern): |
|
388 |
+ def test_330_sign_error_responses( |
|
389 |
+ self, monkeypatch, key, check, response, exc_type, exc_pattern |
|
390 |
+ ): |
|
317 | 391 |
client = ssh_agent_client.SSHAgentClient() |
318 | 392 |
monkeypatch.setattr(client, 'request', lambda a, b: response) # noqa: ARG005 |
319 | 393 |
KeyCommentPair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
320 |
- loaded_keys = [KeyCommentPair(v['public_key_data'], b'no comment') |
|
321 |
- for v in tests.SUPPORTED_KEYS.values()] |
|
394 |
+ loaded_keys = [ |
|
395 |
+ KeyCommentPair(v['public_key_data'], b'no comment') |
|
396 |
+ for v in tests.SUPPORTED_KEYS.values() |
|
397 |
+ ] |
|
322 | 398 |
monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys) |
323 | 399 |
with pytest.raises(exc_type, match=exc_pattern): |
324 | 400 |
client.sign(key, b'abc', check_if_key_loaded=check) |
325 | 401 |