9a88450ed3941eae25938053149a8bbf89c319c8
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 Use the logging system to e...

Marco Ricci authored 1 month ago

5) # ruff: noqa: TRY400
6) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

8) 
9) from __future__ import annotations
10) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

11) import base64
12) import collections
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

16) import inspect
17) import json
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

20) import unicodedata
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

21) import warnings
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

22) from typing import (
23)     TYPE_CHECKING,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

24)     Callable,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

27)     TextIO,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

28)     TypeVar,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

29)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

31) 
32) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

33) from typing_extensions import (
34)     Any,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

35)     ParamSpec,
36)     Self,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

37)     assert_never,
38) )
39) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

42) 
43) if TYPE_CHECKING:
44)     import pathlib
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

47)     from collections.abc import (
48)         Iterator,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

49)         MutableSequence,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

50)         Sequence,
51)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

52) 
53) __author__ = dpp.__author__
54) __version__ = dpp.__version__
55) 
56) __all__ = ('derivepassphrase',)
57) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 5 months ago

60) 
61) # Error messages
62) _INVALID_VAULT_CONFIG = 'Invalid vault config'
63) _AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent'
64) _NO_USABLE_KEYS = 'No usable SSH keys were found'
65) _EMPTY_SELECTION = 'Empty selection'
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

66) 
67) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

68) # Logging
69) # =======
70) 
71) 
72) class ClickEchoStderrHandler(logging.Handler):
73)     """A [`logging.Handler`][] for `click` applications.
74) 
75)     Outputs log messages to [`sys.stderr`][] via [`click.echo`][].
76) 
77)     """
78) 
79)     def emit(self, record: logging.LogRecord) -> None:
80)         """Emit a log record.
81) 
82)         Format the log record, then emit it via [`click.echo`][] to
83)         [`sys.stderr`][].
84) 
85)         """
86)         click.echo(self.format(record), err=True)
87) 
88) 
89) class CLIofPackageFormatter(logging.Formatter):
90)     """A [`logging.LogRecord`][] formatter for the CLI of a Python package.
91) 
92)     Assuming a package `PKG` and loggers within the same hierarchy
93)     `PKG`, format all log records from that hierarchy for proper user
94)     feedback on the console.  Intended for use with [`click`][CLICK] and
95)     when `PKG` provides a command-line tool `PKG` and when logs from
96)     that package should show up as output of the command-line tool.
97) 
98)     Essentially, this prepends certain short strings to the log message
99)     lines to make them readable as standard error output.
100) 
101)     Because this log output is intended to be displayed on standard
102)     error as high-level diagnostic output, you are strongly discouraged
103)     from changing the output format to include more tokens besides the
104)     log message.  Use a dedicated log file handler instead, without this
105)     formatter.
106) 
107)     [CLICK]: https://pypi.org/projects/click/
108) 
109)     """
110) 
111)     def __init__(
112)         self,
113)         *,
114)         prog_name: str = PROG_NAME,
115)         package_name: str | None = None,
116)     ) -> None:
117)         self.prog_name = prog_name
118)         self.package_name = (
119)             package_name
120)             if package_name is not None
121)             else prog_name.lower().replace(' ', '_').replace('-', '_')
122)         )
123) 
124)     def format(self, record: logging.LogRecord) -> str:
125)         """Format a log record suitably for standard error console output.
126) 
127)         Prepend the formatted string `"PROG_NAME: LABEL"` to each line
128)         of the message, where `PROG_NAME` is the program name, and
129)         `LABEL` depends on the record's level and on the logger name as
130)         follows:
131) 
132)           * For records at level [`logging.DEBUG`][], `LABEL` is
133)             `"Debug: "`.
134)           * For records at level [`logging.INFO`][], `LABEL` is the
135)             empty string.
136)           * For records at level [`logging.WARNING`][], `LABEL` is
137)             `"Deprecation warning: "` if the logger is named
138)             `PKG.deprecation` (where `PKG` is the package name), else
139)             `"Warning: "`.
140)           * For records at level [`logging.ERROR`][] and
141)             [`logging.CRITICAL`][] `"Error: "`, `LABEL` is `"ERROR: "`.
142) 
143)         The level indication strings at level `WARNING` or above are
144)         highlighted.  Use [`click.echo`][] to output them and remove
145)         color output if necessary.
146) 
147)         Args:
148)             record: A log record.
149) 
150)         Returns:
151)             A formatted log record.
152) 
153)         Raises:
154)             AssertionError:
155)                 The log level is not supported.
156) 
157)         """
158)         preliminary_result = record.getMessage()
159)         prefix = f'{self.prog_name}: '
160)         if record.levelname == 'DEBUG':  # pragma: no cover
161)             level_indicator = 'Debug: '
162)         elif record.levelname == 'INFO':
163)             level_indicator = ''
164)         elif record.levelname == 'WARNING':
165)             level_indicator = (
166)                 f'{click.style("Deprecation warning", bold=True)}: '
167)                 if record.name.endswith('.deprecation')
168)                 else f'{click.style("Warning", bold=True)}: '
169)             )
170)         elif record.levelname in {'ERROR', 'CRITICAL'}:
171)             level_indicator = ''
172)         else:  # pragma: no cover
173)             msg = f'Unsupported logging level: {record.levelname}'
174)             raise AssertionError(msg)
175)         return ''.join(
176)             prefix + level_indicator + line
177)             for line in preliminary_result.splitlines(True)  # noqa: FBT003
178)         )
179) 
180) 
181) class StandardCLILogging:
182)     """Set up CLI logging handlers upon instantiation."""
183) 
184)     prog_name = PROG_NAME
185)     package_name = PROG_NAME.lower().replace(' ', '_').replace('-', '_')
186)     cli_formatter = CLIofPackageFormatter(
187)         prog_name=prog_name, package_name=package_name
188)     )
189)     cli_handler = ClickEchoStderrHandler()
190)     cli_handler.addFilter(logging.Filter(name=package_name))
191)     cli_handler.setFormatter(cli_formatter)
192)     cli_handler.setLevel(logging.WARNING)
193)     warnings_handler = ClickEchoStderrHandler()
194)     warnings_handler.addFilter(logging.Filter(name='py.warnings'))
195)     warnings_handler.setFormatter(cli_formatter)
196)     warnings_handler.setLevel(logging.WARNING)
197) 
198)     @classmethod
199)     def ensure_standard_logging(cls) -> StandardLoggingContextManager:
200)         """Return a context manager to ensure standard logging is set up."""
201)         return StandardLoggingContextManager(
202)             handler=cls.cli_handler,
203)             root_logger=cls.package_name,
204)         )
205) 
206)     @classmethod
207)     def ensure_standard_warnings_logging(
208)         cls,
209)     ) -> StandardWarningsLoggingContextManager:
210)         """Return a context manager to ensure warnings logging is set up."""
211)         return StandardWarningsLoggingContextManager(
212)             handler=cls.warnings_handler,
213)         )
214) 
215) 
216) class StandardLoggingContextManager:
217)     """A reentrant context manager setting up standard CLI logging.
218) 
219)     Ensures that the given handler (defaulting to the CLI logging
220)     handler) is added to the named logger (defaulting to the root
221)     logger), and if it had to be added, then that it will be removed
222)     upon exiting the context.
223) 
224)     Reentrant, but not thread safe, because it temporarily modifies
225)     global state.
226) 
227)     """
228) 
229)     def __init__(
230)         self,
231)         handler: logging.Handler,
232)         root_logger: str | None = None,
233)     ) -> None:
234)         self.handler = handler
235)         self.root_logger_name = root_logger
236)         self.base_logger = logging.getLogger(self.root_logger_name)
237)         self.action_required: MutableSequence[bool] = collections.deque()
238) 
239)     def __enter__(self) -> Self:
240)         self.action_required.append(
241)             self.handler not in self.base_logger.handlers
242)         )
243)         if self.action_required[-1]:
244)             self.base_logger.addHandler(self.handler)
245)         return self
246) 
247)     def __exit__(
248)         self,
249)         exc_type: type[BaseException] | None,
250)         exc_value: BaseException | None,
251)         exc_tb: types.TracebackType | None,
252)     ) -> Literal[False]:
253)         if self.action_required[-1]:
254)             self.base_logger.removeHandler(self.handler)
255)         self.action_required.pop()
256)         return False
257) 
258) 
259) class StandardWarningsLoggingContextManager(StandardLoggingContextManager):
260)     """A reentrant context manager setting up standard warnings logging.
261) 
262)     Ensures that warnings are being diverted to the logging system, and
263)     that the given handler (defaulting to the CLI logging handler) is
264)     added to the warnings logger. If the handler had to be added, then
265)     it will be removed upon exiting the context.
266) 
267)     Reentrant, but not thread safe, because it temporarily modifies
268)     global state.
269) 
270)     """
271) 
272)     def __init__(
273)         self,
274)         handler: logging.Handler,
275)     ) -> None:
276)         super().__init__(handler=handler, root_logger='py.warnings')
277)         self.stack: MutableSequence[
278)             tuple[
279)                 Callable[
280)                     [
281)                         type[BaseException] | None,
282)                         BaseException | None,
283)                         types.TracebackType | None,
284)                     ],
285)                     None,
286)                 ],
287)                 Callable[
288)                     [
289)                         str | Warning,
290)                         type[Warning],
291)                         str,
292)                         int,
293)                         TextIO | None,
294)                         str | None,
295)                     ],
296)                     None,
297)                 ],
298)             ]
299)         ] = collections.deque()
300) 
301)     def __enter__(self) -> Self:
302)         def showwarning(  # noqa: PLR0913,PLR0917
303)             message: str | Warning,
304)             category: type[Warning],
305)             filename: str,
306)             lineno: int,
307)             file: TextIO | None = None,
308)             line: str | None = None,
309)         ) -> None:
310)             if file is not None:  # pragma: no cover
311)                 self.stack[0][1](
312)                     message, category, filename, lineno, file, line
313)                 )
314)             else:
315)                 logging.getLogger('py.warnings').warning(
316)                     str(
317)                         warnings.formatwarning(
318)                             message, category, filename, lineno, line
319)                         )
320)                     )
321)                 )
322) 
323)         ctx = warnings.catch_warnings()
324)         exit_func = ctx.__exit__
325)         ctx.__enter__()
326)         self.stack.append((exit_func, warnings.showwarning))
327)         warnings.showwarning = showwarning
328)         return super().__enter__()
329) 
330)     def __exit__(
331)         self,
332)         exc_type: type[BaseException] | None,
333)         exc_value: BaseException | None,
334)         exc_tb: types.TracebackType | None,
335)     ) -> Literal[False]:
336)         ret = super().__exit__(exc_type, exc_value, exc_tb)
337)         val = self.stack.pop()[0](exc_type, exc_value, exc_tb)
338)         assert not val
339)         return ret
340) 
341) 
342) P = ParamSpec('P')
343) R = TypeVar('R')
344) 
345) 
346) def log_debug(
347)     ctx: click.Context,
348)     /,
349)     param: click.Parameter | None = None,
350)     value: Any = None,  # noqa: ANN401
351) ) -> None:
352)     """Request that DEBUG-level logs be emitted to standard error.
353) 
354)     This modifies the [`StandardCLILogging`][] settings such that log
355)     records at level [`logging.DEBUG`][] and [`logging.INFO`][] are
356)     emitted as well.
357) 
358)     """
359)     del ctx, param, value
360)     StandardCLILogging.cli_handler.setLevel(logging.DEBUG)
361) 
362) 
363) def log_info(
364)     ctx: click.Context,
365)     /,
366)     param: click.Parameter | None = None,
367)     value: Any = None,  # noqa: ANN401
368) ) -> None:
369)     """Request that INFO-level logs be emitted to standard error.
370) 
371)     This modifies the [`StandardCLILogging`][] settings such that log
372)     records at level [`logging.INFO`][] are emitted as well.
373) 
374)     """
375)     del ctx, param, value
376)     StandardCLILogging.cli_handler.setLevel(logging.INFO)
377) 
378) 
379) def silence_warnings(
380)     ctx: click.Context,
381)     /,
382)     param: click.Parameter | None = None,
383)     value: Any = None,  # noqa: ANN401
384) ) -> None:
385)     """Request that WARNING-level logs not be emitted to standard error.
386) 
387)     This modifies the [`StandardCLILogging`][] settings such that log
388)     records at level [`logging.WARNING`][] and below are *not* emitted.
389) 
390)     """
391)     del ctx, param, value
392)     StandardCLILogging.cli_handler.setLevel(logging.ERROR)
393) 
394) 
Marco Ricci Shift option parsing and gr...

