6e2d2728b76c22a0d2280c34a55dba45885084cb
Marco Ricci Change the author e-mail ad...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

11) import copy
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

35) 
36) if TYPE_CHECKING:
37)     import pathlib
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

38)     import socket
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 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 5 months ago

50) PROG_NAME = 'derivepassphrase'
Marco Ricci Make suitable SSH key listi...

Marco Ricci authored 1 month ago

51) KEY_DISPLAY_LENGTH = 50
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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 6 months ago

58) 
59) 
Marco Ricci Shift option parsing and gr...

Marco Ricci authored 1 month ago

60) # Option parsing and grouping
61) # ===========================
62) 
63) 
64) class OptionGroupOption(click.Option):
65)     """A [`click.Option`][] with an associated group name and group epilog.
66) 
67)     Used by [`CommandWithHelpGroups`][] to print help sections.  Each
68)     subclass contains its own group name and epilog.
69) 
70)     Attributes:
71)         option_group_name:
72)             The name of the option group.  Used as a heading on the help
73)             text for options in this section.
74)         epilog:
75)             An epilog to print after listing the options in this
76)             section.
77) 
78)     """
79) 
80)     option_group_name: str = ''
81)     """"""
82)     epilog: str = ''
83)     """"""
84) 
85)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
86)         if self.__class__ == __class__:  # type: ignore[name-defined]
87)             raise NotImplementedError
88)         super().__init__(*args, **kwargs)
89) 
90) 
91) class CommandWithHelpGroups(click.Command):
92)     """A [`click.Command`][] with support for help/option groups.
93) 
94)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
95)     further modified to support group epilogs.
96) 
97)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
98) 
99)     """
100) 
101)     def format_options(
102)         self,
103)         ctx: click.Context,
104)         formatter: click.HelpFormatter,
105)     ) -> None:
106)         r"""Format options on the help listing, grouped into sections.
107) 
108)         This is a callback for [`click.Command.get_help`][] that
109)         implements the `--help` listing, by calling appropriate methods
110)         of the `formatter`.  We list all options (like the base
111)         implementation), but grouped into sections according to the
112)         concrete [`click.Option`][] subclass being used.  If the option
113)         is an instance of some subclass of [`OptionGroupOption`][], then
114)         the section heading and the epilog are taken from the
115)         [`option_group_name`] [OptionGroupOption.option_group_name] and
116)         [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
117)         section heading is "Options" (or "Other options" if there are
118)         other option groups) and the epilog is empty.
119) 
120)         Args:
121)             ctx:
122)                 The click context.
123)             formatter:
124)                 The formatter for the `--help` listing.
125) 
126)         """
127)         help_records: dict[str, list[tuple[str, str]]] = {}
128)         epilogs: dict[str, str] = {}
129)         params = self.params[:]
130)         if (  # pragma: no branch
131)             (help_opt := self.get_help_option(ctx)) is not None
132)             and help_opt not in params
133)         ):
134)             params.append(help_opt)
135)         for param in params:
136)             rec = param.get_help_record(ctx)
137)             if rec is not None:
138)                 if isinstance(param, OptionGroupOption):
139)                     group_name = param.option_group_name
140)                     epilogs.setdefault(group_name, param.epilog)
141)                 else:
142)                     group_name = ''
143)                 help_records.setdefault(group_name, []).append(rec)
144)         default_group = help_records.pop('')
145)         default_group_name = (
146)             'Other Options' if len(default_group) > 1 else 'Options'
147)         )
148)         help_records[default_group_name] = default_group
149)         for group_name, records in help_records.items():
150)             with formatter.section(group_name):
151)                 formatter.write_dl(records)
152)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
153)             if epilog:
154)                 formatter.write_paragraph()
155)                 with formatter.indentation():
156)                     formatter.write_text(epilog)
157) 
158) 
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

159) # Top-level
160) # =========
161) 
162) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

163) class _DefaultToVaultGroup(click.Group):
164)     """A helper class to implement the default-to-"vault"-subcommand behavior.
165) 
166)     Modifies internal [`click.MultiCommand`][] methods, and thus is both
167)     an implementation detail and a kludge.
168) 
169)     """
170) 
171)     def resolve_command(
172)         self, ctx: click.Context, args: list[str]
173)     ) -> tuple[str | None, click.Command | None, list[str]]:
174)         """Resolve a command, but default to "vault" instead of erroring out.
175) 
176)         Based on code from click 8.1, which appears to be essentially
177)         untouched since at least click 3.2.
178) 
179)         """
180)         cmd_name = click.utils.make_str(args[0])
181) 
182)         # ORIGINAL COMMENT
183)         # Get the command
184)         cmd = self.get_command(ctx, cmd_name)
185) 
186)         # ORIGINAL COMMENT
187)         # If we can't find the command but there is a normalization
188)         # function available, we try with that one.
189)         if (  # pragma: no cover
190)             cmd is None and ctx.token_normalize_func is not None
191)         ):
192)             cmd_name = ctx.token_normalize_func(cmd_name)
193)             cmd = self.get_command(ctx, cmd_name)
194) 
195)         # ORIGINAL COMMENT
196)         # If we don't find the command we want to show an error message
197)         # to the user that it was not provided.  However, there is
198)         # something else we should do: if the first argument looks like
199)         # an option we want to kick off parsing again for arguments to
200)         # resolve things like --help which now should go to the main
201)         # place.
202)         if cmd is None and not ctx.resilient_parsing:
203)             if click.parser.split_opt(cmd_name)[0]:
204)                 self.parse_args(ctx, ctx.args)
205)             # Instead of calling ctx.fail here, default to "vault", and
206)             # issue a deprecation warning.
207)             click.echo(
208)                 (
209)                     f'{PROG_NAME}: Deprecation warning: A subcommand will be '
210)                     f'required in v1.0. See --help for available subcommands.'
211)                 ),
212)                 err=True,
213)             )
214)             click.echo(
215)                 f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
216)                 err=True,
217)             )
218)             cmd_name = 'vault'
219)             cmd = self.get_command(ctx, cmd_name)
220)             assert cmd is not None, 'Mandatory subcommand "vault" missing!'
221)             args = [cmd_name, *args]
222)         return cmd_name if cmd else None, cmd, args[1:]  # noqa: DOC201
223) 
224) 
225) @click.group(
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

226)     context_settings={
227)         'help_option_names': ['-h', '--help'],
228)         'ignore_unknown_options': True,
229)         'allow_interspersed_args': False,
230)     },
231)     epilog=r"""
232)         Configuration is stored in a directory according to the
233)         DERIVEPASSPHRASE_PATH variable, which defaults to
234)         `~/.derivepassphrase` on UNIX-like systems and
235)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 3 months ago

236)     """,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

