4ea247a8c0739e114d44fae84f6baa09ad4ebb5c
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 1 week ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 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 1 month 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 2 months ago

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

Marco Ricci authored 1 week 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.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 1 week ago

75)     """,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 1 week ago

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 Rename the configuration fi...

Marco Ricci authored 1 week ago

343) def _config_filename(
344)     subsystem: str | None = 'settings',
345) ) -> str | bytes | pathlib.Path:
346)     """Return the filename of the configuration file for the subsystem.
347) 
348)     The (implicit default) file is currently named `settings.json`,
349)     located within the configuration directory as determined by the
350)     `DERIVEPASSPHRASE_PATH` environment variable, or by
351)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
352)     subsystem, this will usually be a different file within that
353)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

354) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 1 week ago

355)     Args:
356)         subsystem:
357)             Name of the configuration subsystem whose configuration
358)             filename to return.  If not given, return the old filename
359)             from before the subcommand migration.  If `None`, return the
360)             configuration directory instead.
361) 
362)     Raises:
363)         AssertionError:
364)             An unknown subsystem was passed.
365) 
366)     Deprecated:
367)         Since v0.2.0: The implicit default subsystem and the old
368)         configuration filename are deprecated, and will be removed in v1.0.
369)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

370) 
371)     """
372)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

373)     path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
374)         PROG_NAME, force_posix=True
375)     )
Marco Ricci Rename the configuration fi...

Marco Ricci authored 1 week ago

376)     match subsystem:
377)         case None:
378)             return path
379)         case 'vault' | 'settings':
380)             filename = f'{subsystem}.json'
381)         case _:  # pragma: no cover
382)             msg = f'Unknown configuration subsystem: {subsystem!r}'
383)             raise AssertionError(msg)
384)     return os.path.join(path, filename)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

385) 
386) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

388)     """Load a vault(1)-compatible config from the application directory.
389) 
390)     The filename is obtained via
391)     [`derivepassphrase.cli._config_filename`][].  This must be an
392)     unencrypted JSON file.
393) 
394)     Returns:
395)         The vault settings.  See
396)         [`derivepassphrase.types.VaultConfig`][] for details.
397) 
398)     Raises:
399)         OSError:
400)             There was an OS error accessing the file.
401)         ValueError:
402)             The data loaded from the file is not a vault(1)-compatible
403)             config.
404) 
405)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 1 week ago

406)     filename = _config_filename(subsystem='vault')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

407)     with open(filename, 'rb') as fileobj:
408)         data = json.load(fileobj)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

411)     return data
412) 
413) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 1 week ago

414) def _migrate_and_load_old_config() -> (
415)     tuple[_types.VaultConfig, OSError | None]
416) ):
417)     """Load and migrate a vault(1)-compatible config.
418) 
419)     The (old) filename is obtained via
420)     [`derivepassphrase.cli._config_filename`][].  This must be an
421)     unencrypted JSON file.  After loading, the file is migrated to the new
422)     standard filename.
423) 
424)     Returns:
425)         The vault settings, and an optional exception encountered during
426)         migration.  See [`derivepassphrase.types.VaultConfig`][] for
427)         details on the former.
428) 
429)     Raises:
430)         OSError:
431)             There was an OS error accessing the old file.
432)         ValueError:
433)             The data loaded from the file is not a vault(1)-compatible
434)             config.
435) 
436)     """
437)     new_filename = _config_filename(subsystem='vault')
438)     old_filename = _config_filename()
439)     with open(old_filename, 'rb') as fileobj:
440)         data = json.load(fileobj)
441)     if not _types.is_vault_config(data):
442)         raise ValueError(_INVALID_VAULT_CONFIG)
443)     try:
444)         os.replace(old_filename, new_filename)
445)     except OSError as exc:
446)         return data, exc
447)     else:
448)         return data, None
449) 
450) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

453) 
454)     The filename is obtained via
455)     [`derivepassphrase.cli._config_filename`][].  The config will be
456)     stored as an unencrypted JSON file.
457) 
458)     Args:
459)         config:
460)             vault configuration to save.
461) 
462)     Raises:
463)         OSError:
464)             There was an OS error accessing or writing the file.
465)         ValueError:
466)             The data cannot be stored as a vault(1)-compatible config.
467) 
468)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