Marco Ricci authored 1 month ago

395) # Option parsing and grouping
396) # ===========================
397) 
398) 
399) class OptionGroupOption(click.Option):
400)     """A [`click.Option`][] with an associated group name and group epilog.
401) 
402)     Used by [`CommandWithHelpGroups`][] to print help sections.  Each
403)     subclass contains its own group name and epilog.
404) 
405)     Attributes:
406)         option_group_name:
407)             The name of the option group.  Used as a heading on the help
408)             text for options in this section.
409)         epilog:
410)             An epilog to print after listing the options in this
411)             section.
412) 
413)     """
414) 
415)     option_group_name: str = ''
416)     """"""
417)     epilog: str = ''
418)     """"""
419) 
420)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
421)         if self.__class__ == __class__:  # type: ignore[name-defined]
422)             raise NotImplementedError
423)         super().__init__(*args, **kwargs)
424) 
425) 
426) class CommandWithHelpGroups(click.Command):
427)     """A [`click.Command`][] with support for help/option groups.
428) 
429)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
430)     further modified to support group epilogs.
431) 
432)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
433) 
434)     """
435) 
436)     def format_options(
437)         self,
438)         ctx: click.Context,
439)         formatter: click.HelpFormatter,
440)     ) -> None:
441)         r"""Format options on the help listing, grouped into sections.
442) 
443)         This is a callback for [`click.Command.get_help`][] that
444)         implements the `--help` listing, by calling appropriate methods
445)         of the `formatter`.  We list all options (like the base
446)         implementation), but grouped into sections according to the
447)         concrete [`click.Option`][] subclass being used.  If the option
448)         is an instance of some subclass of [`OptionGroupOption`][], then
449)         the section heading and the epilog are taken from the
450)         [`option_group_name`] [OptionGroupOption.option_group_name] and
451)         [`epilog`] [OptionGroupOption.epilog] attributes; otherwise, the
452)         section heading is "Options" (or "Other options" if there are
453)         other option groups) and the epilog is empty.
454) 
455)         Args:
456)             ctx:
457)                 The click context.
458)             formatter:
459)                 The formatter for the `--help` listing.
460) 
461)         """
462)         help_records: dict[str, list[tuple[str, str]]] = {}
463)         epilogs: dict[str, str] = {}
464)         params = self.params[:]
465)         if (  # pragma: no branch
466)             (help_opt := self.get_help_option(ctx)) is not None
467)             and help_opt not in params
468)         ):
469)             params.append(help_opt)
470)         for param in params:
471)             rec = param.get_help_record(ctx)
472)             if rec is not None:
473)                 if isinstance(param, OptionGroupOption):
474)                     group_name = param.option_group_name
475)                     epilogs.setdefault(group_name, param.epilog)
476)                 else:
477)                     group_name = ''
478)                 help_records.setdefault(group_name, []).append(rec)
479)         default_group = help_records.pop('')
480)         default_group_name = (
481)             'Other Options' if len(default_group) > 1 else 'Options'
482)         )
483)         help_records[default_group_name] = default_group
484)         for group_name, records in help_records.items():
485)             with formatter.section(group_name):
486)                 formatter.write_dl(records)
487)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
488)             if epilog:
489)                 formatter.write_paragraph()
490)                 with formatter.indentation():
491)                     formatter.write_text(epilog)
492) 
493) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