237)     invoke_without_command=True,
238)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

239) )
240) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

241) @click.pass_context
242) def derivepassphrase(ctx: click.Context, /) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

243)     """Derive a strong passphrase, deterministically, from a master secret.
244) 
245)     Using a master secret, derive a passphrase for a named service,
246)     subject to constraints e.g. on passphrase length, allowed
247)     characters, etc.  The exact derivation depends on the selected
248)     derivation scheme.  For each scheme, it is computationally
249)     infeasible to discern the master secret from the derived passphrase.
250)     The derivations are also deterministic, given the same inputs, thus
251)     the resulting passphrases need not be stored explicitly.  The
252)     service name and constraints themselves also generally need not be
253)     kept secret, depending on the scheme.
254) 
255)     The currently implemented subcommands are "vault" (for the scheme
256)     used by vault) and "export" (for exporting foreign configuration
257)     data).  See the respective `--help` output for instructions.  If no
258)     subcommand is given, we default to "vault".
259) 
260)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
261)     in v1.0, the subcommand must be specified explicitly.\f
262) 
263)     This is a [`click`][CLICK]-powered command-line interface function,
264)     and not intended for programmatic use.  Call with arguments
265)     `['--help']` to see full documentation of the interface.  (See also
266)     [`click.testing.CliRunner`][] for controlled, programmatic
267)     invocation.)
268) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

269)     [CLICK]: https://pypi.org/package/click/
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

270) 
271)     """  # noqa: D301
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

272)     if ctx.invoked_subcommand is None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

273)         click.echo(
274)             (
275)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
276)                 f'required in v1.0. See --help for available subcommands.'
277)             ),
278)             err=True,
279)         )
280)         click.echo(
281)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
282)             err=True,
283)         )
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

284)         # See definition of click.Group.invoke, non-chained case.
285)         with ctx:
286)             sub_ctx = derivepassphrase_vault.make_context(
287)                 'vault', ctx.args, parent=ctx
288)             )
289)             with sub_ctx:
290)                 return derivepassphrase_vault.invoke(sub_ctx)
291)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

292) 
293) 
294) # Exporter
295) # ========
296) 
297) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

298) @derivepassphrase.group(
299)     'export',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

300)     context_settings={
301)         'help_option_names': ['-h', '--help'],
302)         'ignore_unknown_options': True,
303)         'allow_interspersed_args': False,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

304)     },
305)     invoke_without_command=True,
306)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

307) )
308) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

309) @click.pass_context
310) def derivepassphrase_export(ctx: click.Context, /) -> None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

311)     """Export a foreign configuration to standard output.
312) 
313)     Read a foreign system configuration, extract all information from
314)     it, and export the resulting configuration to standard output.
315) 
316)     The only available subcommand is "vault", which implements the
317)     vault-native configuration scheme.  If no subcommand is given, we
318)     default to "vault".
319) 
320)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
321)     in v1.0, the subcommand must be specified explicitly.\f
322) 
323)     This is a [`click`][CLICK]-powered command-line interface function,
324)     and not intended for programmatic use.  Call with arguments
325)     `['--help']` to see full documentation of the interface.  (See also
326)     [`click.testing.CliRunner`][] for controlled, programmatic
327)     invocation.)
328) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

329)     [CLICK]: https://pypi.org/package/click/
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

330) 
331)     """  # noqa: D301
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

332)     if ctx.invoked_subcommand is None:
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

333)         click.echo(
334)             (
335)                 f'{PROG_NAME}: Deprecation warning: A subcommand will be '
336)                 f'required in v1.0. See --help for available subcommands.'
337)             ),
338)             err=True,
339)         )
340)         click.echo(
341)             f'{PROG_NAME}: Warning: Defaulting to subcommand "vault".',
342)             err=True,
343)         )
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

344)         # See definition of click.Group.invoke, non-chained case.
345)         with ctx:
346)             sub_ctx = derivepassphrase_export_vault.make_context(
347)                 'vault', ctx.args, parent=ctx
348)             )
349)             # Constructing the subcontext above will usually already
350)             # lead to a click.UsageError, so this block typically won't
351)             # actually be called.
352)             with sub_ctx:  # pragma: no cover
353)                 return derivepassphrase_export_vault.invoke(sub_ctx)
354)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

355) 
356) 
357) def _load_data(
358)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
359)     path: str | bytes | os.PathLike[str],
360)     key: bytes,
361) ) -> Any:  # noqa: ANN401
362)     contents: bytes
363)     module: types.ModuleType
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