470)         raise ValueError(_INVALID_VAULT_CONFIG)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 1 week ago

471)     filename = _config_filename(subsystem='vault')
Marco Ricci Create the configuration di...

Marco Ricci authored 1 month ago

472)     filedir = os.path.dirname(os.path.abspath(filename))
473)     try:
474)         os.makedirs(filedir, exist_ok=False)
475)     except FileExistsError:
476)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

479)         json.dump(config, fileobj)
480) 
481) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

489) 
490)     Args:
491)         conn:
492)             An optional connection hint to the SSH agent; specifically,
493)             an SSH agent client, or a socket connected to an SSH agent.
494) 
495)             If an existing SSH agent client, then this client will be
496)             queried for the SSH keys, and otherwise left intact.
497) 
498)             If a socket, then a one-shot client will be constructed
499)             based on the socket to query the agent, and deconstructed
500)             afterwards.
501) 
502)             If neither are given, then the agent's socket location is
503)             looked up in the `SSH_AUTH_SOCK` environment variable, and
504)             used to construct/deconstruct a one-shot client, as in the
505)             previous case.
506) 
507)     Yields:
508)         :
509)             Every SSH key from the SSH agent that is suitable for
510)             passphrase derivation.
511) 
512)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

513)         KeyError:
514)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
515)             variable was not found.
516)         OSError:
517)             `conn` was a socket or `None`, and there was an error
518)             setting up a socket connection to the agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

526) 
527)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

532)             client = conn
533)             client_context = contextlib.nullcontext()
534)         case socket.socket() | None:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

536)             client_context = client
537)         case _:  # pragma: no cover
538)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

541)     with client_context:
542)         try:
543)             all_key_comment_pairs = list(client.list_keys())
544)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

553) 
554) 
555) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

558)     single_choice_prompt: str = 'Confirm this choice?',
559) ) -> int:
560)     """Prompt user for a choice among the given items.
561) 
562)     Print the heading, if any, then present the items to the user.  If
563)     there are multiple items, prompt the user for a selection, validate
564)     the choice, then return the list index of the selected item.  If
565)     there is only a single item, request confirmation for that item
566)     instead, and return the correct index.
567) 
568)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

571)         heading:
572)             A heading for the list of items, to print immediately
573)             before.  Defaults to a reasonable standard heading.  If
574)             explicitly empty, print no heading.
575)         single_choice_prompt:
576)             The confirmation prompt if there is only a single possible
577)             choice.  Defaults to a reasonable standard prompt.
578) 
579)     Returns:
580)         An index into the items sequence, indicating the user's
581)         selection.
582) 
583)     Raises:
584)         IndexError:
585)             The user made an invalid or empty selection, or requested an
586)             abort.
587) 
588)     """
589)     n = len(items)
590)     if heading:
591)         click.echo(click.style(heading, bold=True))
592)     for i, x in enumerate(items, start=1):
593)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
594)         click.echo(' ', nl=False)
595)         click.echo(x)
596)     if n > 1:
597)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
598)         choice = click.prompt(
599)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

600)             err=True,
601)             type=choices,
602)             show_choices=False,
603)             show_default=False,
604)             default='',
605)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

609)     prompt_suffix = (
610)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
611)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

612)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

613)         click.confirm(
614)             single_choice_prompt,
615)             prompt_suffix=prompt_suffix,
616)             err=True,
617)             abort=True,
618)             default=False,
619)             show_default=False,
620)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

621)     except click.Abort:
622)         raise IndexError(_EMPTY_SELECTION) from None
623)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

624) 
625) 
626) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

633)     prompted interactively (see [`click.prompt`][]) for a selection.
634) 
635)     Args:
636)         conn:
637)             An optional connection hint to the SSH agent; specifically,
638)             an SSH agent client, or a socket connected to an SSH agent.
639) 
640)             If an existing SSH agent client, then this client will be
641)             queried for the SSH keys, and otherwise left intact.
642) 
643)             If a socket, then a one-shot client will be constructed
644)             based on the socket to query the agent, and deconstructed
645)             afterwards.
646) 
647)             If neither are given, then the agent's socket location is
648)             looked up in the `SSH_AUTH_SOCK` environment variable, and
649)             used to construct/deconstruct a one-shot client, as in the
650)             previous case.
651) 
652)     Returns:
653)         The selected SSH key.
654) 
655)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