494) class LoggingOption(OptionGroupOption):
495)     """Logging options for the CLI."""
496) 
497)     option_group_name = 'Logging'
498)     epilog = ''
499) 
500) 
501) def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:
502)     """Decorate the function with standard logging click options.
503) 
504)     Adds the three click options `-v`/`--verbose`, `-q`/`--quiet` and
505)     `--debug`, which issue callbacks to the [`log_info`][],
506)     [`silence_warnings`][] and [`log_debug`][] functions, respectively.
507) 
508)     Args:
509)         f: A callable to decorate.
510) 
511)     Returns:
512)         The decorated callable.
513) 
514)     """
515)     dec1 = click.option(
516)         '-q',
517)         '--quiet',
518)         is_flag=True,
519)         is_eager=True,
520)         expose_value=False,
521)         callback=silence_warnings,
522)         help='suppress even warnings, emit only errors',
523)         cls=LoggingOption,
524)     )
525)     dec2 = click.option(
526)         '-v',
527)         '--verbose',
528)         is_flag=True,
529)         is_eager=True,
530)         expose_value=False,
531)         callback=log_info,
532)         help='emit extra/progress information to standard error',
533)         cls=LoggingOption,
534)     )
535)     dec3 = click.option(
536)         '--debug',
537)         is_flag=True,
538)         is_eager=True,
539)         expose_value=False,
540)         callback=log_debug,
541)         help='also emit debug information (implies --verbose)',
542)         cls=LoggingOption,
543)     )
544)     return dec1(dec2(dec3(f)))
545) 
546) 
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

547) # Top-level
548) # =========
549) 
550) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

551) class _DefaultToVaultGroup(click.Group):
552)     """A helper class to implement the default-to-"vault"-subcommand behavior.
553) 
554)     Modifies internal [`click.MultiCommand`][] methods, and thus is both
555)     an implementation detail and a kludge.
556) 
557)     """
558) 
559)     def resolve_command(
560)         self, ctx: click.Context, args: list[str]
561)     ) -> tuple[str | None, click.Command | None, list[str]]:
562)         """Resolve a command, but default to "vault" instead of erroring out.
563) 
564)         Based on code from click 8.1, which appears to be essentially
565)         untouched since at least click 3.2.
566) 
567)         """
568)         cmd_name = click.utils.make_str(args[0])
569) 
570)         # ORIGINAL COMMENT
571)         # Get the command
572)         cmd = self.get_command(ctx, cmd_name)
573) 
574)         # ORIGINAL COMMENT
575)         # If we can't find the command but there is a normalization
576)         # function available, we try with that one.
577)         if (  # pragma: no cover
578)             cmd is None and ctx.token_normalize_func is not None
579)         ):
580)             cmd_name = ctx.token_normalize_func(cmd_name)
581)             cmd = self.get_command(ctx, cmd_name)
582) 
583)         # ORIGINAL COMMENT
584)         # If we don't find the command we want to show an error message
585)         # to the user that it was not provided.  However, there is
586)         # something else we should do: if the first argument looks like
587)         # an option we want to kick off parsing again for arguments to
588)         # resolve things like --help which now should go to the main
589)         # place.
590)         if cmd is None and not ctx.resilient_parsing:
591)             if click.parser.split_opt(cmd_name)[0]:
592)                 self.parse_args(ctx, ctx.args)
593)             # Instead of calling ctx.fail here, default to "vault", and
594)             # issue a deprecation warning.
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

595)             logger = logging.getLogger(PROG_NAME)
596)             deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
597)             deprecation.warning(
598)                 'A subcommand will be required in v1.0. '
599)                 'See --help for available subcommands.'
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

600)             )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

601)             logger.warning('Defaulting to subcommand "vault".')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

602)             cmd_name = 'vault'
603)             cmd = self.get_command(ctx, cmd_name)
604)             assert cmd is not None, 'Mandatory subcommand "vault" missing!'
605)             args = [cmd_name, *args]
606)         return cmd_name if cmd else None, cmd, args[1:]  # noqa: DOC201
607) 
608) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

609) class _TopLevelCLIEntryPoint(_DefaultToVaultGroup):
610)     """A minor variation of _DefaultToVaultGroup for the top-level command.
611) 
612)     When called as a function, this sets up the environment properly
613)     before invoking the actual callbacks.  Currently, this means setting
614)     up the logging subsystem and the delegation of Python warnings to
615)     the logging subsystem.
616) 
617)     The environment setup can be bypassed by calling the `.main` method
618)     directly.
619) 
620)     """
621) 
622)     def __call__(  # pragma: no cover
623)         self,
624)         *args: Any,  # noqa: ANN401
625)         **kwargs: Any,  # noqa: ANN401
626)     ) -> Any:  # noqa: ANN401
627)         """"""  # noqa: D419
628)         # Coverage testing is done with the `click.testing` module,
629)         # which does not use the `__call__` shortcut.  So it is normal
630)         # that this function is never called, and thus should be
631)         # excluded from coverage.
632)         with (
633)             StandardCLILogging.ensure_standard_logging(),
634)             StandardCLILogging.ensure_standard_warnings_logging(),
635)         ):
636)             return self.main(*args, **kwargs)
637) 
638) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

639) @click.group(
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

640)     context_settings={
641)         'help_option_names': ['-h', '--help'],
642)         'ignore_unknown_options': True,
643)         'allow_interspersed_args': False,
644)     },
645)     epilog=r"""
646)         Configuration is stored in a directory according to the
647)         DERIVEPASSPHRASE_PATH variable, which defaults to
648)         `~/.derivepassphrase` on UNIX-like systems and
649)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
Marco Ricci Fix minor typo, formatting...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 1 month ago

651)     invoke_without_command=True,
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

652)     cls=_TopLevelCLIEntryPoint,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

653) )
654) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

655) @standard_logging_options
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

658)     """Derive a strong passphrase, deterministically, from a master secret.
659) 
660)     Using a master secret, derive a passphrase for a named service,
661)     subject to constraints e.g. on passphrase length, allowed
662)     characters, etc.  The exact derivation depends on the selected
663)     derivation scheme.  For each scheme, it is computationally
664)     infeasible to discern the master secret from the derived passphrase.
665)     The derivations are also deterministic, given the same inputs, thus
666)     the resulting passphrases need not be stored explicitly.  The
667)     service name and constraints themselves also generally need not be
668)     kept secret, depending on the scheme.
669) 
670)     The currently implemented subcommands are "vault" (for the scheme
671)     used by vault) and "export" (for exporting foreign configuration
672)     data).  See the respective `--help` output for instructions.  If no
673)     subcommand is given, we default to "vault".
674) 
675)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
676)     in v1.0, the subcommand must be specified explicitly.\f
677) 
678)     This is a [`click`][CLICK]-powered command-line interface function,
679)     and not intended for programmatic use.  Call with arguments
680)     `['--help']` to see full documentation of the interface.  (See also
681)     [`click.testing.CliRunner`][] for controlled, programmatic
682)     invocation.)
683) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

