61f4c67ce11a1dd77b59613e550822d64877c904
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 2 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <software@the13thletter.info>
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

2) #
3) # SPDX-License-Identifier: MIT
4) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

5) """Command-line interface for derivepassphrase."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

6) 
7) from __future__ import annotations
8) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

9) import base64
10) import collections
11) import contextlib
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

12) import copy
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

13) import importlib
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

14) import inspect
15) import json
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

16) import logging
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

17) import os
18) import socket
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

19) import unicodedata
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

20) from typing import (
21)     TYPE_CHECKING,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

22)     Literal,
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

23)     NoReturn,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

24)     TextIO,
25)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

26) )
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

27) 
28) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

29) from typing_extensions import (
30)     Any,
31)     assert_never,
32) )
33) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

34) import derivepassphrase as dpp
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

35) from derivepassphrase import _types, exporter, ssh_agent, vault
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

36) 
37) if TYPE_CHECKING:
38)     import pathlib
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

39)     import types
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

40)     from collections.abc import (
41)         Iterator,
42)         Sequence,
43)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

44) 
45) __author__ = dpp.__author__
46) __version__ = dpp.__version__
47) 
48) __all__ = ('derivepassphrase',)
49) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

50) PROG_NAME = 'derivepassphrase'
51) KEY_DISPLAY_LENGTH = 30
52) 
53) # Error messages
54) _INVALID_VAULT_CONFIG = 'Invalid vault config'
55) _AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent'
56) _NO_USABLE_KEYS = 'No usable SSH keys were found'
57) _EMPTY_SELECTION = 'Empty selection'
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

58) 
59) 
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

60) # Top-level
61) # =========
62) 
63) 
64) @click.command(
65)     context_settings={
66)         'help_option_names': ['-h', '--help'],
67)         'ignore_unknown_options': True,
68)         'allow_interspersed_args': False,
69)     },
70)     epilog=r"""
71)         Configuration is stored in a directory according to the
72)         DERIVEPASSPHRASE_PATH variable, which defaults to
73)         `~/.derivepassphrase` on UNIX-like systems and
74)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
75)     """
76) )
77) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
78) @click.argument('subcommand_args', nargs=-1, type=click.UNPROCESSED)
79) def derivepassphrase(
80)     *,
81)     subcommand_args: list[str],
82) ) -> None:
83)     """Derive a strong passphrase, deterministically, from a master secret.
84) 
85)     Using a master secret, derive a passphrase for a named service,
86)     subject to constraints e.g. on passphrase length, allowed
87)     characters, etc.  The exact derivation depends on the selected
88)     derivation scheme.  For each scheme, it is computationally
89)     infeasible to discern the master secret from the derived passphrase.
90)     The derivations are also deterministic, given the same inputs, thus
91)     the resulting passphrases need not be stored explicitly.  The
92)     service name and constraints themselves also generally need not be
93)     kept secret, depending on the scheme.
94) 
95)     The currently implemented subcommands are "vault" (for the scheme
96)     used by vault) and "export" (for exporting foreign configuration
97)     data).  See the respective `--help` output for instructions.  If no
98)     subcommand is given, we default to "vault".
99) 
100)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
101)     in v1.0, the subcommand must be specified explicitly.\f
102) 
103)     This is a [`click`][CLICK]-powered command-line interface function,
104)     and not intended for programmatic use.  Call with arguments
105)     `['--help']` to see full documentation of the interface.  (See also
106)     [`click.testing.CliRunner`][] for controlled, programmatic
107)     invocation.)
108) 
109)     [CLICK]: https://click.palletsprojects.com/
110) 
111)     """  # noqa: D301
112)     if subcommand_args and subcommand_args[0] == 'export':
113)         return derivepassphrase_export.main(
114)             args=subcommand_args[1:],
115)             prog_name=f'{PROG_NAME} export',
116)             standalone_mode=False,
117)         )
118)     if not (subcommand_args and subcommand_args[0] == 'vault'):
119)         click.echo(
120)             (
121)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
122)                 f'required in v1.0. See --help for available subcommands.'
123)             ),
124)             err=True,
125)         )
126)         click.echo(
127)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
128)             err=True,
129)         )
130)     else:
131)         subcommand_args = subcommand_args[1:]
132)     return derivepassphrase_vault.main(
133)         args=subcommand_args,
134)         prog_name=f'{PROG_NAME} vault',
135)         standalone_mode=False,
136)     )
137) 
138) 
139) # Exporter
140) # ========
141) 
142) 
143) @click.command(
144)     context_settings={
145)         'help_option_names': ['-h', '--help'],
146)         'ignore_unknown_options': True,
147)         'allow_interspersed_args': False,
148)     }
149) )
150) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
151) @click.argument('subcommand_args', nargs=-1, type=click.UNPROCESSED)
152) def derivepassphrase_export(
153)     *,
154)     subcommand_args: list[str],
155) ) -> None:
156)     """Export a foreign configuration to standard output.
157) 
158)     Read a foreign system configuration, extract all information from
159)     it, and export the resulting configuration to standard output.
160) 
161)     The only available subcommand is "vault", which implements the
162)     vault-native configuration scheme.  If no subcommand is given, we
163)     default to "vault".
164) 
165)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
166)     in v1.0, the subcommand must be specified explicitly.\f
167) 
168)     This is a [`click`][CLICK]-powered command-line interface function,
169)     and not intended for programmatic use.  Call with arguments
170)     `['--help']` to see full documentation of the interface.  (See also
171)     [`click.testing.CliRunner`][] for controlled, programmatic
172)     invocation.)
173) 
174)     [CLICK]: https://click.palletsprojects.com/
175) 
176)     """  # noqa: D301
177)     if not (subcommand_args and subcommand_args[0] == 'vault'):
178)         click.echo(
179)             (
180)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
181)                 f'required in v1.0. See --help for available subcommands.'
182)             ),
183)             err=True,
184)         )
185)         click.echo(
186)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
187)             err=True,
188)         )
189)     else:
190)         subcommand_args = subcommand_args[1:]
191)     return derivepassphrase_export_vault.main(
192)         args=subcommand_args,
193)         prog_name=f'{PROG_NAME} export vault',
194)         standalone_mode=False,
195)     )
196) 
197) 
198) def _load_data(
199)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
200)     path: str | bytes | os.PathLike[str],
201)     key: bytes,
202) ) -> Any:  # noqa: ANN401
203)     contents: bytes
204)     module: types.ModuleType
205)     match fmt:
206)         case 'v0.2':
207)             module = importlib.import_module(
208)                 'derivepassphrase.exporter.vault_native'
209)             )
210)             if module.STUBBED:
211)                 raise ModuleNotFoundError
212)             with open(path, 'rb') as infile:
213)                 contents = base64.standard_b64decode(infile.read())
214)             return module.export_vault_native_data(
215)                 contents, key, try_formats=['v0.2']
216)             )
217)         case 'v0.3':
218)             module = importlib.import_module(
219)                 'derivepassphrase.exporter.vault_native'
220)             )
221)             if module.STUBBED:
222)                 raise ModuleNotFoundError
223)             with open(path, 'rb') as infile:
224)                 contents = base64.standard_b64decode(infile.read())
225)             return module.export_vault_native_data(
226)                 contents, key, try_formats=['v0.3']
227)             )
228)         case 'storeroom':
229)             module = importlib.import_module(
230)                 'derivepassphrase.exporter.storeroom'
231)             )
232)             if module.STUBBED:
233)                 raise ModuleNotFoundError
234)             return module.export_storeroom_data(path, key)
235)         case _:  # pragma: no cover
236)             assert_never(fmt)
237) 
238) 
239) @click.command(
240)     context_settings={'help_option_names': ['-h', '--help']},
241) )
242) @click.option(
243)     '-f',
244)     '--format',
245)     'formats',
246)     metavar='FMT',
247)     multiple=True,
248)     default=('v0.3', 'v0.2', 'storeroom'),
249)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
250)     help='try the following storage formats, in order (default: v0.3, v0.2)',
251) )
252) @click.option(
253)     '-k',
254)     '--key',
255)     metavar='K',
256)     help=(
257)         'use K as the storage master key '
258)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
259)         '`USERNAME` environment variables)'
260)     ),
261) )
262) @click.argument('path', metavar='PATH', required=True)
263) @click.pass_context
264) def derivepassphrase_export_vault(
265)     ctx: click.Context,
266)     /,
267)     *,
268)     path: str | bytes | os.PathLike[str],
269)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
270)     key: str | bytes | None = None,
271) ) -> None:
272)     """Export a vault-native configuration to standard output.
273) 
274)     Read the vault-native configuration at PATH, extract all information
275)     from it, and export the resulting configuration to standard output.
276)     Depending on the configuration format, PATH may either be a file or
277)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
278)     formats.
279) 
280)     If PATH is explicitly given as `VAULT_PATH`, then use the
281)     `VAULT_PATH` environment variable to determine the correct path.
282)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
283)     named `VAULT_PATH`.)
284) 
285)     """
286)     logging.basicConfig()
287)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
288)         path = exporter.get_vault_path()
289)     if key is None:
290)         key = exporter.get_vault_key()
291)     elif isinstance(key, str):  # pragma: no branch
292)         key = key.encode('utf-8')
293)     for fmt in formats:
294)         try:
295)             config = _load_data(fmt, path, key)
296)         except (
297)             IsADirectoryError,
298)             NotADirectoryError,
299)             ValueError,
300)             RuntimeError,
301)         ):
302)             logging.info('Cannot load as %s: %s', fmt, path)
303)             continue
304)         except OSError as exc:
305)             click.echo(
306)                 (
307)                     f'{PROG_NAME}: ERROR: Cannot parse {path!r} as '
308)                     f'a valid config: {exc.strerror}: {exc.filename!r}'
309)                 ),
310)                 err=True,
311)             )
312)             ctx.exit(1)
313)         except ModuleNotFoundError:
314)             # TODO(the-13th-letter): Use backslash continuation.
315)             # https://github.com/nedbat/coveragepy/issues/1836
316)             msg = f"""
317) {PROG_NAME}: ERROR: Cannot load the required Python module "cryptography".
318) {PROG_NAME}: INFO: pip users: see the "export" extra.
319) """.lstrip('\n')
320)             click.echo(msg, nl=False, err=True)
321)             ctx.exit(1)
322)         else:
323)             if not _types.is_vault_config(config):
324)                 click.echo(
325)                     f'{PROG_NAME}: ERROR: Invalid vault config: {config!r}',
326)                     err=True,
327)                 )
328)                 ctx.exit(1)
329)             click.echo(json.dumps(config, indent=2, sort_keys=True))
330)             break
331)     else:
332)         click.echo(
333)             f'{PROG_NAME}: ERROR: Cannot parse {path!r} as a valid config.',
334)             err=True,
335)         )
336)         ctx.exit(1)
337) 
338) 
339) # Vault
340) # =====
341) 
342) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

343) def _config_filename() -> str | bytes | pathlib.Path:
344)     """Return the filename of the configuration file.
345) 
346)     The file is currently named `settings.json`, located within the
347)     configuration directory as determined by the `DERIVEPASSPHRASE_PATH`
348)     environment variable, or by [`click.get_app_dir`][] in POSIX
349)     mode.
350) 
351)     """
352)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

353)     path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
354)         PROG_NAME, force_posix=True
355)     )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

356)     return os.path.join(path, 'settings.json')
357) 
358) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

359) def _load_config() -> _types.VaultConfig:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

360)     """Load a vault(1)-compatible config from the application directory.
361) 
362)     The filename is obtained via
363)     [`derivepassphrase.cli._config_filename`][].  This must be an
364)     unencrypted JSON file.
365) 
366)     Returns:
367)         The vault settings.  See
368)         [`derivepassphrase.types.VaultConfig`][] for details.
369) 
370)     Raises:
371)         OSError:
372)             There was an OS error accessing the file.
373)         ValueError:
374)             The data loaded from the file is not a vault(1)-compatible
375)             config.
376) 
377)     """
378)     filename = _config_filename()
379)     with open(filename, 'rb') as fileobj:
380)         data = json.load(fileobj)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

381)     if not _types.is_vault_config(data):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

382)         raise ValueError(_INVALID_VAULT_CONFIG)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

383)     return data
384) 
385) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

386) def _save_config(config: _types.VaultConfig, /) -> None:
Marco Ricci Create the configuration di...

Marco Ricci authored 3 months ago

387)     """Save a vault(1)-compatible config to the application directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