364)     # Use match/case here once Python 3.9 becomes unsupported.
365)     if fmt == 'v0.2':
366)         module = importlib.import_module(
367)             'derivepassphrase.exporter.vault_native'
368)         )
369)         if module.STUBBED:
370)             raise ModuleNotFoundError
371)         with open(path, 'rb') as infile:
372)             contents = base64.standard_b64decode(infile.read())
373)         return module.export_vault_native_data(
374)             contents, key, try_formats=['v0.2']
375)         )
376)     elif fmt == 'v0.3':  # noqa: RET505
377)         module = importlib.import_module(
378)             'derivepassphrase.exporter.vault_native'
379)         )
380)         if module.STUBBED:
381)             raise ModuleNotFoundError
382)         with open(path, 'rb') as infile:
383)             contents = base64.standard_b64decode(infile.read())
384)         return module.export_vault_native_data(
385)             contents, key, try_formats=['v0.3']
386)         )
387)     elif fmt == 'storeroom':
388)         module = importlib.import_module('derivepassphrase.exporter.storeroom')
389)         if module.STUBBED:
390)             raise ModuleNotFoundError
391)         return module.export_storeroom_data(path, key)
392)     else:  # pragma: no cover
393)         assert_never(fmt)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

394) 
395) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

396) @derivepassphrase_export.command(
397)     'vault',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

398)     context_settings={'help_option_names': ['-h', '--help']},
399) )
400) @click.option(
401)     '-f',
402)     '--format',
403)     'formats',
404)     metavar='FMT',
405)     multiple=True,
406)     default=('v0.3', 'v0.2', 'storeroom'),
407)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
408)     help='try the following storage formats, in order (default: v0.3, v0.2)',
409) )
410) @click.option(
411)     '-k',
412)     '--key',
413)     metavar='K',
414)     help=(
415)         'use K as the storage master key '
416)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
417)         '`USERNAME` environment variables)'
418)     ),
419) )
420) @click.argument('path', metavar='PATH', required=True)
421) @click.pass_context
422) def derivepassphrase_export_vault(
423)     ctx: click.Context,
424)     /,
425)     *,
426)     path: str | bytes | os.PathLike[str],
427)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
428)     key: str | bytes | None = None,
429) ) -> None:
430)     """Export a vault-native configuration to standard output.
431) 
432)     Read the vault-native configuration at PATH, extract all information
433)     from it, and export the resulting configuration to standard output.
434)     Depending on the configuration format, PATH may either be a file or
435)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
436)     formats.
437) 
438)     If PATH is explicitly given as `VAULT_PATH`, then use the
439)     `VAULT_PATH` environment variable to determine the correct path.
440)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
441)     named `VAULT_PATH`.)
442) 
443)     """
444)     logging.basicConfig()
445)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
446)         path = exporter.get_vault_path()
447)     if key is None:
448)         key = exporter.get_vault_key()
449)     elif isinstance(key, str):  # pragma: no branch
450)         key = key.encode('utf-8')
451)     for fmt in formats:
452)         try:
453)             config = _load_data(fmt, path, key)
454)         except (
455)             IsADirectoryError,
456)             NotADirectoryError,
457)             ValueError,
458)             RuntimeError,
459)         ):
460)             logging.info('Cannot load as %s: %s', fmt, path)
461)             continue
462)         except OSError as exc:
463)             click.echo(
464)                 (
465)                     f'{PROG_NAME}: ERROR: Cannot parse {path!r} as '
466)                     f'a valid config: {exc.strerror}: {exc.filename!r}'
467)                 ),
468)                 err=True,
469)             )
470)             ctx.exit(1)
471)         except ModuleNotFoundError:
472)             # TODO(the-13th-letter): Use backslash continuation.
473)             # https://github.com/nedbat/coveragepy/issues/1836
474)             msg = f"""
475) {PROG_NAME}: ERROR: Cannot load the required Python module "cryptography".
476) {PROG_NAME}: INFO: pip users: see the "export" extra.
477) """.lstrip('\n')
478)             click.echo(msg, nl=False, err=True)
479)             ctx.exit(1)
480)         else:
481)             if not _types.is_vault_config(config):
482)                 click.echo(
483)                     f'{PROG_NAME}: ERROR: Invalid vault config: {config!r}',
484)                     err=True,
485)                 )
486)                 ctx.exit(1)
487)             click.echo(json.dumps(config, indent=2, sort_keys=True))
488)             break
489)     else:
490)         click.echo(
491)             f'{PROG_NAME}: ERROR: Cannot parse {path!r} as a valid config.',
492)             err=True,
493)         )
494)         ctx.exit(1)
495) 
496) 
497) # Vault
498) # =====
499) 
500) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

501) def _config_filename(
502)     subsystem: str | None = 'settings',
503) ) -> str | bytes | pathlib.Path:
504)     """Return the filename of the configuration file for the subsystem.
505) 
506)     The (implicit default) file is currently named `settings.json`,
507)     located within the configuration directory as determined by the
508)     `DERIVEPASSPHRASE_PATH` environment variable, or by
509)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
510)     subsystem, this will usually be a different file within that
511)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

512) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

513)     Args:
514)         subsystem:
515)             Name of the configuration subsystem whose configuration
516)             filename to return.  If not given, return the old filename
517)             from before the subcommand migration.  If `None`, return the
518)             configuration directory instead.
519) 
520)     Raises:
521)         AssertionError:
522)             An unknown subsystem was passed.
523) 
524)     Deprecated:
525)         Since v0.2.0: The implicit default subsystem and the old
526)         configuration filename are deprecated, and will be removed in v1.0.
527)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

528) 
529)     """
530)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

531)     path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
532)         PROG_NAME, force_posix=True
533)     )
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

534)     # Use match/case here once Python 3.9 becomes unsupported.
535)     if subsystem is None:
536)         return path
537)     elif subsystem in {'vault', 'settings'}:  # noqa: RET505
538)         filename = f'{subsystem}.json'
539)     else:  # pragma: no cover
540)         msg = f'Unknown configuration subsystem: {subsystem!r}'
541)         raise AssertionError(msg)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

542)     return os.path.join(path, filename)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

543) 
544) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