685) 
686)     """  # noqa: D301
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

687)     logger = logging.getLogger(PROG_NAME)
688)     deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

689)     if ctx.invoked_subcommand is None:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

690)         deprecation.warning(
691)             'A subcommand will be required in v1.0. '
692)             'See --help for available subcommands.'
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

693)         )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

694)         logger.warning('Defaulting to subcommand "vault".')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

695)         # See definition of click.Group.invoke, non-chained case.
696)         with ctx:
697)             sub_ctx = derivepassphrase_vault.make_context(
698)                 'vault', ctx.args, parent=ctx
699)             )
700)             with sub_ctx:
701)                 return derivepassphrase_vault.invoke(sub_ctx)
702)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

703) 
704) 
705) # Exporter
706) # ========
707) 
708) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

709) @derivepassphrase.group(
710)     'export',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

711)     context_settings={
712)         'help_option_names': ['-h', '--help'],
713)         'ignore_unknown_options': True,
714)         'allow_interspersed_args': False,
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

715)     },
716)     invoke_without_command=True,
717)     cls=_DefaultToVaultGroup,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

718) )
719) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

720) @standard_logging_options
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 3 months ago

723)     """Export a foreign configuration to standard output.
724) 
725)     Read a foreign system configuration, extract all information from
726)     it, and export the resulting configuration to standard output.
727) 
728)     The only available subcommand is "vault", which implements the
729)     vault-native configuration scheme.  If no subcommand is given, we
730)     default to "vault".
731) 
732)     Deprecation notice: Defaulting to "vault" is deprecated.  Starting
733)     in v1.0, the subcommand must be specified explicitly.\f
734) 
735)     This is a [`click`][CLICK]-powered command-line interface function,
736)     and not intended for programmatic use.  Call with arguments
737)     `['--help']` to see full documentation of the interface.  (See also
738)     [`click.testing.CliRunner`][] for controlled, programmatic
739)     invocation.)
740) 
Marco Ricci Update all URLs to stable a...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

742) 
743)     """  # noqa: D301
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

744)     logger = logging.getLogger(PROG_NAME)
745)     deprecation = logging.getLogger(f'{PROG_NAME}.deprecation')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

746)     if ctx.invoked_subcommand is None:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

747)         deprecation.warning(
748)             'A subcommand will be required in v1.0. '
749)             'See --help for available subcommands.'
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

750)         )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

751)         logger.warning('Defaulting to subcommand "vault".')
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

752)         # See definition of click.Group.invoke, non-chained case.
753)         with ctx:
754)             sub_ctx = derivepassphrase_export_vault.make_context(
755)                 'vault', ctx.args, parent=ctx
756)             )
757)             # Constructing the subcontext above will usually already
758)             # lead to a click.UsageError, so this block typically won't
759)             # actually be called.
760)             with sub_ctx:  # pragma: no cover
761)                 return derivepassphrase_export_vault.invoke(sub_ctx)
762)     return None
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

763) 
764) 
765) def _load_data(
766)     fmt: Literal['v0.2', 'v0.3', 'storeroom'],
767)     path: str | bytes | os.PathLike[str],
768)     key: bytes,
769) ) -> Any:  # noqa: ANN401
770)     contents: bytes
771)     module: types.ModuleType
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

772)     # Use match/case here once Python 3.9 becomes unsupported.
773)     if fmt == 'v0.2':
774)         module = importlib.import_module(
775)             'derivepassphrase.exporter.vault_native'
776)         )
777)         if module.STUBBED:
778)             raise ModuleNotFoundError
779)         with open(path, 'rb') as infile:
780)             contents = base64.standard_b64decode(infile.read())
781)         return module.export_vault_native_data(
782)             contents, key, try_formats=['v0.2']
783)         )
784)     elif fmt == 'v0.3':  # noqa: RET505
785)         module = importlib.import_module(
786)             'derivepassphrase.exporter.vault_native'
787)         )
788)         if module.STUBBED:
789)             raise ModuleNotFoundError
790)         with open(path, 'rb') as infile:
791)             contents = base64.standard_b64decode(infile.read())
792)         return module.export_vault_native_data(
793)             contents, key, try_formats=['v0.3']
794)         )
795)     elif fmt == 'storeroom':
796)         module = importlib.import_module('derivepassphrase.exporter.storeroom')
797)         if module.STUBBED:
798)             raise ModuleNotFoundError
799)         return module.export_storeroom_data(path, key)
800)     else:  # pragma: no cover
801)         assert_never(fmt)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

802) 
803) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

804) @derivepassphrase_export.command(
805)     'vault',
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

806)     context_settings={'help_option_names': ['-h', '--help']},
807) )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

808) @standard_logging_options
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

809) @click.option(
810)     '-f',
811)     '--format',
812)     'formats',
813)     metavar='FMT',
814)     multiple=True,
815)     default=('v0.3', 'v0.2', 'storeroom'),
816)     type=click.Choice(['v0.2', 'v0.3', 'storeroom']),
817)     help='try the following storage formats, in order (default: v0.3, v0.2)',
818) )
819) @click.option(
820)     '-k',
821)     '--key',
822)     metavar='K',
823)     help=(
824)         'use K as the storage master key '
825)         '(default: check the `VAULT_KEY`, `LOGNAME`, `USER` or '
826)         '`USERNAME` environment variables)'
827)     ),
828) )
829) @click.argument('path', metavar='PATH', required=True)
830) @click.pass_context
831) def derivepassphrase_export_vault(
832)     ctx: click.Context,
833)     /,
834)     *,
835)     path: str | bytes | os.PathLike[str],
836)     formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),
837)     key: str | bytes | None = None,
838) ) -> None:
839)     """Export a vault-native configuration to standard output.
840) 
841)     Read the vault-native configuration at PATH, extract all information
842)     from it, and export the resulting configuration to standard output.
843)     Depending on the configuration format, PATH may either be a file or
844)     a directory.  Supports the vault "v0.2", "v0.3" and "storeroom"
845)     formats.
846) 
847)     If PATH is explicitly given as `VAULT_PATH`, then use the
848)     `VAULT_PATH` environment variable to determine the correct path.
849)     (Use `./VAULT_PATH` or similar to indicate a file/directory actually
850)     named `VAULT_PATH`.)
851) 
852)     """
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

853)     logger = logging.getLogger(PROG_NAME)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

854)     if path in {'VAULT_PATH', b'VAULT_PATH'}:
855)         path = exporter.get_vault_path()
856)     if key is None:
857)         key = exporter.get_vault_key()
858)     elif isinstance(key, str):  # pragma: no branch
859)         key = key.encode('utf-8')
860)     for fmt in formats:
861)         try:
862)             config = _load_data(fmt, path, key)
863)         except (
864)             IsADirectoryError,
865)             NotADirectoryError,
866)             ValueError,
867)             RuntimeError,
868)         ):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

869)             logger.info('Cannot load as %s: %s', fmt, path)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

870)             continue
871)         except OSError as exc:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

872)             logger.error(
873)                 'Cannot parse %r as a valid config: %s: %r',
874)                 path,
875)                 exc.strerror,
876)                 exc.filename,
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

877)             )
878)             ctx.exit(1)
879)         except ModuleNotFoundError:
880)             # TODO(the-13th-letter): Use backslash continuation.
881)             # https://github.com/nedbat/coveragepy/issues/1836
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

882)             logger.error(
883)                 'Cannot load the required Python module "cryptography".'
884)             )
885)             logger.info('pip users: see the "export" extra.')
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

886)             ctx.exit(1)
887)         else:
888)             if not _types.is_vault_config(config):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

889)                 logger.error('Invalid vault config: %r', config)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

890)                 ctx.exit(1)
891)             click.echo(json.dumps(config, indent=2, sort_keys=True))
892)             break
893)     else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

894)         logger.error('Cannot parse %r as a valid config.', path)
Marco Ricci Reintegrate all functionali...

Marco Ricci authored 3 months ago

895)         ctx.exit(1)
896) 
897) 
898) # Vault
899) # =====
900) 
901) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

902) def _config_filename(
903)     subsystem: str | None = 'settings',
904) ) -> str | bytes | pathlib.Path:
905)     """Return the filename of the configuration file for the subsystem.
906) 
907)     The (implicit default) file is currently named `settings.json`,
908)     located within the configuration directory as determined by the
909)     `DERIVEPASSPHRASE_PATH` environment variable, or by
910)     [`click.get_app_dir`][] in POSIX mode.  Depending on the requested
911)     subsystem, this will usually be a different file within that
912)     directory.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