388) 
389)     The filename is obtained via
390)     [`derivepassphrase.cli._config_filename`][].  The config will be
391)     stored as an unencrypted JSON file.
392) 
393)     Args:
394)         config:
395)             vault configuration to save.
396) 
397)     Raises:
398)         OSError:
399)             There was an OS error accessing or writing the file.
400)         ValueError:
401)             The data cannot be stored as a vault(1)-compatible config.
402) 
403)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

404)     if not _types.is_vault_config(config):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

405)         raise ValueError(_INVALID_VAULT_CONFIG)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

406)     filename = _config_filename()
Marco Ricci Create the configuration di...

Marco Ricci authored 3 months ago

407)     filedir = os.path.dirname(os.path.abspath(filename))
408)     try:
409)         os.makedirs(filedir, exist_ok=False)
410)     except FileExistsError:
411)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

412)             raise  # noqa: DOC501
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

413)     with open(filename, 'w', encoding='UTF-8') as fileobj:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

414)         json.dump(config, fileobj)
415) 
416) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

417) def _get_suitable_ssh_keys(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

418)     conn: ssh_agent.SSHAgentClient | socket.socket | None = None, /
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

419) ) -> Iterator[_types.KeyCommentPair]:
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

420)     """Yield all SSH keys suitable for passphrase derivation.
421) 
422)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

423)     [`ssh_agent.SSHAgentClient.list_keys`][]).
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

424) 
425)     Args:
426)         conn:
427)             An optional connection hint to the SSH agent; specifically,
428)             an SSH agent client, or a socket connected to an SSH agent.
429) 
430)             If an existing SSH agent client, then this client will be
431)             queried for the SSH keys, and otherwise left intact.
432) 
433)             If a socket, then a one-shot client will be constructed
434)             based on the socket to query the agent, and deconstructed
435)             afterwards.
436) 
437)             If neither are given, then the agent's socket location is
438)             looked up in the `SSH_AUTH_SOCK` environment variable, and
439)             used to construct/deconstruct a one-shot client, as in the
440)             previous case.
441) 
442)     Yields:
443)         :
444)             Every SSH key from the SSH agent that is suitable for
445)             passphrase derivation.
446) 
447)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

448)         KeyError:
449)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
450)             variable was not found.
451)         OSError:
452)             `conn` was a socket or `None`, and there was an error
453)             setting up a socket connection to the agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 4 months ago

454)         LookupError:
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

455)             No keys usable for passphrase derivation are loaded into the
456)             SSH agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 4 months ago

457)         RuntimeError:
458)             There was an error communicating with the SSH agent.
Marco Ricci Add a specific error class...

Marco Ricci authored 2 months ago

459)         SSHAgentFailedError:
460)             The agent failed to supply a list of loaded keys.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

461) 
462)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

463)     client: ssh_agent.SSHAgentClient
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

464)     client_context: contextlib.AbstractContextManager[Any]
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

465)     match conn:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

466)         case ssh_agent.SSHAgentClient():
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

467)             client = conn
468)             client_context = contextlib.nullcontext()
469)         case socket.socket() | None:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

470)             client = ssh_agent.SSHAgentClient(socket=conn)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

471)             client_context = client
472)         case _:  # pragma: no cover
473)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

474)             msg = f'invalid connection hint: {conn!r}'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

475)             raise TypeError(msg)  # noqa: DOC501
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

476)     with client_context:
477)         try:
478)             all_key_comment_pairs = list(client.list_keys())
479)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

480)             raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from e
481)     suitable_keys = copy.copy(all_key_comment_pairs)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

482)     for pair in all_key_comment_pairs:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

483)         key, _comment = pair
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

484)         if vault.Vault._is_suitable_ssh_key(key):  # noqa: SLF001
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

485)             yield pair
486)     if not suitable_keys:  # pragma: no cover
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

487)         raise LookupError(_NO_USABLE_KEYS)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

488) 
489) 
490) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

491)     items: Sequence[str | bytes],
492)     heading: str = 'Possible choices:',
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

493)     single_choice_prompt: str = 'Confirm this choice?',
494) ) -> int:
495)     """Prompt user for a choice among the given items.
496) 
497)     Print the heading, if any, then present the items to the user.  If
498)     there are multiple items, prompt the user for a selection, validate
499)     the choice, then return the list index of the selected item.  If
500)     there is only a single item, request confirmation for that item
501)     instead, and return the correct index.
502) 
503)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