546)     """Load a vault(1)-compatible config from the application directory.
547) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

548)     The filename is obtained via [`_config_filename`][].  This must be
549)     an unencrypted JSON file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

550) 
551)     Returns:
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

552)         The vault settings.  See [`_types.VaultConfig`][] for details.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

553) 
554)     Raises:
555)         OSError:
556)             There was an OS error accessing the file.
557)         ValueError:
558)             The data loaded from the file is not a vault(1)-compatible
559)             config.
560) 
561)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

567)     return data
568) 
569) 
Marco Ricci Permit one flaky test and f...

Marco Ricci authored 3 months ago

570) def _migrate_and_load_old_config() -> tuple[
571)     _types.VaultConfig, OSError | None
572) ]:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

573)     """Load and migrate a vault(1)-compatible config.
574) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

575)     The (old) filename is obtained via [`_config_filename`][].  This
576)     must be an unencrypted JSON file.  After loading, the file is
577)     migrated to the new standard filename.
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

578) 
579)     Returns:
580)         The vault settings, and an optional exception encountered during
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

581)         migration.  See [`_types.VaultConfig`][] for details on the
582)         former.
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

583) 
584)     Raises:
585)         OSError:
586)             There was an OS error accessing the old file.
587)         ValueError:
588)             The data loaded from the file is not a vault(1)-compatible
589)             config.
590) 
591)     """
592)     new_filename = _config_filename(subsystem='vault')
593)     old_filename = _config_filename()
594)     with open(old_filename, 'rb') as fileobj:
595)         data = json.load(fileobj)
596)     if not _types.is_vault_config(data):
597)         raise ValueError(_INVALID_VAULT_CONFIG)
598)     try:
599)         os.replace(old_filename, new_filename)
600)     except OSError as exc:
601)         return data, exc
602)     else:
603)         return data, None
604) 
605) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

608) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

609)     The filename is obtained via [`_config_filename`][].  The config
610)     will be stored as an unencrypted JSON file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

611) 
612)     Args:
613)         config:
614)             vault configuration to save.
615) 
616)     Raises:
617)         OSError:
618)             There was an OS error accessing or writing the file.
619)         ValueError:
620)             The data cannot be stored as a vault(1)-compatible config.
621) 
622)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

626)     filedir = os.path.dirname(os.path.abspath(filename))
627)     try:
628)         os.makedirs(filedir, exist_ok=False)
629)     except FileExistsError:
630)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

633)         json.dump(config, fileobj)
634) 
635) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

639)     """Yield all SSH keys suitable for passphrase derivation.
640) 
641)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

643) 
644)     Args:
645)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

646)             An optional connection hint to the SSH agent.  See
647)             [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][].
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

648) 
649)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 3 months ago

650)         Every SSH key from the SSH agent that is suitable for passphrase
651)         derivation.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

652) 
653)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

654)         KeyError:
655)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
656)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

657)         NotImplementedError:
658)             `conn` was `None`, and this Python does not support
659)             [`socket.AF_UNIX`][], so the SSH agent client cannot be
660)             automatically set up.
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

661)         OSError:
662)             `conn` was a socket or `None`, and there was an error
663)             setting up a socket connection to the agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

667)         RuntimeError:
668)             There was an error communicating with the SSH agent.
Marco Ricci Fix miscellaneous small doc...

Marco Ricci authored 3 months ago

669)         ssh_agent.SSHAgentFailedError:
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

671) 
672)     """
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

673)     with ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn) as client:
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

674)         try:
675)             all_key_comment_pairs = list(client.list_keys())
676)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

677)             raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from e
Marco Ricci Publish polished `is_suitab...

Marco Ricci authored 1 month ago

678)         suitable_keys = copy.copy(all_key_comment_pairs)
679)         for pair in all_key_comment_pairs:
680)             key, _comment = pair
681)             if vault.Vault.is_suitable_ssh_key(key, client=client):
682)                 yield pair
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

683)     if not suitable_keys:  # pragma: no cover
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

685) 
686) 
687) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

690)     single_choice_prompt: str = 'Confirm this choice?',
691) ) -> int:
692)     """Prompt user for a choice among the given items.
693) 
694)     Print the heading, if any, then present the items to the user.  If
695)     there are multiple items, prompt the user for a selection, validate
696)     the choice, then return the list index of the selected item.  If
697)     there is only a single item, request confirmation for that item
698)     instead, and return the correct index.
699) 
700)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

703)         heading:
704)             A heading for the list of items, to print immediately
705)             before.  Defaults to a reasonable standard heading.  If
706)             explicitly empty, print no heading.
707)         single_choice_prompt:
708)             The confirmation prompt if there is only a single possible
709)             choice.  Defaults to a reasonable standard prompt.
710) 
711)     Returns:
712)         An index into the items sequence, indicating the user's
713)         selection.
714) 
715)     Raises:
716)         IndexError:
717)             The user made an invalid or empty selection, or requested an
718)             abort.
719) 
720)     """
721)     n = len(items)
722)     if heading:
723)         click.echo(click.style(heading, bold=True))
724)     for i, x in enumerate(items, start=1):
725)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
726)         click.echo(' ', nl=False)
727)         click.echo(x)
728)     if n > 1:
729)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
730)         choice = click.prompt(
731)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

732)             err=True,
733)             type=choices,
734)             show_choices=False,
735)             show_default=False,
736)             default='',
737)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

741)     prompt_suffix = (
742)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
743)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

744)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

745)         click.confirm(
746)             single_choice_prompt,
747)             prompt_suffix=prompt_suffix,
748)             err=True,
749)             abort=True,
750)             default=False,
751)             show_default=False,
752)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

753)     except click.Abort:
754)         raise IndexError(_EMPTY_SELECTION) from None
755)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

756) 
757) 
758) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

760) ) -> bytes | bytearray:
761)     """Interactively select an SSH key for passphrase derivation.
762) 
763)     Suitable SSH keys are queried from the running SSH agent (see
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

764)     [`ssh_agent.SSHAgentClient.list_keys`][]), then the user is prompted
765)     interactively (see [`click.prompt`][]) for a selection.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