913) 
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

914)     Args:
915)         subsystem:
916)             Name of the configuration subsystem whose configuration
917)             filename to return.  If not given, return the old filename
918)             from before the subcommand migration.  If `None`, return the
919)             configuration directory instead.
920) 
921)     Raises:
922)         AssertionError:
923)             An unknown subsystem was passed.
924) 
925)     Deprecated:
926)         Since v0.2.0: The implicit default subsystem and the old
927)         configuration filename are deprecated, and will be removed in v1.0.
928)         The subsystem will be mandatory to specify.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

929) 
930)     """
931)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

935)     # Use match/case here once Python 3.9 becomes unsupported.
936)     if subsystem is None:
937)         return path
938)     elif subsystem in {'vault', 'settings'}:  # noqa: RET505
939)         filename = f'{subsystem}.json'
940)     else:  # pragma: no cover
941)         msg = f'Unknown configuration subsystem: {subsystem!r}'
942)         raise AssertionError(msg)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

944) 
945) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

951) 
952)     Returns:
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

954) 
955)     Raises:
956)         OSError:
957)             There was an OS error accessing the file.
958)         ValueError:
959)             The data loaded from the file is not a vault(1)-compatible
960)             config.
961) 
962)     """
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

968)     return data
969) 
970) 
Marco Ricci Permit one flaky test and f...

Marco Ricci authored 3 months ago

971) def _migrate_and_load_old_config() -> tuple[
972)     _types.VaultConfig, OSError | None
973) ]:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

979) 
980)     Returns:
981)         The vault settings, and an optional exception encountered during
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 3 months ago

984) 
985)     Raises:
986)         OSError:
987)             There was an OS error accessing the old file.
988)         ValueError:
989)             The data loaded from the file is not a vault(1)-compatible
990)             config.
991) 
992)     """
993)     new_filename = _config_filename(subsystem='vault')
994)     old_filename = _config_filename()
995)     with open(old_filename, 'rb') as fileobj:
996)         data = json.load(fileobj)
997)     if not _types.is_vault_config(data):
998)         raise ValueError(_INVALID_VAULT_CONFIG)
999)     try:
1000)         os.replace(old_filename, new_filename)
1001)     except OSError as exc:
1002)         return data, exc
1003)     else:
1004)         return data, None
1005) 
1006) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1009) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1012) 
1013)     Args:
1014)         config:
1015)             vault configuration to save.
1016) 
1017)     Raises:
1018)         OSError:
1019)             There was an OS error accessing or writing the file.
1020)         ValueError:
1021)             The data cannot be stored as a vault(1)-compatible config.
1022) 
1023)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1027)     filedir = os.path.dirname(os.path.abspath(filename))
1028)     try:
1029)         os.makedirs(filedir, exist_ok=False)
1030)     except FileExistsError:
1031)         if not os.path.isdir(filedir):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1034)         json.dump(config, fileobj)
1035) 
1036) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1044) 
1045)     Args:
1046)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1049) 
1050)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1053) 
1054)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1055)         KeyError:
1056)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
1057)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1072) 
1073)     """
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 1 month ago

1079)         suitable_keys = copy.copy(all_key_comment_pairs)
1080)         for pair in all_key_comment_pairs:
1081)             key, _comment = pair
1082)             if vault.Vault.is_suitable_ssh_key(key, client=client):
1083)                 yield pair
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1086) 
1087) 
1088) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1091)     single_choice_prompt: str = 'Confirm this choice?',
1092) ) -> int:
1093)     """Prompt user for a choice among the given items.
1094) 
1095)     Print the heading, if any, then present the items to the user.  If
1096)     there are multiple items, prompt the user for a selection, validate
1097)     the choice, then return the list index of the selected item.  If
1098)     there is only a single item, request confirmation for that item
1099)     instead, and return the correct index.
1100) 
1101)     Args:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1104)         heading:
1105)             A heading for the list of items, to print immediately
1106)             before.  Defaults to a reasonable standard heading.  If
1107)             explicitly empty, print no heading.
1108)         single_choice_prompt:
1109)             The confirmation prompt if there is only a single possible
1110)             choice.  Defaults to a reasonable standard prompt.
1111) 
1112)     Returns:
1113)         An index into the items sequence, indicating the user's
1114)         selection.
1115) 
1116)     Raises:
1117)         IndexError:
1118)             The user made an invalid or empty selection, or requested an
1119)             abort.
1120) 
1121)     """
1122)     n = len(items)
1123)     if heading:
1124)         click.echo(click.style(heading, bold=True))
1125)     for i, x in enumerate(items, start=1):
1126)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
1127)         click.echo(' ', nl=False)
1128)         click.echo(x)
1129)     if n > 1:
1130)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
1131)         choice = click.prompt(
1132)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1133)             err=True,
1134)             type=choices,
1135)             show_choices=False,
1136)             show_default=False,
1137)             default='',
1138)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1142)     prompt_suffix = (
1143)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
1144)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1145)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1146)         click.confirm(
1147)             single_choice_prompt,
1148)             prompt_suffix=prompt_suffix,
1149)             err=True,
1150)             abort=True,
1151)             default=False,
1152)             show_default=False,
1153)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1154)     except click.Abort:
1155)         raise IndexError(_EMPTY_SELECTION) from None
1156)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1157) 
1158) 
1159) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1167) 
1168)     Args:
1169)         conn:
Marco Ricci Support one-off SSH agent c...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

1172) 
1173)     Returns:
1174)         The selected SSH key.
1175) 
1176)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1177)         KeyError:
1178)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
1179)             variable was not found.
Marco Ricci Fail gracefully if UNIX dom...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1187)         IndexError:
1188)             The user made an invalid or empty selection, or requested an
1189)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1197)     """
1198)     suitable_keys = list(_get_suitable_ssh_keys(conn))
1199)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

1204)         remaining_key_display_length = KEY_DISPLAY_LENGTH - 1 - len(keytype)
1205)         key_extract = min(
1206)             key_str,
1207)             '...' + key_str[-remaining_key_display_length:],
1208)             key=len,
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1213)         key_listing,
1214)         heading='Suitable SSH keys:',
1215)         single_choice_prompt='Use this key?',
1216)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1217)     return suitable_keys[choice].key
1218) 
1219) 
1220) def _prompt_for_passphrase() -> str:
1221)     """Interactively prompt for the passphrase.
1222) 
1223)     Calls [`click.prompt`][] internally.  Moved into a separate function
1224)     mainly for testing/mocking purposes.
1225) 
1226)     Returns:
1227)         The user input.
1228) 
1229)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