504)         items:
505)             The list of items to choose from.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

506)         heading:
507)             A heading for the list of items, to print immediately
508)             before.  Defaults to a reasonable standard heading.  If
509)             explicitly empty, print no heading.
510)         single_choice_prompt:
511)             The confirmation prompt if there is only a single possible
512)             choice.  Defaults to a reasonable standard prompt.
513) 
514)     Returns:
515)         An index into the items sequence, indicating the user's
516)         selection.
517) 
518)     Raises:
519)         IndexError:
520)             The user made an invalid or empty selection, or requested an
521)             abort.
522) 
523)     """
524)     n = len(items)
525)     if heading:
526)         click.echo(click.style(heading, bold=True))
527)     for i, x in enumerate(items, start=1):
528)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
529)         click.echo(' ', nl=False)
530)         click.echo(x)
531)     if n > 1:
532)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
533)         choice = click.prompt(
534)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

535)             err=True,
536)             type=choices,
537)             show_choices=False,
538)             show_default=False,
539)             default='',
540)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

541)         if not choice:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

542)             raise IndexError(_EMPTY_SELECTION)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

543)         return int(choice) - 1
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

544)     prompt_suffix = (
545)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
546)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

547)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

548)         click.confirm(
549)             single_choice_prompt,
550)             prompt_suffix=prompt_suffix,
551)             err=True,
552)             abort=True,
553)             default=False,
554)             show_default=False,
555)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

556)     except click.Abort:
557)         raise IndexError(_EMPTY_SELECTION) from None
558)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

559) 
560) 
561) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

562)     conn: ssh_agent.SSHAgentClient | socket.socket | None = None, /
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

563) ) -> bytes | bytearray:
564)     """Interactively select an SSH key for passphrase derivation.
565) 
566)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

567)     [`ssh_agent.SSHAgentClient.list_keys`][]), then the user is
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

568)     prompted interactively (see [`click.prompt`][]) for a selection.
569) 
570)     Args:
571)         conn:
572)             An optional connection hint to the SSH agent; specifically,
573)             an SSH agent client, or a socket connected to an SSH agent.
574) 
575)             If an existing SSH agent client, then this client will be
576)             queried for the SSH keys, and otherwise left intact.
577) 
578)             If a socket, then a one-shot client will be constructed
579)             based on the socket to query the agent, and deconstructed
580)             afterwards.
581) 
582)             If neither are given, then the agent's socket location is
583)             looked up in the `SSH_AUTH_SOCK` environment variable, and
584)             used to construct/deconstruct a one-shot client, as in the
585)             previous case.
586) 
587)     Returns:
588)         The selected SSH key.
589) 
590)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