766) 
767)     Args:
768)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

769)             An optional connection hint to the SSH agent.  See
770)             [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][].
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

771) 
772)     Returns:
773)         The selected SSH key.
774) 
775)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

776)         KeyError:
777)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
778)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

779)         NotImplementedError:
780)             `conn` was `None`, and this Python does not support
781)             [`socket.AF_UNIX`][], so the SSH agent client cannot be
782)             automatically set up.
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

783)         OSError:
784)             `conn` was a socket or `None`, and there was an error
785)             setting up a socket connection to the agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

786)         IndexError:
787)             The user made an invalid or empty selection, or requested an
788)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

796)     """
797)     suitable_keys = list(_get_suitable_ssh_keys(conn))
798)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

800)     for key, comment in suitable_keys:
801)         keytype = unstring_prefix(key)[0].decode('ASCII')
802)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Make suitable SSH key listi...

Marco Ricci authored 1 month ago

803)         remaining_key_display_length = KEY_DISPLAY_LENGTH - 1 - len(keytype)
804)         key_extract = min(
805)             key_str,
806)             '...' + key_str[-remaining_key_display_length:],
807)             key=len,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

809)         comment_str = comment.decode('UTF-8', errors='replace')
Marco Ricci Make suitable SSH key listi...

Marco Ricci authored 1 month ago

810)         key_listing.append(f'{keytype} {key_extract}  {comment_str}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

811)     choice = _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

812)         key_listing,
813)         heading='Suitable SSH keys:',
814)         single_choice_prompt='Use this key?',
815)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

816)     return suitable_keys[choice].key
817) 
818) 
819) def _prompt_for_passphrase() -> str:
820)     """Interactively prompt for the passphrase.
821) 
822)     Calls [`click.prompt`][] internally.  Moved into a separate function
823)     mainly for testing/mocking purposes.
824) 
825)     Returns:
826)         The user input.
827) 
828)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

829)     return cast(
830)         str,
831)         click.prompt(
832)             'Passphrase',
833)             default='',
834)             hide_input=True,
835)             show_default=False,
836)             err=True,
837)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

839) 
840) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

841) class _ORIGIN(enum.Enum):
842)     INTERACTIVE: str = 'interactive'
843) 
844) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

845) def _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

846)     key: tuple[str, ...] | _ORIGIN,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

847)     value: dict[str, Any],
848)     *,
849)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
850) ) -> None:
851)     if 'phrase' in value:
852)         phrase = value['phrase']
853)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

854)             formatted_key = (
855)                 key.value
856)                 if isinstance(key, _ORIGIN)
857)                 else _types.json_path(key)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

858)             )
859)             click.echo(
860)                 (
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

861)                     f'{PROG_NAME}: Warning: the {formatted_key} '
862)                     f'passphrase is not {form}-normalized. Make sure to '
863)                     f'double-check this is really the passphrase you want.'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

864)                 ),
865)                 err=True,
866)             )
867) 
868) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

879) 
880) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

888) 
889) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

896)     """
897) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

898) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

899) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

904)     """Check that the occurrence constraint is valid (int, 0 or larger).
905) 
906)     Args:
907)         ctx: The `click` context.
908)         param: The current command-line parameter.
909)         value: The parameter value to be checked.
910) 
911)     Returns:
912)         The parsed parameter value.
913) 
914)     Raises:
915)         click.BadParameter: The parameter value is invalid.
916) 
917)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

920)     if value is None:
921)         return value
922)     if isinstance(value, int):
923)         int_value = value
924)     else:
925)         try:
926)             int_value = int(value, 10)
927)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

933)     return int_value
934) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

935) 
936) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