1230)     return cast(
1231)         str,
1232)         click.prompt(
1233)             'Passphrase',
1234)             default='',
1235)             hide_input=True,
1236)             show_default=False,
1237)             err=True,
1238)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1240) 
1241) 
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1242) class _ORIGIN(enum.Enum):
1243)     INTERACTIVE: str = 'interactive'
1244) 
1245) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1248)     value: dict[str, Any],
1249)     *,
1250)     form: Literal['NFC', 'NFD', 'NFKC', 'NFKD'] = 'NFC',
1251) ) -> None:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1252)     logger = logging.getLogger(PROG_NAME)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1253)     if 'phrase' in value:
1254)         phrase = value['phrase']
1255)         if not unicodedata.is_normalized(form, phrase):
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1256)             formatted_key = (
1257)                 key.value
1258)                 if isinstance(key, _ORIGIN)
1259)                 else _types.json_path(key)
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1260)             )
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1261)             logger.warning(
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1262)                 (
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1263)                     'the %s passphrase is not %s-normalized. '
1264)                     'Make sure to double-check this is really the '
1265)                     'passphrase you want.'
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1266)                 ),
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1267)                 formatted_key,
1268)                 form,
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

1269)             )
1270) 
1271) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1282) 
1283) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1291) 
1292) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1299)     """
1300) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1301) 
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1302) class CompatibilityOption(OptionGroupOption):
1303)     """Compatibility and incompatibility options for the CLI."""
1304) 
1305)     option_group_name = 'Options concerning compatibility with other tools'
1306) 
1307) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1308) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1313)     """Check that the occurrence constraint is valid (int, 0 or larger).
1314) 
1315)     Args:
1316)         ctx: The `click` context.
1317)         param: The current command-line parameter.
1318)         value: The parameter value to be checked.
1319) 
1320)     Returns:
1321)         The parsed parameter value.
1322) 
1323)     Raises:
1324)         click.BadParameter: The parameter value is invalid.
1325) 
1326)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1329)     if value is None:
1330)         return value
1331)     if isinstance(value, int):
1332)         int_value = value
1333)     else:
1334)         try:
1335)             int_value = int(value, 10)
1336)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1342)     return int_value
1343) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1344) 
1345) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1350)     """Check that the length is valid (int, 1 or larger).
1351) 
1352)     Args:
1353)         ctx: The `click` context.
1354)         param: The current command-line parameter.
1355)         value: The parameter value to be checked.
1356) 
1357)     Returns:
1358)         The parsed parameter value.
1359) 
1360)     Raises:
1361)         click.BadParameter: The parameter value is invalid.
1362) 
1363)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1366)     if value is None:
1367)         return value
1368)     if isinstance(value, int):
1369)         int_value = value
1370)     else:
1371)         try:
1372)             int_value = int(value, 10)
1373)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1379)     return int_value
1380) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1381) 
1382) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1383) # Enter notes below the line with the cut mark (ASCII scissors and
1384) # dashes).  Lines above the cut mark (such as this one) will be ignored.
1385) #
1386) # If you wish to clear the notes, leave everything beyond the cut mark
1387) # blank.  However, if you leave the *entire* file blank, also removing
1388) # the cut mark, then the edit is aborted, and the old notes contents are
1389) # retained.
1390) #
1391) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1393) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
1394) 
1395) 
Marco Ricci Reimplement deprecated subc...

Marco Ricci authored 1 month ago

1396) @derivepassphrase.command(
1397)     'vault',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1409)     """,
1410) )
1411) @click.option(
1412)     '-p',
1413)     '--phrase',
1414)     'use_phrase',
1415)     is_flag=True,
1416)     help='prompts you for your passphrase',
1417)     cls=PasswordGenerationOption,
1418) )
1419) @click.option(
1420)     '-k',
1421)     '--key',
1422)     'use_key',
1423)     is_flag=True,
1424)     help='uses your SSH private key to generate passwords',
1425)     cls=PasswordGenerationOption,
1426) )
1427) @click.option(
1428)     '-l',
1429)     '--length',
1430)     metavar='NUMBER',
1431)     callback=_validate_length,
1432)     help='emits password of length NUMBER',
1433)     cls=PasswordGenerationOption,
1434) )
1435) @click.option(
1436)     '-r',
1437)     '--repeat',
1438)     metavar='NUMBER',
1439)     callback=_validate_occurrence_constraint,
1440)     help='allows maximum of NUMBER repeated adjacent chars',
1441)     cls=PasswordGenerationOption,
1442) )
1443) @click.option(
1444)     '--lower',
1445)     metavar='NUMBER',
1446)     callback=_validate_occurrence_constraint,
1447)     help='includes at least NUMBER lowercase letters',
1448)     cls=PasswordGenerationOption,
1449) )
1450) @click.option(
1451)     '--upper',
1452)     metavar='NUMBER',
1453)     callback=_validate_occurrence_constraint,
1454)     help='includes at least NUMBER uppercase letters',
1455)     cls=PasswordGenerationOption,
1456) )
1457) @click.option(
1458)     '--number',
1459)     metavar='NUMBER',
1460)     callback=_validate_occurrence_constraint,
1461)     help='includes at least NUMBER digits',
1462)     cls=PasswordGenerationOption,
1463) )
1464) @click.option(
1465)     '--space',
1466)     metavar='NUMBER',
1467)     callback=_validate_occurrence_constraint,
1468)     help='includes at least NUMBER spaces',
1469)     cls=PasswordGenerationOption,
1470) )
1471) @click.option(
1472)     '--dash',
1473)     metavar='NUMBER',
1474)     callback=_validate_occurrence_constraint,
1475)     help='includes at least NUMBER "-" or "_"',
1476)     cls=PasswordGenerationOption,
1477) )
1478) @click.option(
1479)     '--symbol',
1480)     metavar='NUMBER',
1481)     callback=_validate_occurrence_constraint,
1482)     help='includes at least NUMBER symbol chars',
1483)     cls=PasswordGenerationOption,
1484) )
1485) @click.option(
1486)     '-n',
1487)     '--notes',
1488)     'edit_notes',
1489)     is_flag=True,
1490)     help='spawn an editor to edit notes for SERVICE',
1491)     cls=ConfigurationOption,
1492) )
1493) @click.option(
1494)     '-c',
1495)     '--config',
1496)     'store_config_only',
1497)     is_flag=True,
1498)     help='saves the given settings for SERVICE or global',
1499)     cls=ConfigurationOption,
1500) )
1501) @click.option(
1502)     '-x',
1503)     '--delete',
1504)     'delete_service_settings',
1505)     is_flag=True,
1506)     help='deletes settings for SERVICE',
1507)     cls=ConfigurationOption,
1508) )
1509) @click.option(
1510)     '--delete-globals',
1511)     is_flag=True,
1512)     help='deletes the global shared settings',
1513)     cls=ConfigurationOption,
1514) )
1515) @click.option(
1516)     '-X',
1517)     '--clear',
1518)     'clear_all_settings',
1519)     is_flag=True,
1520)     help='deletes all settings',
1521)     cls=ConfigurationOption,
1522) )
1523) @click.option(
1524)     '-e',
1525)     '--export',
1526)     'export_settings',
1527)     metavar='PATH',
1528)     help='export all saved settings into file PATH',
1529)     cls=StorageManagementOption,
1530) )
1531) @click.option(
1532)     '-i',
1533)     '--import',
1534)     'import_settings',
1535)     metavar='PATH',
1536)     help='import saved settings from file PATH',
1537)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1538) )
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1539) @click.option(
1540)     '--overwrite-existing/--merge-existing',
1541)     'overwrite_config',
1542)     default=False,
1543)     help='overwrite or merge (default) the existing configuration',
1544)     cls=CompatibilityOption,
1545) )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1546) @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1547) @standard_logging_options
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