591)         KeyError:
592)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
593)             variable was not found.
594)         OSError:
595)             `conn` was a socket or `None`, and there was an error
596)             setting up a socket connection to the agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

597)         IndexError:
598)             The user made an invalid or empty selection, or requested an
599)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 4 months ago

600)         LookupError:
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

601)             No keys usable for passphrase derivation are loaded into the
602)             SSH agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 4 months ago

603)         RuntimeError:
604)             There was an error communicating with the SSH agent.
Marco Ricci Add a specific error class...

Marco Ricci authored 2 months ago

605)         SSHAgentFailedError:
606)             The agent failed to supply a list of loaded keys.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

607)     """
608)     suitable_keys = list(_get_suitable_ssh_keys(conn))
609)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

610)     unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

611)     for key, comment in suitable_keys:
612)         keytype = unstring_prefix(key)[0].decode('ASCII')
613)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

614)         key_prefix = (
615)             key_str
616)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
617)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
618)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

619)         comment_str = comment.decode('UTF-8', errors='replace')
620)         key_listing.append(f'{keytype} {key_prefix} {comment_str}')
621)     choice = _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

622)         key_listing,
623)         heading='Suitable SSH keys:',
624)         single_choice_prompt='Use this key?',
625)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

626)     return suitable_keys[choice].key
627) 
628) 
629) def _prompt_for_passphrase() -> str:
630)     """Interactively prompt for the passphrase.
631) 
632)     Calls [`click.prompt`][] internally.  Moved into a separate function
633)     mainly for testing/mocking purposes.
634) 
635)     Returns:
636)         The user input.
637) 
638)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

639)     return cast(
640)         str,
641)         click.prompt(
642)             'Passphrase',
643)             default='',
644)             hide_input=True,
645)             show_default=False,
646)             err=True,
647)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

648)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

649) 
650) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

651) def _check_for_misleading_passphrase(
652)     key: tuple[str, ...],
653)     value: dict[str, Any],
654)     *,
655)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
656) ) -> None:
657)     def is_json_identifier(x: str) -> bool:
658)         return not x.startswith(tuple('0123456789')) and not any(
659)             c.lower() not in set('0123456789abcdefghijklmnopqrstuvwxyz_')
660)             for c in x
661)         )
662) 
663)     if 'phrase' in value:
664)         phrase = value['phrase']
665)         if not unicodedata.is_normalized(form, phrase):
666)             key_path = '.'.join(
667)                 x if is_json_identifier(x) else repr(x) for x in key
668)             )
669)             click.echo(
670)                 (
671)                     f'{PROG_NAME}: Warning: the {key_path} passphrase '
672)                     f'is not {form}-normalized. Make sure to double-check '
673)                     f'this is really the passphrase you want.'
674)                 ),
675)                 err=True,
676)             )
677) 
678) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

679) class OptionGroupOption(click.Option):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

680)     """A [`click.Option`][] with an associated group name and group epilog.
681) 
682)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
683)     help sections.  Each subclass contains its own group name and
684)     epilog.
685) 
686)     Attributes:
687)         option_group_name:
688)             The name of the option group.  Used as a heading on the help
689)             text for options in this section.
690)         epilog:
691)             An epilog to print after listing the options in this
692)             section.
693) 
694)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

695) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

696)     option_group_name: str = ''
697)     epilog: str = ''
698) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

699)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

700)         if self.__class__ == __class__:  # type: ignore[name-defined]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

701)             raise NotImplementedError
702)         super().__init__(*args, **kwargs)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

703) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

704) 
705) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

706)     """A [`click.Command`][] with support for help/option groups.
707) 
708)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
709)     further modified to support group epilogs.
710) 
711)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
712) 
713)     """
714) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

715)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

716)         self,
717)         ctx: click.Context,
718)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

719)     ) -> None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

720)         r"""Format options on the help listing, grouped into sections.
721) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 4 months ago

722)         This is a callback for [`click.Command.get_help`][] that
723)         implements the `--help` listing, by calling appropriate methods
724)         of the `formatter`.  We list all options (like the base
725)         implementation), but grouped into sections according to the
726)         concrete [`click.Option`][] subclass being used.  If the option
727)         is an instance of some subclass `X` of
728)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
729)         heading and the epilog are taken from `X.option_group_name` and
730)         `X.epilog`; otherwise, the section heading is "Options" (or
731)         "Other options" if there are other option groups) and the epilog
732)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

733) 
734)         Args:
735)             ctx:
736)                 The click context.
737)             formatter:
738)                 The formatter for the `--help` listing.
739) 
740)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

741)         help_records: dict[str, list[tuple[str, str]]] = {}
742)         epilogs: dict[str, str] = {}
743)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

744)         if (  # pragma: no branch
745)             (help_opt := self.get_help_option(ctx)) is not None
746)             and help_opt not in params
747)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

748)             params.append(help_opt)
749)         for param in params:
750)             rec = param.get_help_record(ctx)
751)             if rec is not None:
752)                 if isinstance(param, OptionGroupOption):
753)                     group_name = param.option_group_name
754)                     epilogs.setdefault(group_name, param.epilog)
755)                 else:
756)                     group_name = ''
757)                 help_records.setdefault(group_name, []).append(rec)
758)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

759)         default_group_name = (
760)             'Other Options' if len(default_group) > 1 else 'Options'
761)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

762)         help_records[default_group_name] = default_group
763)         for group_name, records in help_records.items():
764)             with formatter.section(group_name):
765)                 formatter.write_dl(records)
766)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
767)             if epilog:
768)                 formatter.write_paragraph()
769)                 with formatter.indentation():
770)                     formatter.write_text(epilog)
771) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

772) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

773) # Concrete option groups used by this command-line interface.
774) class PasswordGenerationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

775)     """Password generation options for the CLI."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

776) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

777)     option_group_name = 'Password generation'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

778)     epilog = """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

779)         Use NUMBER=0, e.g. "--symbol 0", to exclude a character type
780)         from the output.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

781)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

782) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

783) 
784) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

785)     """Configuration options for the CLI."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

786) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

787)     option_group_name = 'Configuration'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

788)     epilog = """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