656)         KeyError:
657)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
658)             variable was not found.
659)         OSError:
660)             `conn` was a socket or `None`, and there was an error
661)             setting up a socket connection to the agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

662)         IndexError:
663)             The user made an invalid or empty selection, or requested an
664)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

672)     """
673)     suitable_keys = list(_get_suitable_ssh_keys(conn))
674)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

679)         key_prefix = (
680)             key_str
681)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
682)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
683)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

687)         key_listing,
688)         heading='Suitable SSH keys:',
689)         single_choice_prompt='Use this key?',
690)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

691)     return suitable_keys[choice].key
692) 
693) 
694) def _prompt_for_passphrase() -> str:
695)     """Interactively prompt for the passphrase.
696) 
697)     Calls [`click.prompt`][] internally.  Moved into a separate function
698)     mainly for testing/mocking purposes.
699) 
700)     Returns:
701)         The user input.
702) 
703)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

704)     return cast(
705)         str,
706)         click.prompt(
707)             'Passphrase',
708)             default='',
709)             hide_input=True,
710)             show_default=False,
711)             err=True,
712)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

714) 
715) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 weeks ago

716) def _check_for_misleading_passphrase(
717)     key: tuple[str, ...],
718)     value: dict[str, Any],
719)     *,
720)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
721) ) -> None:
722)     def is_json_identifier(x: str) -> bool:
723)         return not x.startswith(tuple('0123456789')) and not any(
724)             c.lower() not in set('0123456789abcdefghijklmnopqrstuvwxyz_')
725)             for c in x
726)         )
727) 
728)     if 'phrase' in value:
729)         phrase = value['phrase']
730)         if not unicodedata.is_normalized(form, phrase):
731)             key_path = '.'.join(
732)                 x if is_json_identifier(x) else repr(x) for x in key
733)             )
734)             click.echo(
735)                 (
736)                     f'{PROG_NAME}: Warning: the {key_path} passphrase '
737)                     f'is not {form}-normalized. Make sure to double-check '
738)                     f'this is really the passphrase you want.'
739)                 ),
740)                 err=True,
741)             )
742) 
743) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

745)     """A [`click.Option`][] with an associated group name and group epilog.
746) 
747)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
748)     help sections.  Each subclass contains its own group name and
749)     epilog.
750) 
751)     Attributes:
752)         option_group_name:
753)             The name of the option group.  Used as a heading on the help
754)             text for options in this section.
755)         epilog:
756)             An epilog to print after listing the options in this
757)             section.
758) 
759)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

760) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

761)     option_group_name: str = ''
762)     epilog: str = ''
763) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

769) 
770) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

771)     """A [`click.Command`][] with support for help/option groups.
772) 
773)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
774)     further modified to support group epilogs.
775) 
776)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
777) 
778)     """
779) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

780)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

781)         self,
782)         ctx: click.Context,
783)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

787)         This is a callback for [`click.Command.get_help`][] that
788)         implements the `--help` listing, by calling appropriate methods
789)         of the `formatter`.  We list all options (like the base
790)         implementation), but grouped into sections according to the
791)         concrete [`click.Option`][] subclass being used.  If the option
792)         is an instance of some subclass `X` of
793)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
794)         heading and the epilog are taken from `X.option_group_name` and
795)         `X.epilog`; otherwise, the section heading is "Options" (or
796)         "Other options" if there are other option groups) and the epilog
797)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

798) 
799)         Args:
800)             ctx:
801)                 The click context.
802)             formatter:
803)                 The formatter for the `--help` listing.
804) 
805)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

806)         help_records: dict[str, list[tuple[str, str]]] = {}
807)         epilogs: dict[str, str] = {}
808)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

809)         if (  # pragma: no branch
810)             (help_opt := self.get_help_option(ctx)) is not None
811)             and help_opt not in params
812)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

813)             params.append(help_opt)
814)         for param in params:
815)             rec = param.get_help_record(ctx)
816)             if rec is not None:
817)                 if isinstance(param, OptionGroupOption):
818)                     group_name = param.option_group_name
819)                     epilogs.setdefault(group_name, param.epilog)
820)                 else:
821)                     group_name = ''
822)                 help_records.setdefault(group_name, []).append(rec)
823)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

824)         default_group_name = (
825)             'Other Options' if len(default_group) > 1 else 'Options'
826)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

827)         help_records[default_group_name] = default_group
828)         for group_name, records in help_records.items():
829)             with formatter.section(group_name):
830)                 formatter.write_dl(records)
831)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
832)             if epilog:
833)                 formatter.write_paragraph()
834)                 with formatter.indentation():
835)                     formatter.write_text(epilog)
836) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

848) 
849) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

857) 
858) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

865)     """
866) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

867) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

868) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

873)     """Check that the occurrence constraint is valid (int, 0 or larger).
874) 
875)     Args:
876)         ctx: The `click` context.
877)         param: The current command-line parameter.
878)         value: The parameter value to be checked.
879) 
880)     Returns:
881)         The parsed parameter value.
882) 
883)     Raises:
884)         click.BadParameter: The parameter value is invalid.
885) 
886)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

889)     if value is None:
890)         return value
891)     if isinstance(value, int):
892)         int_value = value
893)     else:
894)         try:
895)             int_value = int(value, 10)
896)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

902)     return int_value
903) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

904) 
905) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

910)     """Check that the length is valid (int, 1 or larger).
911) 
912)     Args:
913)         ctx: The `click` context.
914)         param: The current command-line parameter.
915)         value: The parameter value to be checked.
916) 
917)     Returns:
918)         The parsed parameter value.
919) 
920)     Raises:
921)         click.BadParameter: The parameter value is invalid.
922) 
923)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

926)     if value is None:
927)         return value
928)     if isinstance(value, int):
929)         int_value = value
930)     else:
931)         try:
932)             int_value = int(value, 10)
933)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

939)     return int_value
940) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

941) 
942) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

943) # Enter notes below the line with the cut mark (ASCII scissors and
944) # dashes).  Lines above the cut mark (such as this one) will be ignored.
945) #
946) # If you wish to clear the notes, leave everything beyond the cut mark
947) # blank.  However, if you leave the *entire* file blank, also removing
948) # the cut mark, then the edit is aborted, and the old notes contents are
949) # retained.
950) #
951) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

953) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
954) 
955) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