1551)     ctx: click.Context,
1552)     /,
1553)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1554)     service: str | None = None,
1555)     use_phrase: bool = False,
1556)     use_key: bool = False,
1557)     length: int | None = None,
1558)     repeat: int | None = None,
1559)     lower: int | None = None,
1560)     upper: int | None = None,
1561)     number: int | None = None,
1562)     space: int | None = None,
1563)     dash: int | None = None,
1564)     symbol: int | None = None,
1565)     edit_notes: bool = False,
1566)     store_config_only: bool = False,
1567)     delete_service_settings: bool = False,
1568)     delete_globals: bool = False,
1569)     clear_all_settings: bool = False,
1570)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
1571)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1572)     overwrite_config: bool = False,
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1576)     Using a master passphrase or a master SSH key, derive a passphrase
1577)     for SERVICE, subject to length, character and character repetition
1578)     constraints.  The derivation is cryptographically strong, meaning
1579)     that even if a single passphrase is compromised, guessing the master
1580)     passphrase or a different service's passphrase is computationally
1581)     infeasible.  The derivation is also deterministic, given the same
1582)     inputs, thus the resulting passphrase need not be stored explicitly.
1583)     The service name and constraints themselves also need not be kept
1584)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

1596) 
1597)     Parameters:
1598)         ctx (click.Context):
1599)             The `click` context.
1600) 
1601)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1654)         export_settings:
1655)             Command-line argument `-e`/`--export`.  If a file object,
1656)             then it must be open for writing and accept `str` inputs.
1657)             Otherwise, a filename to open for writing.  Using `-` for
1658)             standard output is supported.
1659)         import_settings:
1660)             Command-line argument `-i`/`--import`.  If a file object, it
1661)             must be open for reading and yield `str` values.  Otherwise,
1662)             a filename to open for reading.  Using `-` for standard
1663)             input is supported.
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1664)         overwrite_config:
1665)             Command-line arguments `--overwrite-existing` (True) and
1666)             `--merge-existing` (False).  Controls whether config saving
1667)             and config importing overwrite existing configurations, or
1668)             merge them section-wise instead.
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