789)         Use $VISUAL or $EDITOR to configure the spawned editor.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

790)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

791) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

792) 
793) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

794)     """Storage management options for the CLI."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

795) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

796)     option_group_name = 'Storage management'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

797)     epilog = """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

798)         Using "-" as PATH for standard input/standard output is
799)         supported.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

800)     """
801) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

802) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

803) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

804)     ctx: click.Context,
805)     param: click.Parameter,
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

806)     value: Any,  # noqa: ANN401
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

807) ) -> int | None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

808)     """Check that the occurrence constraint is valid (int, 0 or larger).
809) 
810)     Args:
811)         ctx: The `click` context.
812)         param: The current command-line parameter.
813)         value: The parameter value to be checked.
814) 
815)     Returns:
816)         The parsed parameter value.
817) 
818)     Raises:
819)         click.BadParameter: The parameter value is invalid.
820) 
821)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

822)     del ctx  # Unused.
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

823)     del param  # Unused.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

824)     if value is None:
825)         return value
826)     if isinstance(value, int):
827)         int_value = value
828)     else:
829)         try:
830)             int_value = int(value, 10)
831)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

832)             msg = 'not an integer'
833)             raise click.BadParameter(msg) from e
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

834)     if int_value < 0:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

835)         msg = 'not a non-negative integer'
836)         raise click.BadParameter(msg)
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

837)     return int_value
838) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

839) 
840) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

841)     ctx: click.Context,
842)     param: click.Parameter,
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

843)     value: Any,  # noqa: ANN401
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

844) ) -> int | None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

845)     """Check that the length is valid (int, 1 or larger).
846) 
847)     Args:
848)         ctx: The `click` context.
849)         param: The current command-line parameter.
850)         value: The parameter value to be checked.
851) 
852)     Returns:
853)         The parsed parameter value.
854) 
855)     Raises:
856)         click.BadParameter: The parameter value is invalid.
857) 
858)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

859)     del ctx  # Unused.
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

860)     del param  # Unused.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

861)     if value is None:
862)         return value
863)     if isinstance(value, int):
864)         int_value = value
865)     else:
866)         try:
867)             int_value = int(value, 10)
868)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

869)             msg = 'not an integer'
870)             raise click.BadParameter(msg) from e
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

871)     if int_value < 1:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

872)         msg = 'not a positive integer'
873)         raise click.BadParameter(msg)
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

874)     return int_value
875) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

876) 
877) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

878) # Enter notes below the line with the cut mark (ASCII scissors and
879) # dashes).  Lines above the cut mark (such as this one) will be ignored.
880) #
881) # If you wish to clear the notes, leave everything beyond the cut mark
882) # blank.  However, if you leave the *entire* file blank, also removing
883) # the cut mark, then the edit is aborted, and the old notes contents are
884) # retained.
885) #
886) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

887) """
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

888) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
889) 
890) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

891) @click.command(
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

892)     # 'vault',
893)     # help="derivation scheme compatible with James Coglan's vault(1)",
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

894)     context_settings={'help_option_names': ['-h', '--help']},
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

895)     cls=CommandWithHelpGroups,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

896)     epilog=r"""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

897)         WARNING: There is NO WAY to retrieve the generated passphrases
898)         if the master passphrase, the SSH key, or the exact passphrase
899)         settings are lost, short of trying out all possible
900)         combinations.  You are STRONGLY advised to keep independent
901)         backups of the settings and the SSH key, if any.
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

902) 
903)         The configuration is NOT encrypted, and you are STRONGLY
904)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

905)     """,
906) )
907) @click.option(
908)     '-p',
909)     '--phrase',
910)     'use_phrase',
911)     is_flag=True,
912)     help='prompts you for your passphrase',
913)     cls=PasswordGenerationOption,
914) )
915) @click.option(
916)     '-k',
917)     '--key',
918)     'use_key',
919)     is_flag=True,
920)     help='uses your SSH private key to generate passwords',
921)     cls=PasswordGenerationOption,
922) )
923) @click.option(
924)     '-l',
925)     '--length',
926)     metavar='NUMBER',
927)     callback=_validate_length,
928)     help='emits password of length NUMBER',
929)     cls=PasswordGenerationOption,
930) )
931) @click.option(
932)     '-r',
933)     '--repeat',
934)     metavar='NUMBER',
935)     callback=_validate_occurrence_constraint,
936)     help='allows maximum of NUMBER repeated adjacent chars',
937)     cls=PasswordGenerationOption,
938) )
939) @click.option(
940)     '--lower',
941)     metavar='NUMBER',
942)     callback=_validate_occurrence_constraint,
943)     help='includes at least NUMBER lowercase letters',
944)     cls=PasswordGenerationOption,
945) )
946) @click.option(
947)     '--upper',
948)     metavar='NUMBER',
949)     callback=_validate_occurrence_constraint,
950)     help='includes at least NUMBER uppercase letters',
951)     cls=PasswordGenerationOption,
952) )
953) @click.option(
954)     '--number',
955)     metavar='NUMBER',
956)     callback=_validate_occurrence_constraint,
957)     help='includes at least NUMBER digits',
958)     cls=PasswordGenerationOption,
959) )
960) @click.option(
961)     '--space',
962)     metavar='NUMBER',
963)     callback=_validate_occurrence_constraint,
964)     help='includes at least NUMBER spaces',
965)     cls=PasswordGenerationOption,
966) )
967) @click.option(
968)     '--dash',
969)     metavar='NUMBER',
970)     callback=_validate_occurrence_constraint,
971)     help='includes at least NUMBER "-" or "_"',
972)     cls=PasswordGenerationOption,
973) )
974) @click.option(
975)     '--symbol',
976)     metavar='NUMBER',
977)     callback=_validate_occurrence_constraint,
978)     help='includes at least NUMBER symbol chars',
979)     cls=PasswordGenerationOption,
980) )
981) @click.option(
982)     '-n',
983)     '--notes',
984)     'edit_notes',
985)     is_flag=True,
986)     help='spawn an editor to edit notes for SERVICE',
987)     cls=ConfigurationOption,
988) )
989) @click.option(
990)     '-c',
991)     '--config',
992)     'store_config_only',
993)     is_flag=True,
994)     help='saves the given settings for SERVICE or global',
995)     cls=ConfigurationOption,
996) )
997) @click.option(
998)     '-x',
999)     '--delete',
1000)     'delete_service_settings',
1001)     is_flag=True,
1002)     help='deletes settings for SERVICE',
1003)     cls=ConfigurationOption,
1004) )
1005) @click.option(
1006)     '--delete-globals',
1007)     is_flag=True,
1008)     help='deletes the global shared settings',
1009)     cls=ConfigurationOption,
1010) )
1011) @click.option(
1012)     '-X',
1013)     '--clear',
1014)     'clear_all_settings',
1015)     is_flag=True,
1016)     help='deletes all settings',
1017)     cls=ConfigurationOption,
1018) )
1019) @click.option(
1020)     '-e',
1021)     '--export',
1022)     'export_settings',
1023)     metavar='PATH',
1024)     help='export all saved settings into file PATH',
1025)     cls=StorageManagementOption,
1026) )
1027) @click.option(
1028)     '-i',
1029)     '--import',
1030)     'import_settings',
1031)     metavar='PATH',
1032)     help='import saved settings from file PATH',
1033)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1034) )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1035) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1036) @click.argument('service', required=False)
1037) @click.pass_context
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