970)     """,
971) )
972) @click.option(
973)     '-p',
974)     '--phrase',
975)     'use_phrase',
976)     is_flag=True,
977)     help='prompts you for your passphrase',
978)     cls=PasswordGenerationOption,
979) )
980) @click.option(
981)     '-k',
982)     '--key',
983)     'use_key',
984)     is_flag=True,
985)     help='uses your SSH private key to generate passwords',
986)     cls=PasswordGenerationOption,
987) )
988) @click.option(
989)     '-l',
990)     '--length',
991)     metavar='NUMBER',
992)     callback=_validate_length,
993)     help='emits password of length NUMBER',
994)     cls=PasswordGenerationOption,
995) )
996) @click.option(
997)     '-r',
998)     '--repeat',
999)     metavar='NUMBER',
1000)     callback=_validate_occurrence_constraint,
1001)     help='allows maximum of NUMBER repeated adjacent chars',
1002)     cls=PasswordGenerationOption,
1003) )
1004) @click.option(
1005)     '--lower',
1006)     metavar='NUMBER',
1007)     callback=_validate_occurrence_constraint,
1008)     help='includes at least NUMBER lowercase letters',
1009)     cls=PasswordGenerationOption,
1010) )
1011) @click.option(
1012)     '--upper',
1013)     metavar='NUMBER',
1014)     callback=_validate_occurrence_constraint,
1015)     help='includes at least NUMBER uppercase letters',
1016)     cls=PasswordGenerationOption,
1017) )
1018) @click.option(
1019)     '--number',
1020)     metavar='NUMBER',
1021)     callback=_validate_occurrence_constraint,
1022)     help='includes at least NUMBER digits',
1023)     cls=PasswordGenerationOption,
1024) )
1025) @click.option(
1026)     '--space',
1027)     metavar='NUMBER',
1028)     callback=_validate_occurrence_constraint,
1029)     help='includes at least NUMBER spaces',
1030)     cls=PasswordGenerationOption,
1031) )
1032) @click.option(
1033)     '--dash',
1034)     metavar='NUMBER',
1035)     callback=_validate_occurrence_constraint,
1036)     help='includes at least NUMBER "-" or "_"',
1037)     cls=PasswordGenerationOption,
1038) )
1039) @click.option(
1040)     '--symbol',
1041)     metavar='NUMBER',
1042)     callback=_validate_occurrence_constraint,
1043)     help='includes at least NUMBER symbol chars',
1044)     cls=PasswordGenerationOption,
1045) )
1046) @click.option(
1047)     '-n',
1048)     '--notes',
1049)     'edit_notes',
1050)     is_flag=True,
1051)     help='spawn an editor to edit notes for SERVICE',
1052)     cls=ConfigurationOption,
1053) )
1054) @click.option(
1055)     '-c',
1056)     '--config',
1057)     'store_config_only',
1058)     is_flag=True,
1059)     help='saves the given settings for SERVICE or global',
1060)     cls=ConfigurationOption,
1061) )
1062) @click.option(
1063)     '-x',
1064)     '--delete',
1065)     'delete_service_settings',
1066)     is_flag=True,
1067)     help='deletes settings for SERVICE',
1068)     cls=ConfigurationOption,
1069) )
1070) @click.option(
1071)     '--delete-globals',
1072)     is_flag=True,
1073)     help='deletes the global shared settings',
1074)     cls=ConfigurationOption,
1075) )
1076) @click.option(
1077)     '-X',
1078)     '--clear',
1079)     'clear_all_settings',
1080)     is_flag=True,
1081)     help='deletes all settings',
1082)     cls=ConfigurationOption,
1083) )
1084) @click.option(
1085)     '-e',
1086)     '--export',
1087)     'export_settings',
1088)     metavar='PATH',
1089)     help='export all saved settings into file PATH',
1090)     cls=StorageManagementOption,
1091) )
1092) @click.option(
1093)     '-i',
1094)     '--import',
1095)     'import_settings',
1096)     metavar='PATH',
1097)     help='import saved settings from file PATH',
1098)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 1 month ago

1104)     ctx: click.Context,
1105)     /,
1106)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

1107)     service: str | None = None,
1108)     use_phrase: bool = False,
1109)     use_key: bool = False,
1110)     length: int | None = None,
1111)     repeat: int | None = None,
1112)     lower: int | None = None,
1113)     upper: int | None = None,
1114)     number: int | None = None,
1115)     space: int | None = None,
1116)     dash: int | None = None,
1117)     symbol: int | None = None,
1118)     edit_notes: bool = False,
1119)     store_config_only: bool = False,
1120)     delete_service_settings: bool = False,
1121)     delete_globals: bool = False,
1122)     clear_all_settings: bool = False,
1123)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1124)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 week ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1128)     Using a master passphrase or a master SSH key, derive a passphrase
1129)     for SERVICE, subject to length, character and character repetition
1130)     constraints.  The derivation is cryptographically strong, meaning
1131)     that even if a single passphrase is compromised, guessing the master
1132)     passphrase or a different service's passphrase is computationally
1133)     infeasible.  The derivation is also deterministic, given the same
1134)     inputs, thus the resulting passphrase need not be stored explicitly.
1135)     The service name and constraints themselves also need not be kept
1136)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1146) 
1147)     [CLICK]: https://click.palletsprojects.com/
1148) 
1149)     Parameters:
1150)         ctx (click.Context):
1151)             The `click` context.
1152) 
1153)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1206)         export_settings:
1207)             Command-line argument `-e`/`--export`.  If a file object,
1208)             then it must be open for writing and accept `str` inputs.
1209)             Otherwise, a filename to open for writing.  Using `-` for
1210)             standard output is supported.
1211)         import_settings:
1212)             Command-line argument `-i`/`--import`.  If a file object, it
1213)             must be open for reading and yield `str` values.  Otherwise,
1214)             a filename to open for reading.  Using `-` for standard
1215)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1223)             match param:
1224)                 case PasswordGenerationOption():
1225)                     group = PasswordGenerationOption
1226)                 case ConfigurationOption():
1227)                     group = ConfigurationOption
1228)                 case StorageManagementOption():
1229)                     group = StorageManagementOption
1230)                 case OptionGroupOption():
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1234)                 case _:
1235)                     group = click.Option
1236)             options_in_group.setdefault(group, []).append(param)
1237)         params_by_str[param.human_readable_name] = param
1238)         for name in param.opts + param.secondary_opts:
1239)             params_by_str[name] = param
1240) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1244)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

1248)         if isinstance(param, str):
1249)             param = params_by_str[param]
1250)         assert isinstance(param, click.Parameter)
1251)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

1252)             return
1253)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1256)             assert isinstance(other, click.Parameter)
1257)             if other != param and is_param_set(other):
1258)                 opt_str = param.opts[0]
1259)                 other_str = other.opts[0]
1260)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1264)     def err(msg: str) -> NoReturn:
1265)         click.echo(f'{PROG_NAME}: {msg}', err=True)
1266)         ctx.exit(1)
1267) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1269)         try:
1270)             return _load_config()
1271)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 1 week ago

1272)             try:
1273)                 backup_config, exc = _migrate_and_load_old_config()
1274)             except FileNotFoundError:
1275)                 return {'services': {}}
1276)             old_name = os.path.basename(_config_filename())
1277)             new_name = os.path.basename(_config_filename(subsystem='vault'))
1278)             click.echo(
1279)                 (
1280)                     f'{PROG_NAME}: Using deprecated v0.1-style config file '
1281)                     f'{old_name!r}, instead of v0.2-style {new_name!r}.  '
1282)                     f'Support for v0.1-style config filenames will be '
1283)                     f'removed in v1.0.'
1284)                 ),
1285)                 err=True,
1286)             )
1287)             if isinstance(exc, OSError):
1288)                 click.echo(
1289)                     (
1290)                         f'{PROG_NAME}: Warning: Failed to migrate to '
1291)                         f'{new_name!r}: {exc.strerror}: {exc.filename!r}'
1292)                     ),
1293)                     err=True,
1294)                 )
1295)             else:
1296)                 click.echo(
1297)                     f'{PROG_NAME}: Successfully migrated to {new_name!r}.',
1298)                     err=True,
1299)                 )
1300)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1304)             err(f'Cannot load config: {e}')
1305) 
1306)     def put_config(config: _types.VaultConfig, /) -> None:
1307)         try:
1308)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1315) 
1316)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1321)                     opt, *options_in_group[PasswordGenerationOption]
1322)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1327)                 opt,
1328)                 *options_in_group[ConfigurationOption],
1329)                 *options_in_group[StorageManagementOption],
1330)             )
1331)     sv_options = options_in_group[PasswordGenerationOption] + [
1332)         params_by_str['--notes'],
1333)         params_by_str['--delete'],
1334)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

1335)     sv_options.remove(params_by_str['--key'])
1336)     sv_options.remove(params_by_str['--phrase'])
1337)     for param in sv_options:
1338)         if is_param_set(param) and not service:
1339)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1349)     no_sv_options = [
1350)         params_by_str['--delete-globals'],
1351)         params_by_str['--clear'],
1352)         *options_in_group[StorageManagementOption],
1353)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1359) 
1360)     if edit_notes:
1361)         assert service is not None
1362)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 2 months ago

1369)             while notes_lines:
1370)                 line = notes_lines.popleft()
1371)                 if line.startswith(DEFAULT_NOTES_MARKER):
1372)                     notes_value = ''.join(notes_lines)
1373)                     break
1374)             else:
1375)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1378)                 notes_value.strip('\n')
1379)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1381)     elif delete_service_settings:
1382)         assert service is not None
1383)         configuration = get_config()
1384)         if service in configuration['services']:
1385)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1387)     elif delete_globals:
1388)         configuration = get_config()
1389)         if 'global' in configuration:
1390)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1394)     elif import_settings:
1395)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

1398)             infile = (
1399)                 cast(TextIO, import_settings)
1400)                 if hasattr(import_settings, 'close')
1401)                 else click.open_file(os.fspath(import_settings), 'rt')
1402)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1403)             with infile:
1404)                 maybe_config = json.load(infile)
1405)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 weeks ago

1410)             form = cast(
1411)                 Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1412)                 maybe_config.get('global', {}).get(
1413)                     'unicode_normalization_form', 'NFC'
1414)                 ),
1415)             )
1416)             assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1417)             _check_for_misleading_passphrase(
1418)                 ('global',),
1419)                 cast(dict[str, Any], maybe_config.get('global', {})),
1420)                 form=form,
1421)             )
1422)             for key, value in maybe_config['services'].items():
1423)                 _check_for_misleading_passphrase(
1424)                     ('services', key),
1425)                     cast(dict[str, Any], value),
1426)                     form=form,
1427)                 )
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1431)     elif export_settings:
1432)         configuration = get_config()
1433)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 1 month ago

1436)             outfile = (
1437)                 cast(TextIO, export_settings)
1438)                 if hasattr(export_settings, 'close')
1439)                 else click.open_file(os.fspath(export_settings), 'wt')
1440)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1441)             with outfile:
1442)                 json.dump(configuration, outfile)
1443)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1452)         service_keys = {
1453)             'key',
1454)             'phrase',
1455)             'length',
1456)             'repeat',
1457)             'lower',
1458)             'upper',
1459)             'number',
1460)             'space',
1461)             'dash',
1462)             'symbol',
1463)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1465)             {
1466)                 k: v
1467)                 for k, v in locals().items()
1468)                 if k in service_keys and v is not None
1469)             },
1470)             cast(
1471)                 dict[str, Any],
1472)                 configuration['services'].get(service or '', {}),
1473)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1474)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1476)         )
1477)         if use_key:
1478)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1479)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1480)                     'ASCII'
1481)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1486)             except OSError as e:
1487)                 err(
1488)                     f'Cannot connect to SSH agent: {e.strerror}: '
1489)                     f'{e.filename!r}'
1490)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 1 month ago

1491)             except (
1492)                 LookupError,
1493)                 RuntimeError,
1494)                 ssh_agent.SSHAgentFailedError,
1495)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1497)         elif use_phrase:
1498)             maybe_phrase = _prompt_for_passphrase()
1499)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1501)             else:
1502)                 phrase = maybe_phrase
1503)         if store_config_only:
1504)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1505)             view = (
1506)                 collections.ChainMap(*settings.maps[:2])
1507)                 if service
1508)                 else settings.parents.parents
1509)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1510)             if use_key:
1511)                 view['key'] = key
1512)                 for m in view.maps:
1513)                     m.pop('phrase', '')
1514)             elif use_phrase:
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 weeks ago

1515)                 _check_for_misleading_passphrase(
1516)                     ('services', service) if service else ('global',),
1517)                     {'phrase': phrase},
1518)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1526)                     f'actual settings'
1527)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1529)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1531)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1537)         else:
1538)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1541)             kwargs: dict[str, Any] = {
1542)                 k: v
1543)                 for k, v in settings.items()
1544)                 if k in service_keys and v is not None
1545)             }
1546) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 1 month ago

1547)             def key_to_phrase(
1548)                 key: str | bytes | bytearray,
1549)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1551)                     base64.standard_b64decode(key)
1552)                 )
1553) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 2 weeks ago

1554)             if use_phrase:
1555)                 form = cast(
1556)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1557)                     configuration.get('global', {}).get(
1558)                         'unicode_normalization_form', 'NFC'
1559)                     ),
1560)                 )
1561)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1562)                 _check_for_misleading_passphrase(
1563)                     ('interactive',), {'phrase': phrase}, form=form
1564)                 )
1565) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1566)             # If either --key or --phrase are given, use that setting.
1567)             # Otherwise, if both key and phrase are set in the config,
1568)             # one must be global (ignore it) and one must be
1569)             # service-specific (use that one). Otherwise, if only one of
1570)             # key and phrase is set in the config, use that one.  In all
1571)             # these above cases, set the phrase via
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1581)             elif kwargs.get('phrase'):
1582)                 pass
1583)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1586)                     'or in configuration'
1587)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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