941)     """Check that the length is valid (int, 1 or larger).
942) 
943)     Args:
944)         ctx: The `click` context.
945)         param: The current command-line parameter.
946)         value: The parameter value to be checked.
947) 
948)     Returns:
949)         The parsed parameter value.
950) 
951)     Raises:
952)         click.BadParameter: The parameter value is invalid.
953) 
954)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

957)     if value is None:
958)         return value
959)     if isinstance(value, int):
960)         int_value = value
961)     else:
962)         try:
963)             int_value = int(value, 10)
964)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

970)     return int_value
971) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

972) 
973) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

974) # Enter notes below the line with the cut mark (ASCII scissors and
975) # dashes).  Lines above the cut mark (such as this one) will be ignored.
976) #
977) # If you wish to clear the notes, leave everything beyond the cut mark
978) # blank.  However, if you leave the *entire* file blank, also removing
979) # the cut mark, then the edit is aborted, and the old notes contents are
980) # retained.
981) #
982) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

984) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
985) 
986) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

987) @derivepassphrase.command(
988)     'vault',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1000)     """,
1001) )
1002) @click.option(
1003)     '-p',
1004)     '--phrase',
1005)     'use_phrase',
1006)     is_flag=True,
1007)     help='prompts you for your passphrase',
1008)     cls=PasswordGenerationOption,
1009) )
1010) @click.option(
1011)     '-k',
1012)     '--key',
1013)     'use_key',
1014)     is_flag=True,
1015)     help='uses your SSH private key to generate passwords',
1016)     cls=PasswordGenerationOption,
1017) )
1018) @click.option(
1019)     '-l',
1020)     '--length',
1021)     metavar='NUMBER',
1022)     callback=_validate_length,
1023)     help='emits password of length NUMBER',
1024)     cls=PasswordGenerationOption,
1025) )
1026) @click.option(
1027)     '-r',
1028)     '--repeat',
1029)     metavar='NUMBER',
1030)     callback=_validate_occurrence_constraint,
1031)     help='allows maximum of NUMBER repeated adjacent chars',
1032)     cls=PasswordGenerationOption,
1033) )
1034) @click.option(
1035)     '--lower',
1036)     metavar='NUMBER',
1037)     callback=_validate_occurrence_constraint,
1038)     help='includes at least NUMBER lowercase letters',
1039)     cls=PasswordGenerationOption,
1040) )
1041) @click.option(
1042)     '--upper',
1043)     metavar='NUMBER',
1044)     callback=_validate_occurrence_constraint,
1045)     help='includes at least NUMBER uppercase letters',
1046)     cls=PasswordGenerationOption,
1047) )
1048) @click.option(
1049)     '--number',
1050)     metavar='NUMBER',
1051)     callback=_validate_occurrence_constraint,
1052)     help='includes at least NUMBER digits',
1053)     cls=PasswordGenerationOption,
1054) )
1055) @click.option(
1056)     '--space',
1057)     metavar='NUMBER',
1058)     callback=_validate_occurrence_constraint,
1059)     help='includes at least NUMBER spaces',
1060)     cls=PasswordGenerationOption,
1061) )
1062) @click.option(
1063)     '--dash',
1064)     metavar='NUMBER',
1065)     callback=_validate_occurrence_constraint,
1066)     help='includes at least NUMBER "-" or "_"',
1067)     cls=PasswordGenerationOption,
1068) )
1069) @click.option(
1070)     '--symbol',
1071)     metavar='NUMBER',
1072)     callback=_validate_occurrence_constraint,
1073)     help='includes at least NUMBER symbol chars',
1074)     cls=PasswordGenerationOption,
1075) )
1076) @click.option(
1077)     '-n',
1078)     '--notes',
1079)     'edit_notes',
1080)     is_flag=True,
1081)     help='spawn an editor to edit notes for SERVICE',
1082)     cls=ConfigurationOption,
1083) )
1084) @click.option(
1085)     '-c',
1086)     '--config',
1087)     'store_config_only',
1088)     is_flag=True,
1089)     help='saves the given settings for SERVICE or global',
1090)     cls=ConfigurationOption,
1091) )
1092) @click.option(
1093)     '-x',
1094)     '--delete',
1095)     'delete_service_settings',
1096)     is_flag=True,
1097)     help='deletes settings for SERVICE',
1098)     cls=ConfigurationOption,
1099) )
1100) @click.option(
1101)     '--delete-globals',
1102)     is_flag=True,
1103)     help='deletes the global shared settings',
1104)     cls=ConfigurationOption,
1105) )
1106) @click.option(
1107)     '-X',
1108)     '--clear',
1109)     'clear_all_settings',
1110)     is_flag=True,
1111)     help='deletes all settings',
1112)     cls=ConfigurationOption,
1113) )
1114) @click.option(
1115)     '-e',
1116)     '--export',
1117)     'export_settings',
1118)     metavar='PATH',
1119)     help='export all saved settings into file PATH',
1120)     cls=StorageManagementOption,
1121) )
1122) @click.option(
1123)     '-i',
1124)     '--import',
1125)     'import_settings',
1126)     metavar='PATH',
1127)     help='import saved settings from file PATH',
1128)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1134)     ctx: click.Context,
1135)     /,
1136)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1137)     service: str | None = None,
1138)     use_phrase: bool = False,
1139)     use_key: bool = False,
1140)     length: int | None = None,
1141)     repeat: int | None = None,
1142)     lower: int | None = None,
1143)     upper: int | None = None,
1144)     number: int | None = None,
1145)     space: int | None = None,
1146)     dash: int | None = None,
1147)     symbol: int | None = None,
1148)     edit_notes: bool = False,
1149)     store_config_only: bool = False,
1150)     delete_service_settings: bool = False,
1151)     delete_globals: bool = False,
1152)     clear_all_settings: bool = False,
1153)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1154)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1158)     Using a master passphrase or a master SSH key, derive a passphrase
1159)     for SERVICE, subject to length, character and character repetition
1160)     constraints.  The derivation is cryptographically strong, meaning
1161)     that even if a single passphrase is compromised, guessing the master
1162)     passphrase or a different service's passphrase is computationally
1163)     infeasible.  The derivation is also deterministic, given the same
1164)     inputs, thus the resulting passphrase need not be stored explicitly.
1165)     The service name and constraints themselves also need not be kept
1166)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1176) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

1177)     [CLICK]: https://pypi.org/package/click/
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1178) 
1179)     Parameters:
1180)         ctx (click.Context):
1181)             The `click` context.
1182) 
1183)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1236)         export_settings:
1237)             Command-line argument `-e`/`--export`.  If a file object,
1238)             then it must be open for writing and accept `str` inputs.
1239)             Otherwise, a filename to open for writing.  Using `-` for
1240)             standard output is supported.
1241)         import_settings:
1242)             Command-line argument `-i`/`--import`.  If a file object, it
1243)             must be open for reading and yield `str` values.  Otherwise,
1244)             a filename to open for reading.  Using `-` for standard
1245)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1250)     for param in ctx.command.params:
1251)         if isinstance(param, click.Option):
1252)             group: type[click.Option]
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