1038) def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1039)     ctx: click.Context,
1040)     /,
1041)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1042)     service: str | None = None,
1043)     use_phrase: bool = False,
1044)     use_key: bool = False,
1045)     length: int | None = None,
1046)     repeat: int | None = None,
1047)     lower: int | None = None,
1048)     upper: int | None = None,
1049)     number: int | None = None,
1050)     space: int | None = None,
1051)     dash: int | None = None,
1052)     symbol: int | None = None,
1053)     edit_notes: bool = False,
1054)     store_config_only: bool = False,
1055)     delete_service_settings: bool = False,
1056)     delete_globals: bool = False,
1057)     clear_all_settings: bool = False,
1058)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1059)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1060) ) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 2 months ago

1061)     """Derive a passphrase using the vault(1) derivation scheme.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1062) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 4 months ago

1063)     Using a master passphrase or a master SSH key, derive a passphrase
1064)     for SERVICE, subject to length, character and character repetition
1065)     constraints.  The derivation is cryptographically strong, meaning
1066)     that even if a single passphrase is compromised, guessing the master
1067)     passphrase or a different service's passphrase is computationally
1068)     infeasible.  The derivation is also deterministic, given the same
1069)     inputs, thus the resulting passphrase need not be stored explicitly.
1070)     The service name and constraints themselves also need not be kept
1071)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1072) 
1073)     If operating on global settings, or importing/exporting settings,
1074)     then SERVICE must be omitted.  Otherwise it is required.\f
1075) 
1076)     This is a [`click`][CLICK]-powered command-line interface function,
1077)     and not intended for programmatic use.  Call with arguments
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1078)     `['--help']` to see full documentation of the interface.  (See also
1079)     [`click.testing.CliRunner`][] for controlled, programmatic
1080)     invocation.)
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1081) 
1082)     [CLICK]: https://click.palletsprojects.com/
1083) 
1084)     Parameters:
1085)         ctx (click.Context):
1086)             The `click` context.
1087) 
1088)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1089)         service:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1090)             A service name.  Required, unless operating on global
1091)             settings or importing/exporting settings.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1092)         use_phrase:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1093)             Command-line argument `-p`/`--phrase`.  If given, query the
1094)             user for a passphrase instead of an SSH key.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1095)         use_key:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1096)             Command-line argument `-k`/`--key`.  If given, query the
1097)             user for an SSH key instead of a passphrase.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1098)         length:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1099)             Command-line argument `-l`/`--length`.  Override the default
1100)             length of the generated passphrase.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1101)         repeat:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1102)             Command-line argument `-r`/`--repeat`.  Override the default
1103)             repetition limit if positive, or disable the repetition
1104)             limit if 0.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1105)         lower:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1106)             Command-line argument `--lower`.  Require a given amount of
1107)             ASCII lowercase characters if positive, else forbid ASCII
1108)             lowercase characters if 0.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1109)         upper:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1110)             Command-line argument `--upper`.  Same as `lower`, but for
1111)             ASCII uppercase characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1112)         number:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1113)             Command-line argument `--number`.  Same as `lower`, but for
1114)             ASCII digits.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1115)         space:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1116)             Command-line argument `--space`.  Same as `lower`, but for
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1117)             the space character.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1118)         dash:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1119)             Command-line argument `--dash`.  Same as `lower`, but for
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1120)             the hyphen-minus and underscore characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1121)         symbol:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1122)             Command-line argument `--symbol`.  Same as `lower`, but for
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1123)             all other ASCII printable characters (except backquote).
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1124)         edit_notes:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1125)             Command-line argument `-n`/`--notes`.  If given, spawn an
1126)             editor to edit notes for `service`.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1127)         store_config_only:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1128)             Command-line argument `-c`/`--config`.  If given, saves the
1129)             other given settings (`--key`, ..., `--symbol`) to the
1130)             configuration file, either specifically for `service` or as
1131)             global settings.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1132)         delete_service_settings:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1133)             Command-line argument `-x`/`--delete`.  If given, removes
1134)             the settings for `service` from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1135)         delete_globals:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1136)             Command-line argument `--delete-globals`.  If given, removes
1137)             the global settings from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1138)         clear_all_settings:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1139)             Command-line argument `-X`/`--clear`.  If given, removes all
1140)             settings from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1141)         export_settings:
1142)             Command-line argument `-e`/`--export`.  If a file object,
1143)             then it must be open for writing and accept `str` inputs.
1144)             Otherwise, a filename to open for writing.  Using `-` for
1145)             standard output is supported.
1146)         import_settings:
1147)             Command-line argument `-i`/`--import`.  If a file object, it
1148)             must be open for reading and yield `str` values.  Otherwise,
1149)             a filename to open for reading.  Using `-` for standard
1150)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1151) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1152)     """  # noqa: D301
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1153)     options_in_group: dict[type[click.Option], list[click.Option]] = {}
1154)     params_by_str: dict[str, click.Parameter] = {}
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1155)     for param in ctx.command.params:
1156)         if isinstance(param, click.Option):
1157)             group: type[click.Option]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1158)             match param:
1159)                 case PasswordGenerationOption():
1160)                     group = PasswordGenerationOption
1161)                 case ConfigurationOption():
1162)                     group = ConfigurationOption
1163)                 case StorageManagementOption():
1164)                     group = StorageManagementOption
1165)                 case OptionGroupOption():
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1166)                     raise AssertionError(  # noqa: DOC501,TRY003
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1167)                         f'Unknown option group for {param!r}'  # noqa: EM102
1168)                     )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1169)                 case _:
1170)                     group = click.Option
1171)             options_in_group.setdefault(group, []).append(param)
1172)         params_by_str[param.human_readable_name] = param
1173)         for name in param.opts + param.secondary_opts:
1174)             params_by_str[name] = param
1175) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 3 months ago