1670)     """  # noqa: D301
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1671)     logger = logging.getLogger(PROG_NAME)
1672)     deprecation = logging.getLogger(PROG_NAME + '.deprecation')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

1678)             # Use match/case here once Python 3.9 becomes unsupported.
1679)             if isinstance(param, PasswordGenerationOption):
1680)                 group = PasswordGenerationOption
1681)             elif isinstance(param, ConfigurationOption):
1682)                 group = ConfigurationOption
1683)             elif isinstance(param, StorageManagementOption):
1684)                 group = StorageManagementOption
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1685)             elif isinstance(param, LoggingOption):
1686)                 group = LoggingOption
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1687)             elif isinstance(param, CompatibilityOption):
1688)                 group = CompatibilityOption
Marco Ricci Add support for Python 3.9

Marco Ricci authored 3 months ago

1689)             elif isinstance(param, OptionGroupOption):
1690)                 raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1691)                     f'Unknown option group for {param!r}'  # noqa: EM102
1692)                 )
1693)             else:
1694)                 group = click.Option
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

1695)             options_in_group.setdefault(group, []).append(param)
1696)         params_by_str[param.human_readable_name] = param
1697)         for name in param.opts + param.secondary_opts:
1698)             params_by_str[name] = param
1699) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1703)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

1707)         if isinstance(param, str):
1708)             param = params_by_str[param]
1709)         assert isinstance(param, click.Parameter)
1710)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

1711)             return
1712)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1715)             assert isinstance(other, click.Parameter)
1716)             if other != param and is_param_set(other):
1717)                 opt_str = param.opts[0]
1718)                 other_str = other.opts[0]
1719)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1722) 
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1723)     def err(msg: Any, *args: Any, **kwargs: Any) -> NoReturn:  # noqa: ANN401
1724)         stacklevel = kwargs.pop('stacklevel', 1)
1725)         stacklevel += 1
1726)         logger.error(msg, *args, stacklevel=stacklevel, **kwargs)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1727)         ctx.exit(1)
1728) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1730)         try:
1731)             return _load_config()
1732)         except FileNotFoundError:
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1733)             try:
1734)                 backup_config, exc = _migrate_and_load_old_config()
1735)             except FileNotFoundError:
1736)                 return {'services': {}}
1737)             old_name = os.path.basename(_config_filename())
1738)             new_name = os.path.basename(_config_filename(subsystem='vault'))
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1739)             deprecation.warning(
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1740)                 (
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1741)                     'Using deprecated v0.1-style config file %r, '
1742)                     'instead of v0.2-style %r.  '
1743)                     'Support for v0.1-style config filenames will be '
1744)                     'removed in v1.0.'
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1745)                 ),
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1746)                 old_name,
1747)                 new_name,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1748)             )
1749)             if isinstance(exc, OSError):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1750)                 logger.warning(
1751)                     'Failed to migrate to %r: %s: %r',
1752)                     new_name,
1753)                     exc.strerror,
1754)                     exc.filename,
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1755)                 )
1756)             else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1757)                 logger.info('Successfully migrated to %r.', new_name)
Marco Ricci Rename the configuration fi...

Marco Ricci authored 3 months ago

1758)             return backup_config
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1759)         except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1760)             err('Cannot load config: %s: %r', e.strerror, e.filename)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

1761)         except Exception as e:  # noqa: BLE001
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1762)             err('Cannot load config: %s', str(e), exc_info=e)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1763) 
1764)     def put_config(config: _types.VaultConfig, /) -> None:
1765)         try:
1766)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

1767)         except OSError as exc:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1768)             err('Cannot store config: %s: %r', exc.strerror, exc.filename)
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

1769)         except Exception as exc:  # noqa: BLE001
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1770)             err('Cannot store config: %s', str(exc), exc_info=exc)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

1773) 
1774)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1779)                     opt, *options_in_group[PasswordGenerationOption]
1780)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1785)                 opt,
1786)                 *options_in_group[ConfigurationOption],
1787)                 *options_in_group[StorageManagementOption],
1788)             )
Marco Ricci Correctly model vault globa...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 2 months ago

1796)             raise click.UsageError(msg)  # noqa: DOC501
1797)     sv_options = [params_by_str['--notes'], params_by_str['--delete']]
1798)     for param in sv_options:
1799)         if is_param_set(param) and not service:
1800)             opt_str = param.opts[0]
1801)             msg = f'{opt_str} requires a SERVICE'
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

1803)     no_sv_options = [
1804)         params_by_str['--delete-globals'],
1805)         params_by_str['--clear'],
1806)         *options_in_group[StorageManagementOption],
1807)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

1814)     if service == '':  # noqa: PLC1901
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1815)         logger.warning(
1816)             'An empty SERVICE is not supported by vault(1).  '
1817)             'For compatibility, this will be treated as if SERVICE '
1818)             'was not supplied, i.e., it will error out, or '
1819)             'operate on global settings.'
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1820)         )
1821) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1822)     if edit_notes:
1823)         assert service is not None
1824)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1831)             while notes_lines:
1832)                 line = notes_lines.popleft()
1833)                 if line.startswith(DEFAULT_NOTES_MARKER):
1834)                     notes_value = ''.join(notes_lines)
1835)                     break
1836)             else:
1837)                 if not notes_value.strip():
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1840)                 notes_value.strip('\n')
1841)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1843)     elif delete_service_settings:
1844)         assert service is not None
1845)         configuration = get_config()
1846)         if service in configuration['services']:
1847)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1849)     elif delete_globals:
1850)         configuration = get_config()
1851)         if 'global' in configuration:
1852)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

1856)     elif import_settings:
1857)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1860)             infile = (
1861)                 cast(TextIO, import_settings)
1862)                 if hasattr(import_settings, 'close')
1863)                 else click.open_file(os.fspath(import_settings), 'rt')
1864)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1865)             with infile:
1866)                 maybe_config = json.load(infile)
1867)         except json.JSONDecodeError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1868)             err('Cannot load config: cannot decode JSON: %s', e)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1869)         except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1870)             err('Cannot load config: %s: %r', e.strerror, e.filename)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1871)         cleaned = _types.clean_up_falsy_vault_config_values(maybe_config)
1872)         if not _types.is_vault_config(maybe_config):
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1873)             err('Cannot load config: %s', _INVALID_VAULT_CONFIG)
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1874)         assert cleaned is not None
1875)         for step in cleaned:
1876)             # These are never fatal errors, because the semantics of
1877)             # vault upon encountering these settings are ill-specified,
1878)             # but not ill-defined.
1879)             if step.action == 'replace':
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1880)                 logger.warning(
1881)                     'Replacing invalid value %s for key %s with %s.',
1882)                     json.dumps(step.old_value),
1883)                     _types.json_path(step.path),
1884)                     json.dumps(step.new_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1885)                 )
1886)             else:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1887)                 logger.warning(
1888)                     'Removing ineffective setting %s = %s.',
1889)                     _types.json_path(step.path),
1890)                     json.dumps(step.old_value),
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 2 months ago

1892)         if '' in maybe_config['services']:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1893)             logger.warning(
1894)                 (
1895)                     'An empty SERVICE is not supported by vault(1), '
1896)                     'and the empty-string service settings will be '
1897)                     'inaccessible and ineffective.  To ensure that '
1898)                     'vault(1) and %s see the settings, move them '
1899)                     'into the "global" section.'
1900)                 ),
1901)                 PROG_NAME,
Marco Ricci Warn the user upon supplyin...

Marco Ricci authored 2 months ago

1902)             )
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

1903)         form = cast(
1904)             Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
1905)             maybe_config.get('global', {}).get(
1906)                 'unicode_normalization_form', 'NFC'
1907)             ),
1908)         )
1909)         assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
1910)         _check_for_misleading_passphrase(
1911)             ('global',),
1912)             cast(dict[str, Any], maybe_config.get('global', {})),
1913)             form=form,
1914)         )
1915)         for key, value in maybe_config['services'].items():
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

1919)                 form=form,
1920)             )
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

1921)         if overwrite_config:
1922)             put_config(maybe_config)
1923)         else:
1924)             configuration = get_config()
1925)             merged_config: collections.ChainMap[str, Any] = (
1926)                 collections.ChainMap(
1927)                     {
1928)                         'services': collections.ChainMap(
1929)                             maybe_config['services'],
1930)                             configuration['services'],
1931)                         ),
1932)                     },
1933)                     {'global': maybe_config['global']}
1934)                     if 'global' in maybe_config
1935)                     else {},
1936)                     {'global': configuration['global']}
1937)                     if 'global' in configuration
1938)                     else {},
1939)                 )
1940)             )
1941)             new_config: Any = {
1942)                 k: dict(v) if isinstance(v, collections.ChainMap) else v
1943)                 for k, v in sorted(merged_config.items())
1944)             }
1945)             assert _types.is_vault_config(new_config)
1946)             put_config(new_config)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1947)     elif export_settings:
1948)         configuration = get_config()
1949)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

1952)             outfile = (
1953)                 cast(TextIO, export_settings)
1954)                 if hasattr(export_settings, 'close')
1955)                 else click.open_file(os.fspath(export_settings), 'wt')
1956)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1957)             with outfile:
1958)                 json.dump(configuration, outfile)
1959)         except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

1960)             err('Cannot store config: %s: %r', e.strerror, e.filename)
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1968)         service_keys = {
1969)             'key',
1970)             'phrase',
1971)             'length',
1972)             'repeat',
1973)             'lower',
1974)             'upper',
1975)             'number',
1976)             'space',
1977)             'dash',
1978)             'symbol',
1979)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

1981)             {
1982)                 k: v
1983)                 for k, v in locals().items()
1984)                 if k in service_keys and v is not None
1985)             },
1986)             cast(
1987)                 dict[str, Any],
1988)                 configuration['services'].get(service or '', {}),
1989)             ),
1990)             cast(dict[str, Any], configuration.get('global', {})),
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

1991)         )
1992)         if use_key:
1993)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

1994)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1995)                     'ASCII'
1996)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 3 months ago

2001)             except NotImplementedError:
2002)                 err(
2003)                     'Cannot connect to SSH agent because '
2004)                     'this Python version does not support UNIX domain sockets'
2005)                 )
Marco Ricci Document and handle other e...

Marco Ricci authored 4 months ago

2006)             except OSError as e:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2007)                 err('Cannot connect to SSH agent: %s', e.strerror)
Marco Ricci Add a specific error class...

Marco Ricci authored 4 months ago

2008)             except (
2009)                 LookupError,
2010)                 RuntimeError,
2011)                 ssh_agent.SSHAgentFailedError,
2012)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2014)         elif use_phrase:
2015)             maybe_phrase = _prompt_for_passphrase()
2016)             if not maybe_phrase:
Marco Ricci Fix error message capitaliz...

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2018)             else:
2019)                 phrase = maybe_phrase
2020)         if store_config_only:
2021)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

2022)             view = (
2023)                 collections.ChainMap(*settings.maps[:2])
2024)                 if service
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2027)             if use_key:
2028)                 view['key'] = key
2029)             elif use_phrase:
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 4 months ago

2032)                 _check_for_misleading_passphrase(
2033)                     ('services', service) if service else ('global',),
2034)                     {'phrase': phrase},
2035)                 )
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

2036)                 if 'key' in settings:
Marco Ricci Use the logging system to e...

Marco Ricci authored 1 month ago

2037)                     logger.warning(
2038)                         (
2039)                             'Setting a %s passphrase is ineffective '
2040)                             'because a key is also set.'
2041)                         ),
2042)                         settings_type,
Marco Ricci Fix missing consideration o...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2048)                     f'actual settings'
2049)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

2050)                 raise click.UsageError(msg)
Marco Ricci Allow the user to overwrite...

Marco Ricci authored 4 weeks ago

2051)             subtree: dict[str, Any] = (
2052)                 configuration['services'].setdefault(service, {})  # type: ignore[assignment]
2053)                 if service
2054)                 else configuration.setdefault('global', {})
2055)             )
2056)             if overwrite_config:
2057)                 subtree.clear()
2058)             subtree.update(view)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 6 months ago

2063)         else:
2064)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2067)             kwargs: dict[str, Any] = {
2068)                 k: v
2069)                 for k, v in settings.items()
2070)                 if k in service_keys and v is not None
2071)             }
2072) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 5 months ago

2073)             def key_to_phrase(
2074)                 key: str | bytes | bytearray,
2075)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

2077)                     base64.standard_b64decode(key)
2078)                 )
2079) 
Marco Ricci Allow all textual strings,...

Marco Ricci authored 4 months ago

2080)             if use_phrase:
2081)                 form = cast(
2082)                     Literal['NFC', 'NFD', 'NFKC', 'NFKD'],
2083)                     configuration.get('global', {}).get(
2084)                         'unicode_normalization_form', 'NFC'
2085)                     ),
2086)                 )
2087)                 assert form in {'NFC', 'NFD', 'NFKC', 'NFKD'}
2088)                 _check_for_misleading_passphrase(
Marco Ricci Signal and list falsy value...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 4 months ago

2090)                 )
2091) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 3 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

2102)             elif kwargs.get('phrase'):
2103)                 pass
2104)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 5 months ago

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

Marco Ricci authored 4 months ago

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

Marco Ricci authored 5 months ago

2107)                     'or in configuration'
2108)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

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