1253)             # Use match/case here once Python 3.9 becomes unsupported.
1254)             if isinstance(param, PasswordGenerationOption):
1255)                 group = PasswordGenerationOption
1256)             elif isinstance(param, ConfigurationOption):
1257)                 group = ConfigurationOption
1258)             elif isinstance(param, StorageManagementOption):
1259)                 group = StorageManagementOption
1260)             elif isinstance(param, OptionGroupOption):
1261)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1262)                     f'Unknown option group for {param!r}'  # noqa: EM102
1263)                 )
1264)             else:
1265)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1266)             options_in_group.setdefault(group, []).append(param)
1267)         params_by_str[param.human_readable_name] = param
1268)         for name in param.opts + param.secondary_opts:
1269)             params_by_str[name] = param
1270) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1274)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1278)         if isinstance(param, str):
1279)             param = params_by_str[param]
1280)         assert isinstance(param, click.Parameter)
1281)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1282)             return
1283)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1286)             assert isinstance(other, click.Parameter)
1287)             if other != param and is_param_set(other):
1288)                 opt_str = param.opts[0]
1289)                 other_str = other.opts[0]
1290)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1294)     def err(msg: str) -> NoReturn:
1295)         click.echo(f'{PROG_NAME}: {msg}', err=True)
1296)         ctx.exit(1)
1297) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1299)         try:
1300)             return _load_config()
1301)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1302)             try:
1303)                 backup_config, exc = _migrate_and_load_old_config()
1304)             except FileNotFoundError:
1305)                 return {'services': {}}
1306)             old_name = os.path.basename(_config_filename())
1307)             new_name = os.path.basename(_config_filename(subsystem='vault'))
1308)             click.echo(
1309)                 (
1310)                     f'{PROG_NAME}: Using deprecated v0.1-style config file '
1311)                     f'{old_name!r}, instead of v0.2-style {new_name!r}.  '
1312)                     f'Support for v0.1-style config filenames will be '
1313)                     f'removed in v1.0.'
1314)                 ),
1315)                 err=True,
1316)             )
1317)             if isinstance(exc, OSError):
1318)                 click.echo(
1319)                     (
1320)                         f'{PROG_NAME}: Warning: Failed to migrate to '
1321)                         f'{new_name!r}: {exc.strerror}: {exc.filename!r}'
1322)                     ),
1323)                     err=True,
1324)                 )
1325)             else:
1326)                 click.echo(
1327)                     f'{PROG_NAME}: Successfully migrated to {new_name!r}.',
1328)                     err=True,
1329)                 )
1330)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

1334)             err(f'Cannot load config: {e}')
1335) 
1336)     def put_config(config: _types.VaultConfig, /) -> None:
1337)         try:
1338)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1345) 
1346)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1351)                     opt, *options_in_group[PasswordGenerationOption]
1352)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1357)                 opt,
1358)                 *options_in_group[ConfigurationOption],
1359)                 *options_in_group[StorageManagementOption],
1360)             )
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

1361)     sv_or_global_options = options_in_group[PasswordGenerationOption]
1362)     for param in sv_or_global_options:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1367)             msg = f'{opt_str} requires a SERVICE or --config'
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

1368)             raise click.UsageError(msg)  # noqa: DOC501
1369)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
1370)     for param in sv_options:
1371)         if is_param_set(param) and not service:
1372)             opt_str = param.opts[0]
1373)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1374)             raise click.UsageError(msg)
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1375)     no_sv_options = [
1376)         params_by_str['--delete-globals'],
1377)         params_by_str['--clear'],
1378)         *options_in_group[StorageManagementOption],
1379)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1385) 
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1386)     if service == '':  # noqa: PLC1901
1387)         click.echo(
1388)             (
1389)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1390)                 f'supported by vault(1).  For compatibility, this will be '
1391)                 f'treated as if SERVICE was not supplied, i.e., it will '
1392)                 f'error out, or operate on global settings.'
1393)             ),
1394)             err=True,
1395)         )
1396) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1397)     if edit_notes:
1398)         assert service is not None
1399)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1406)             while notes_lines:
1407)                 line = notes_lines.popleft()
1408)                 if line.startswith(DEFAULT_NOTES_MARKER):
1409)                     notes_value = ''.join(notes_lines)
1410)                     break
1411)             else:
1412)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1415)                 notes_value.strip('\n')
1416)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1418)     elif delete_service_settings:
1419)         assert service is not None
1420)         configuration = get_config()
1421)         if service in configuration['services']:
1422)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1424)     elif delete_globals:
1425)         configuration = get_config()
1426)         if 'global' in configuration:
1427)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1431)     elif import_settings:
1432)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1435)             infile = (
1436)                 cast(TextIO, import_settings)
1437)                 if hasattr(import_settings, 'close')
1438)                 else click.open_file(os.fspath(import_settings), 'rt')
1439)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1440)             with infile:
1441)                 maybe_config = json.load(infile)
1442)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1445)             err(f'Cannot load config: {e.strerror}: {e.filename!r}')
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1446)         cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)
1447)         if not _types.is_vault_config(maybe_config):
1448)             err(f'Cannot load config: {_INVALID_VAULT_CONFIG}')
1449)         assert cleaned is not None
1450)         for step in cleaned:
1451)             # These are never fatal errors, because the semantics of
1452)             # vault upon encountering these settings are ill-specified,
1453)             # but not ill-defined.
1454)             if step.action == 'replace':
1455)                 err_msg = (
1456)                     f'{PROG_NAME}: Warning: Replacing invalid value '
1457)                     f'{json.dumps(step.old_value)} for key '
1458)                     f'{_types.json_path(step.path)} with '
1459)                     f'{json.dumps(step.new_value)}.'
1460)                 )
1461)             else:
1462)                 err_msg = (
1463)                     f'{PROG_NAME}: Warning: Removing ineffective setting '
1464)                     f'{_types.json_path(step.path)} = '
1465)                     f'{json.dumps(step.old_value)}.'
1466)                 )
1467)             click.echo(err_msg, err=True)
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1468)         if '' in maybe_config['services']:
1469)             err_msg = (
1470)                 f'{PROG_NAME}: Warning: An empty SERVICE is not '
1471)                 f'supported by vault(1), and the empty-string service '
1472)                 f'settings will be inaccessible and ineffective.  '
1473)                 f'To ensure that vault(1) and {PROG_NAME} see the settings, '
1474)                 f'move them into the "global" section.'
1475)             )
1476)             click.echo(err_msg, err=True)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1477)         form = cast(
1478)             Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1479)             maybe_config.get('global', {}).get(
1480)                 'unicode_normalization_form', 'NFC'
1481)             ),
1482)         )
1483)         assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1484)         _check_for_misleading_passphrase(
1485)             ('global',),
1486)             cast(dict[str, Any], maybe_config.get('global', {})),
1487)             form=form,
1488)         )
1489)         for key, value in maybe_config['services'].items():
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1490)             _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1491)                 ('services', key),
1492)                 cast(dict[str, Any], value),
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1493)                 form=form,
1494)             )
Marco Ricci Align behavior with vault c...