1176)     def is_param_set(param: click.Parameter) -> bool:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1177)         return bool(ctx.params.get(param.human_readable_name))
1178) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1179)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1180)         param: click.Parameter | str,
1181)         *incompatible: click.Parameter | str,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1182)     ) -> None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1183)         if isinstance(param, str):
1184)             param = params_by_str[param]
1185)         assert isinstance(param, click.Parameter)
1186)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1187)             return
1188)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1189)             if isinstance(other, str):
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1190)                 other = params_by_str[other]  # noqa: PLW2901
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1191)             assert isinstance(other, click.Parameter)
1192)             if other != param and is_param_set(other):
1193)                 opt_str = param.opts[0]
1194)                 other_str = other.opts[0]
1195)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1196)                     opt_str, f'mutually exclusive with {other_str}', ctx=ctx
1197)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1198) 
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1199)     def err(msg: str) -> NoReturn:
1200)         click.echo(f'{PROG_NAME}: {msg}', err=True)
1201)         ctx.exit(1)
1202) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

1203)     def get_config() -> _types.VaultConfig:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1204)         try:
1205)             return _load_config()
1206)         except FileNotFoundError:
1207)             return {'services': {}}
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

1208)         except OSError as e:
1209)             err(f'Cannot load config: {e.strerror}: {e.filename!r}')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1210)         except Exception as e:  # noqa: BLE001
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1211)             err(f'Cannot load config: {e}')
1212) 
1213)     def put_config(config: _types.VaultConfig, /) -> None:
1214)         try:
1215)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

1216)         except OSError as exc:
1217)             err(f'Cannot store config: {exc.strerror}: {exc.filename!r}')
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1218)         except Exception as exc:  # noqa: BLE001
1219)             err(f'Cannot store config: {exc}')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1220) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

1221)     configuration: _types.VaultConfig
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1222) 
1223)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1224)     for group in (ConfigurationOption, StorageManagementOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1225)         for opt in options_in_group[group]:
1226)             if opt != params_by_str['--config']:
1227)                 check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1228)                     opt, *options_in_group[PasswordGenerationOption]
1229)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1230) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1231)     for group in (ConfigurationOption, StorageManagementOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1232)         for opt in options_in_group[group]:
1233)             check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1234)                 opt,
1235)                 *options_in_group[ConfigurationOption],
1236)                 *options_in_group[StorageManagementOption],
1237)             )
1238)     sv_options = options_in_group[PasswordGenerationOption] + [
1239)         params_by_str['--notes'],
1240)         params_by_str['--delete'],
1241)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1242)     sv_options.remove(params_by_str['--key'])
1243)     sv_options.remove(params_by_str['--phrase'])
1244)     for param in sv_options:
1245)         if is_param_set(param) and not service:
1246)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1247)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1248)             raise click.UsageError(msg)  # noqa: DOC501
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1249)     for param in [params_by_str['--key'], params_by_str['--phrase']]:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1250)         if is_param_set(param) and not (
1251)             service or is_param_set(params_by_str['--config'])
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1252)         ):
1253)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1254)             msg = f'{opt_str} requires a SERVICE or --config'
1255)             raise click.UsageError(msg)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1256)     no_sv_options = [
1257)         params_by_str['--delete-globals'],
1258)         params_by_str['--clear'],
1259)         *options_in_group[StorageManagementOption],
1260)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

1261)     for param in no_sv_options:
1262)         if is_param_set(param) and service:
1263)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1264)             msg = f'{opt_str} does not take a SERVICE argument'
1265)             raise click.UsageError(msg)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1266) 
1267)     if edit_notes:
1268)         assert service is not None
1269)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1270)         text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get(
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

1271)             service, cast(_types.VaultConfigServicesSettings, {})
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1272)         ).get('notes', '')
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1273)         notes_value = click.edit(text=text)
1274)         if notes_value is not None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1275)             notes_lines = collections.deque(notes_value.splitlines(True))  # noqa: FBT003
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1276)             while notes_lines:
1277)                 line = notes_lines.popleft()
1278)                 if line.startswith(DEFAULT_NOTES_MARKER):
1279)                     notes_value = ''.join(notes_lines)
1280)                     break
1281)             else:
1282)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1283)                     err('Not saving new notes: user aborted request')
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1284)             configuration['services'].setdefault(service, {})['notes'] = (
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1285)                 notes_value.strip('\n')
1286)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1287)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1288)     elif delete_service_settings:
1289)         assert service is not None
1290)         configuration = get_config()
1291)         if service in configuration['services']:
1292)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1293)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1294)     elif delete_globals:
1295)         configuration = get_config()
1296)         if 'global' in configuration:
1297)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1298)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1299)     elif clear_all_settings:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1300)         put_config({'services': {}})
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1301)     elif import_settings:
1302)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1303)             # TODO(the-13th-letter): keep track of auto-close; try
1304)             # os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1305)             infile = (
1306)                 cast(TextIO, import_settings)
1307)                 if hasattr(import_settings, 'close')
1308)                 else click.open_file(os.fspath(import_settings), 'rt')
1309)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1310)             with infile:
1311)                 maybe_config = json.load(infile)
1312)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1313)             err(f'Cannot load config: cannot decode JSON: {e}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1314)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1315)             err(f'Cannot load config: {e.strerror}: {e.filename!r}')
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

1316)         if _types.is_vault_config(maybe_config):
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

1317)             form = cast(
1318)                 Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1319)                 maybe_config.get('global', {}).get(
1320)                     'unicode_normalization_form', 'NFC'
1321)                 ),
1322)             )
1323)             assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1324)             _check_for_misleading_passphrase(
1325)                 ('global',),
1326)                 cast(dict[str, Any], maybe_config.get('global', {})),
1327)                 form=form,
1328)             )
1329)             for key, value in maybe_config['services'].items():
1330)                 _check_for_misleading_passphrase(
1331)                     ('services', key),
1332)                     cast(dict[str, Any], value),
1333)                     form=form,
1334)                 )
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1335)             put_config(maybe_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1336)         else:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1337)             err(f'Cannot load config: {_INVALID_VAULT_CONFIG}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1338)     elif export_settings:
1339)         configuration = get_config()
1340)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 months ago