Marco Ricci authored 2 months ago

1495)         configuration = get_config()
1496)         merged_config: collections.ChainMap[str, Any] = collections.ChainMap(
1497)             {
1498)                 'services': collections.ChainMap(
1499)                     maybe_config['services'],
1500)                     configuration['services'],
1501)                 ),
1502)             },
1503)             {'global': maybe_config['global']}
1504)             if 'global' in maybe_config
1505)             else {},
1506)             {'global': configuration['global']}
1507)             if 'global' in configuration
1508)             else {},
1509)         )
Marco Ricci Fix a typing issue

Marco Ricci authored 1 month ago

1510)         new_config: Any = {
Marco Ricci Align behavior with vault c...

Marco Ricci authored 2 months ago

1511)             k: dict(v) if isinstance(v, collections.ChainMap) else v
1512)             for k, v in sorted(merged_config.items())
1513)         }
1514)         assert _types.is_vault_config(new_config)
1515)         put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1516)     elif export_settings:
1517)         configuration = get_config()
1518)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1521)             outfile = (
1522)                 cast(TextIO, export_settings)
1523)                 if hasattr(export_settings, 'close')
1524)                 else click.open_file(os.fspath(export_settings), 'wt')
1525)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1526)             with outfile:
1527)                 json.dump(configuration, outfile)
1528)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1537)         service_keys = {
1538)             'key',
1539)             'phrase',
1540)             'length',
1541)             'repeat',
1542)             'lower',
1543)             'upper',
1544)             'number',
1545)             'space',
1546)             'dash',
1547)             'symbol',
1548)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1550)             {
1551)                 k: v
1552)                 for k, v in locals().items()
1553)                 if k in service_keys and v is not None
1554)             },
1555)             cast(
1556)                 dict[str, Any],
1557)                 configuration['services'].get(service or '', {}),
1558)             ),
1559)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1560)         )
1561)         if use_key:
1562)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1563)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1564)                     'ASCII'
1565)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

1569)                 err('Cannot find running SSH agent; check SSH_AUTH_SOCK')
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

1570)             except NotImplementedError:
1571)                 err(
1572)                     'Cannot connect to SSH agent because '
1573)                     'this Python version does not support UNIX domain sockets'
1574)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1575)             except OSError as e:
1576)                 err(
1577)                     f'Cannot connect to SSH agent: {e.strerror}: '
1578)                     f'{e.filename!r}'
1579)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

1580)             except (
1581)                 LookupError,
1582)                 RuntimeError,
1583)                 ssh_agent.SSHAgentFailedError,
1584)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1586)         elif use_phrase:
1587)             maybe_phrase = _prompt_for_passphrase()
1588)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1590)             else:
1591)                 phrase = maybe_phrase
1592)         if store_config_only:
1593)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1594)             view = (
1595)                 collections.ChainMap(*settings.maps[:2])
1596)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1597)                 else collections.ChainMap(settings.maps[0], settings.maps[2])
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1599)             if use_key:
1600)                 view['key'] = key
1601)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1602)                 view['phrase'] = phrase
1603)                 settings_type = 'service' if service else 'global'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1604)                 _check_for_misleading_passphrase(
1605)                     ('services', service) if service else ('global',),
1606)                     {'phrase': phrase},
1607)                 )
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

1608)                 if 'key' in settings:
1609)                     err_msg = (
1610)                         f'{PROG_NAME}: Warning: Setting a {settings_type} '
1611)                         f'passphrase is ineffective because a key is also '
1612)                         f'set.'
1613)                     )
1614)                     click.echo(err_msg, err=True)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1619)                     f'actual settings'
1620)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1622)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1624)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1630)         else:
1631)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1634)             kwargs: dict[str, Any] = {
1635)                 k: v
1636)                 for k, v in settings.items()
1637)                 if k in service_keys and v is not None
1638)             }
1639) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 5 months ago

1640)             def key_to_phrase(
1641)                 key: str | bytes | bytearray,
1642)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1644)                     base64.standard_b64decode(key)
1645)                 )
1646) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1647)             if use_phrase:
1648)                 form = cast(
1649)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1650)                     configuration.get('global', {}).get(
1651)                         'unicode_normalization_form', 'NFC'
1652)                     ),
1653)                 )
1654)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1655)                 _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1656)                     _ORIGIN.INTERACTIVE, {'phrase': phrase}, form=form
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1657)                 )
1658) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1659)             # If either --key or --phrase are given, use that setting.
1660)             # Otherwise, if both key and phrase are set in the config,
Marco Ricci Align behavior with vault c...

Marco Ricci authored 3 months ago

1661)             # use the key.  Otherwise, if only one of key and phrase is
1662)             # set in the config, use that one.  In all these above
1663)             # cases, set the phrase via vault.Vault.phrase_from_key if
1664)             # a key is given.  Finally, if nothing is set, error out.
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1665)             if use_key or use_phrase:
Marco Ricci Avoid crashing when overrid...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1669)             elif kwargs.get('phrase'):
1670)                 pass
1671)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1674)                     'or in configuration'
1675)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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