1341)             # TODO(the-13th-letter): keep track of auto-close; try
1342)             # os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1343)             outfile = (
1344)                 cast(TextIO, export_settings)
1345)                 if hasattr(export_settings, 'close')
1346)                 else click.open_file(os.fspath(export_settings), 'wt')
1347)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1348)             with outfile:
1349)                 json.dump(configuration, outfile)
1350)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1351)             err(f'Cannot store config: {e.strerror}: {e.filename!r}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1352)     else:
1353)         configuration = get_config()
1354)         # This block could be type checked more stringently, but this
1355)         # would probably involve a lot of code repetition.  Since we
1356)         # have a type guarding function anyway, assert that we didn't
1357)         # make any mistakes at the end instead.
1358)         global_keys = {'key', 'phrase'}
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1359)         service_keys = {
1360)             'key',
1361)             'phrase',
1362)             'length',
1363)             'repeat',
1364)             'lower',
1365)             'upper',
1366)             'number',
1367)             'space',
1368)             'dash',
1369)             'symbol',
1370)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1371)         settings: collections.ChainMap[str, Any] = collections.ChainMap(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1372)             {
1373)                 k: v
1374)                 for k, v in locals().items()
1375)                 if k in service_keys and v is not None
1376)             },
1377)             cast(
1378)                 dict[str, Any],
1379)                 configuration['services'].get(service or '', {}),
1380)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1381)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1382)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1383)         )
1384)         if use_key:
1385)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1386)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1387)                     'ASCII'
1388)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1389)             except IndexError:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1390)                 err('No valid SSH key selected')
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

1391)             except KeyError:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1392)                 err('Cannot find running SSH agent; check SSH_AUTH_SOCK')
Marco Ricci Document and handle other e...

Marco Ricci authored 2 months ago

1393)             except OSError as e:
1394)                 err(
1395)                     f'Cannot connect to SSH agent: {e.strerror}: '
1396)                     f'{e.filename!r}'
1397)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 2 months ago

1398)             except (
1399)                 LookupError,
1400)                 RuntimeError,
1401)                 ssh_agent.SSHAgentFailedError,
1402)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 2 months ago

1403)                 err(str(e))
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1404)         elif use_phrase:
1405)             maybe_phrase = _prompt_for_passphrase()
1406)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1407)                 err('No passphrase given')
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1408)             else:
1409)                 phrase = maybe_phrase
1410)         if store_config_only:
1411)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1412)             view = (
1413)                 collections.ChainMap(*settings.maps[:2])
1414)                 if service
1415)                 else settings.parents.parents
1416)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1417)             if use_key:
1418)                 view['key'] = key
1419)                 for m in view.maps:
1420)                     m.pop('phrase', '')
1421)             elif use_phrase:
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

1422)                 _check_for_misleading_passphrase(
1423)                     ('services', service) if service else ('global',),
1424)                     {'phrase': phrase},
1425)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1426)                 view['phrase'] = phrase
1427)                 for m in view.maps:
1428)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1429)             if not view.maps[0]:
1430)                 settings_type = 'service' if service else 'global'
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1431)                 msg = (
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1432)                     f'Cannot update {settings_type} settings without '
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1433)                     f'actual settings'
1434)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1435)                 raise click.UsageError(msg)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1436)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1437)                 configuration['services'].setdefault(service, {}).update(view)  # type: ignore[typeddict-item]
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1438)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1439)                 configuration.setdefault('global', {}).update(view)  # type: ignore[typeddict-item]
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 3 months ago

1440)             assert _types.is_vault_config(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1441)                 configuration
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1442)             ), f'Invalid vault configuration: {configuration!r}'
Marco Ricci Fix error bubbling in outda...

Marco Ricci authored 2 months ago

1443)             put_config(configuration)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1444)         else:
1445)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1446)                 msg = 'SERVICE is required'
1447)                 raise click.UsageError(msg)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1448)             kwargs: dict[str, Any] = {
1449)                 k: v
1450)                 for k, v in settings.items()
1451)                 if k in service_keys and v is not None
1452)             }
1453) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 3 months ago

1454)             def key_to_phrase(
1455)                 key: str | bytes | bytearray,
1456)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

1457)                 return vault.Vault.phrase_from_key(
Marco Ricci Shift misplaced local function

Marco Ricci authored 3 months ago

1458)                     base64.standard_b64decode(key)
1459)                 )
1460) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 months ago

1461)             if use_phrase:
1462)                 form = cast(
1463)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1464)                     configuration.get('global', {}).get(
1465)                         'unicode_normalization_form', 'NFC'
1466)                     ),
1467)                 )
1468)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1469)                 _check_for_misleading_passphrase(
1470)                     ('interactive',), {'phrase': phrase}, form=form
1471)                 )
1472) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1473)             # If either --key or --phrase are given, use that setting.
1474)             # Otherwise, if both key and phrase are set in the config,
1475)             # one must be global (ignore it) and one must be
1476)             # service-specific (use that one). Otherwise, if only one of
1477)             # key and phrase is set in the config, use that one.  In all
1478)             # these above cases, set the phrase via
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

1479)             # derivepassphrase.vault.Vault.phrase_from_key if a key is
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1480)             # given. Finally, if nothing is set, error out.
1481)             if use_key or use_phrase:
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 3 months ago

1482)                 kwargs['phrase'] = key_to_phrase(key) if use_key else phrase
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1483)             elif kwargs.get('phrase') and kwargs.get('key'):
1484)                 if any('key' in m for m in settings.maps[:2]):
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 3 months ago

1485)                     kwargs['phrase'] = key_to_phrase(kwargs['key'])
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1486)             elif kwargs.get('key'):
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 3 months ago

1487)                 kwargs['phrase'] = key_to_phrase(kwargs['key'])
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1488)             elif kwargs.get('phrase'):
1489)                 pass
1490)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1491)                 msg = (
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 2 months ago

1492)                     'No passphrase or key given on command-line '
Marco Ricci Reformat everything with ruff

Marco Ricci authored 3 months ago

1493)                     'or in configuration'
1494)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 3 months ago

1495)                 raise click.UsageError(msg)
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 3 months ago

1496)             kwargs.pop('key', '')
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 3 months ago

1497)             result = vault.Vault(**kwargs).generate(service)
Marco Ricci Add finished command-line i...

Marco Ricci authored 4 months ago

1498)             click.echo(result.decode('ASCII'))