4eb85f64c8f18a55eb83c3909322a78b11ba6c87
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

12) import copy
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

13) import inspect
14) import json
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

15) import os
16) import socket
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

17) from typing import (
18)     TYPE_CHECKING,
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

20)     TextIO,
21)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

23) 
24) import click
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

25) from typing_extensions import (
26)     Any,
27)     assert_never,
28) )
29) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

30) import derivepassphrase as dpp
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

32) 
33) if TYPE_CHECKING:
34)     import pathlib
35)     from collections.abc import (
36)         Iterator,
37)         Sequence,
38)     )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

39) 
40) __author__ = dpp.__author__
41) __version__ = dpp.__version__
42) 
43) __all__ = ('derivepassphrase',)
44) 
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

53) 
54) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

55) def _config_filename() -> str | bytes | pathlib.Path:
56)     """Return the filename of the configuration file.
57) 
58)     The file is currently named `settings.json`, located within the
59)     configuration directory as determined by the `DERIVEPASSPHRASE_PATH`
60)     environment variable, or by [`click.get_app_dir`][] in POSIX
61)     mode.
62) 
63)     """
64)     path: str | bytes | pathlib.Path
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

68)     return os.path.join(path, 'settings.json')
69) 
70) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

72)     """Load a vault(1)-compatible config from the application directory.
73) 
74)     The filename is obtained via
75)     [`derivepassphrase.cli._config_filename`][].  This must be an
76)     unencrypted JSON file.
77) 
78)     Returns:
79)         The vault settings.  See
80)         [`derivepassphrase.types.VaultConfig`][] for details.
81) 
82)     Raises:
83)         OSError:
84)             There was an OS error accessing the file.
85)         ValueError:
86)             The data loaded from the file is not a vault(1)-compatible
87)             config.
88) 
89)     """
90)     filename = _config_filename()
91)     with open(filename, 'rb') as fileobj:
92)         data = json.load(fileobj)
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

95)     return data
96) 
97) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

100) 
101)     The filename is obtained via
102)     [`derivepassphrase.cli._config_filename`][].  The config will be
103)     stored as an unencrypted JSON file.
104) 
105)     Args:
106)         config:
107)             vault configuration to save.
108) 
109)     Raises:
110)         OSError:
111)             There was an OS error accessing or writing the file.
112)         ValueError:
113)             The data cannot be stored as a vault(1)-compatible config.
114) 
115)     """
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

119)     filedir = os.path.dirname(os.path.abspath(filename))
120)     try:
121)         os.makedirs(filedir, exist_ok=False)
122)     except FileExistsError:
123)         if not os.path.isdir(filedir):
124)             raise
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

126)         json.dump(config, fileobj)
127) 
128) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

136) 
137)     Args:
138)         conn:
139)             An optional connection hint to the SSH agent; specifically,
140)             an SSH agent client, or a socket connected to an SSH agent.
141) 
142)             If an existing SSH agent client, then this client will be
143)             queried for the SSH keys, and otherwise left intact.
144) 
145)             If a socket, then a one-shot client will be constructed
146)             based on the socket to query the agent, and deconstructed
147)             afterwards.
148) 
149)             If neither are given, then the agent's socket location is
150)             looked up in the `SSH_AUTH_SOCK` environment variable, and
151)             used to construct/deconstruct a one-shot client, as in the
152)             previous case.
153) 
154)     Yields:
155)         :
156)             Every SSH key from the SSH agent that is suitable for
157)             passphrase derivation.
158) 
159)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

160)         KeyError:
161)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
162)             variable was not found.
163)         OSError:
164)             `conn` was a socket or `None`, and there was an error
165)             setting up a socket connection to the agent.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

173) 
174)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

179)             client = conn
180)             client_context = contextlib.nullcontext()
181)         case socket.socket() | None:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

183)             client_context = client
184)         case _:  # pragma: no cover
185)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

186)             msg = f'invalid connection hint: {conn!r}'
187)             raise TypeError(msg)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

188)     with client_context:
189)         try:
190)             all_key_comment_pairs = list(client.list_keys())
191)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

200) 
201) 
202) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

205)     single_choice_prompt: str = 'Confirm this choice?',
206) ) -> int:
207)     """Prompt user for a choice among the given items.
208) 
209)     Print the heading, if any, then present the items to the user.  If
210)     there are multiple items, prompt the user for a selection, validate
211)     the choice, then return the list index of the selected item.  If
212)     there is only a single item, request confirmation for that item
213)     instead, and return the correct index.
214) 
215)     Args:
216)         heading:
217)             A heading for the list of items, to print immediately
218)             before.  Defaults to a reasonable standard heading.  If
219)             explicitly empty, print no heading.
220)         single_choice_prompt:
221)             The confirmation prompt if there is only a single possible
222)             choice.  Defaults to a reasonable standard prompt.
223) 
224)     Returns:
225)         An index into the items sequence, indicating the user's
226)         selection.
227) 
228)     Raises:
229)         IndexError:
230)             The user made an invalid or empty selection, or requested an
231)             abort.
232) 
233)     """
234)     n = len(items)
235)     if heading:
236)         click.echo(click.style(heading, bold=True))
237)     for i, x in enumerate(items, start=1):
238)         click.echo(click.style(f'[{i}]', bold=True), nl=False)
239)         click.echo(' ', nl=False)
240)         click.echo(x)
241)     if n > 1:
242)         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
243)         choice = click.prompt(
244)             f'Your selection? (1-{n}, leave empty to abort)',
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

245)             err=True,
246)             type=choices,
247)             show_choices=False,
248)             show_default=False,
249)             default='',
250)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

254)     prompt_suffix = (
255)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
256)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

257)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

258)         click.confirm(
259)             single_choice_prompt,
260)             prompt_suffix=prompt_suffix,
261)             err=True,
262)             abort=True,
263)             default=False,
264)             show_default=False,
265)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

266)     except click.Abort:
267)         raise IndexError(_EMPTY_SELECTION) from None
268)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

269) 
270) 
271) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

278)     prompted interactively (see [`click.prompt`][]) for a selection.
279) 
280)     Args:
281)         conn:
282)             An optional connection hint to the SSH agent; specifically,
283)             an SSH agent client, or a socket connected to an SSH agent.
284) 
285)             If an existing SSH agent client, then this client will be
286)             queried for the SSH keys, and otherwise left intact.
287) 
288)             If a socket, then a one-shot client will be constructed
289)             based on the socket to query the agent, and deconstructed
290)             afterwards.
291) 
292)             If neither are given, then the agent's socket location is
293)             looked up in the `SSH_AUTH_SOCK` environment variable, and
294)             used to construct/deconstruct a one-shot client, as in the
295)             previous case.
296) 
297)     Returns:
298)         The selected SSH key.
299) 
300)     Raises:
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

301)         KeyError:
302)             `conn` was `None`, and the `SSH_AUTH_SOCK` environment
303)             variable was not found.
304)         OSError:
305)             `conn` was a socket or `None`, and there was an error
306)             setting up a socket connection to the agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

307)         IndexError:
308)             The user made an invalid or empty selection, or requested an
309)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

317)     """
318)     suitable_keys = list(_get_suitable_ssh_keys(conn))
319)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

324)         key_prefix = (
325)             key_str
326)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
327)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
328)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

332)         key_listing,
333)         heading='Suitable SSH keys:',
334)         single_choice_prompt='Use this key?',
335)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

336)     return suitable_keys[choice].key
337) 
338) 
339) def _prompt_for_passphrase() -> str:
340)     """Interactively prompt for the passphrase.
341) 
342)     Calls [`click.prompt`][] internally.  Moved into a separate function
343)     mainly for testing/mocking purposes.
344) 
345)     Returns:
346)         The user input.
347) 
348)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

349)     return cast(
350)         str,
351)         click.prompt(
352)             'Passphrase',
353)             default='',
354)             hide_input=True,
355)             show_default=False,
356)             err=True,
357)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

359) 
360) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

362)     """A [`click.Option`][] with an associated group name and group epilog.
363) 
364)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
365)     help sections.  Each subclass contains its own group name and
366)     epilog.
367) 
368)     Attributes:
369)         option_group_name:
370)             The name of the option group.  Used as a heading on the help
371)             text for options in this section.
372)         epilog:
373)             An epilog to print after listing the options in this
374)             section.
375) 
376)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

377) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

378)     option_group_name: str = ''
379)     epilog: str = ''
380) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

381)     def __init__(self, *args: Any, **kwargs: Any) -> None:
382)         if self.__class__ == __class__:  # type: ignore[name-defined]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

386) 
387) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

388)     """A [`click.Command`][] with support for help/option groups.
389) 
390)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
391)     further modified to support group epilogs.
392) 
393)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
394) 
395)     """
396) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

397)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

398)         self,
399)         ctx: click.Context,
400)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

404)         This is a callback for [`click.Command.get_help`][] that
405)         implements the `--help` listing, by calling appropriate methods
406)         of the `formatter`.  We list all options (like the base
407)         implementation), but grouped into sections according to the
408)         concrete [`click.Option`][] subclass being used.  If the option
409)         is an instance of some subclass `X` of
410)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
411)         heading and the epilog are taken from `X.option_group_name` and
412)         `X.epilog`; otherwise, the section heading is "Options" (or
413)         "Other options" if there are other option groups) and the epilog
414)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

415) 
416)         Args:
417)             ctx:
418)                 The click context.
419)             formatter:
420)                 The formatter for the `--help` listing.
421) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 2 months ago

422)         Returns:
423)             Nothing.  Output is generated by calling appropriate methods
424)             on `formatter` instead.
425) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

426)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

427)         help_records: dict[str, list[tuple[str, str]]] = {}
428)         epilogs: dict[str, str] = {}
429)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

430)         if (  # pragma: no branch
431)             (help_opt := self.get_help_option(ctx)) is not None
432)             and help_opt not in params
433)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

434)             params.append(help_opt)
435)         for param in params:
436)             rec = param.get_help_record(ctx)
437)             if rec is not None:
438)                 if isinstance(param, OptionGroupOption):
439)                     group_name = param.option_group_name
440)                     epilogs.setdefault(group_name, param.epilog)
441)                 else:
442)                     group_name = ''
443)                 help_records.setdefault(group_name, []).append(rec)
444)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

445)         default_group_name = (
446)             'Other Options' if len(default_group) > 1 else 'Options'
447)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

448)         help_records[default_group_name] = default_group
449)         for group_name, records in help_records.items():
450)             with formatter.section(group_name):
451)                 formatter.write_dl(records)
452)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
453)             if epilog:
454)                 formatter.write_paragraph()
455)                 with formatter.indentation():
456)                     formatter.write_text(epilog)
457) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

469) 
470) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

478) 
479) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

486)     """
487) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

488) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

489) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

490)     ctx: click.Context,
491)     param: click.Parameter,
492)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

493) ) -> int | None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

494)     """Check that the occurrence constraint is valid (int, 0 or larger)."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

497)     if value is None:
498)         return value
499)     if isinstance(value, int):
500)         int_value = value
501)     else:
502)         try:
503)             int_value = int(value, 10)
504)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

510)     return int_value
511) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

512) 
513) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

514)     ctx: click.Context,
515)     param: click.Parameter,
516)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

517) ) -> int | None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

518)     """Check that the length is valid (int, 1 or larger)."""
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

521)     if value is None:
522)         return value
523)     if isinstance(value, int):
524)         int_value = value
525)     else:
526)         try:
527)             int_value = int(value, 10)
528)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

534)     return int_value
535) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

536) 
537) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

538) # Enter notes below the line with the cut mark (ASCII scissors and
539) # dashes).  Lines above the cut mark (such as this one) will be ignored.
540) #
541) # If you wish to clear the notes, leave everything beyond the cut mark
542) # blank.  However, if you leave the *entire* file blank, also removing
543) # the cut mark, then the edit is aborted, and the old notes contents are
544) # retained.
545) #
546) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

548) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
549) 
550) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

551) @click.command(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

560) 
561)         Configuration is stored in a directory according to the
562)         DERIVEPASSPHRASE_PATH variable, which defaults to
563)         `~/.derivepassphrase` on UNIX-like systems and
564)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
565)         The configuration is NOT encrypted, and you are STRONGLY
566)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

567)     """,
568) )
569) @click.option(
570)     '-p',
571)     '--phrase',
572)     'use_phrase',
573)     is_flag=True,
574)     help='prompts you for your passphrase',
575)     cls=PasswordGenerationOption,
576) )
577) @click.option(
578)     '-k',
579)     '--key',
580)     'use_key',
581)     is_flag=True,
582)     help='uses your SSH private key to generate passwords',
583)     cls=PasswordGenerationOption,
584) )
585) @click.option(
586)     '-l',
587)     '--length',
588)     metavar='NUMBER',
589)     callback=_validate_length,
590)     help='emits password of length NUMBER',
591)     cls=PasswordGenerationOption,
592) )
593) @click.option(
594)     '-r',
595)     '--repeat',
596)     metavar='NUMBER',
597)     callback=_validate_occurrence_constraint,
598)     help='allows maximum of NUMBER repeated adjacent chars',
599)     cls=PasswordGenerationOption,
600) )
601) @click.option(
602)     '--lower',
603)     metavar='NUMBER',
604)     callback=_validate_occurrence_constraint,
605)     help='includes at least NUMBER lowercase letters',
606)     cls=PasswordGenerationOption,
607) )
608) @click.option(
609)     '--upper',
610)     metavar='NUMBER',
611)     callback=_validate_occurrence_constraint,
612)     help='includes at least NUMBER uppercase letters',
613)     cls=PasswordGenerationOption,
614) )
615) @click.option(
616)     '--number',
617)     metavar='NUMBER',
618)     callback=_validate_occurrence_constraint,
619)     help='includes at least NUMBER digits',
620)     cls=PasswordGenerationOption,
621) )
622) @click.option(
623)     '--space',
624)     metavar='NUMBER',
625)     callback=_validate_occurrence_constraint,
626)     help='includes at least NUMBER spaces',
627)     cls=PasswordGenerationOption,
628) )
629) @click.option(
630)     '--dash',
631)     metavar='NUMBER',
632)     callback=_validate_occurrence_constraint,
633)     help='includes at least NUMBER "-" or "_"',
634)     cls=PasswordGenerationOption,
635) )
636) @click.option(
637)     '--symbol',
638)     metavar='NUMBER',
639)     callback=_validate_occurrence_constraint,
640)     help='includes at least NUMBER symbol chars',
641)     cls=PasswordGenerationOption,
642) )
643) @click.option(
644)     '-n',
645)     '--notes',
646)     'edit_notes',
647)     is_flag=True,
648)     help='spawn an editor to edit notes for SERVICE',
649)     cls=ConfigurationOption,
650) )
651) @click.option(
652)     '-c',
653)     '--config',
654)     'store_config_only',
655)     is_flag=True,
656)     help='saves the given settings for SERVICE or global',
657)     cls=ConfigurationOption,
658) )
659) @click.option(
660)     '-x',
661)     '--delete',
662)     'delete_service_settings',
663)     is_flag=True,
664)     help='deletes settings for SERVICE',
665)     cls=ConfigurationOption,
666) )
667) @click.option(
668)     '--delete-globals',
669)     is_flag=True,
670)     help='deletes the global shared settings',
671)     cls=ConfigurationOption,
672) )
673) @click.option(
674)     '-X',
675)     '--clear',
676)     'clear_all_settings',
677)     is_flag=True,
678)     help='deletes all settings',
679)     cls=ConfigurationOption,
680) )
681) @click.option(
682)     '-e',
683)     '--export',
684)     'export_settings',
685)     metavar='PATH',
686)     type=click.Path(file_okay=True, allow_dash=True, exists=False),
687)     help='export all saved settings into file PATH',
688)     cls=StorageManagementOption,
689) )
690) @click.option(
691)     '-i',
692)     '--import',
693)     'import_settings',
694)     metavar='PATH',
695)     type=click.Path(file_okay=True, allow_dash=True, exists=False),
696)     help='import saved settings from file PATH',
697)     cls=StorageManagementOption,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

700) @click.argument('service', required=False)
701) @click.pass_context
702) def derivepassphrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

703)     ctx: click.Context,
704)     /,
705)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

706)     service: str | None = None,
707)     use_phrase: bool = False,
708)     use_key: bool = False,
709)     length: int | None = None,
710)     repeat: int | None = None,
711)     lower: int | None = None,
712)     upper: int | None = None,
713)     number: int | None = None,
714)     space: int | None = None,
715)     dash: int | None = None,
716)     symbol: int | None = None,
717)     edit_notes: bool = False,
718)     store_config_only: bool = False,
719)     delete_service_settings: bool = False,
720)     delete_globals: bool = False,
721)     clear_all_settings: bool = False,
722)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
723)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

724) ) -> None:
725)     """Derive a strong passphrase, deterministically, from a master secret.
726) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 2 months ago

727)     Using a master passphrase or a master SSH key, derive a passphrase
728)     for SERVICE, subject to length, character and character repetition
729)     constraints.  The derivation is cryptographically strong, meaning
730)     that even if a single passphrase is compromised, guessing the master
731)     passphrase or a different service's passphrase is computationally
732)     infeasible.  The derivation is also deterministic, given the same
733)     inputs, thus the resulting passphrase need not be stored explicitly.
734)     The service name and constraints themselves also need not be kept
735)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

745) 
746)     [CLICK]: https://click.palletsprojects.com/
747) 
748)     Parameters:
749)         ctx (click.Context):
750)             The `click` context.
751) 
752)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

779)         space:
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

780)             Command-line argument `--number`.  Same as `lower`, but for
781)             the space character.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

783)             Command-line argument `--number`.  Same as `lower`, but for
784)             the hyphen-minus and underscore characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

785)         symbol:
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

786)             Command-line argument `--number`.  Same as `lower`, but for
787)             all other ASCII printable characters (except backquote).
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

805)         export_settings:
806)             Command-line argument `-e`/`--export`.  If a file object,
807)             then it must be open for writing and accept `str` inputs.
808)             Otherwise, a filename to open for writing.  Using `-` for
809)             standard output is supported.
810)         import_settings:
811)             Command-line argument `-i`/`--import`.  If a file object, it
812)             must be open for reading and yield `str` values.  Otherwise,
813)             a filename to open for reading.  Using `-` for standard
814)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

815) 
816)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

823)             match param:
824)                 case PasswordGenerationOption():
825)                     group = PasswordGenerationOption
826)                 case ConfigurationOption():
827)                     group = ConfigurationOption
828)                 case StorageManagementOption():
829)                     group = StorageManagementOption
830)                 case OptionGroupOption():
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

831)                     raise AssertionError(  # noqa: TRY003
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

834)                 case _:
835)                     group = click.Option
836)             options_in_group.setdefault(group, []).append(param)
837)         params_by_str[param.human_readable_name] = param
838)         for name in param.opts + param.secondary_opts:
839)             params_by_str[name] = param
840) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

844)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

848)         if isinstance(param, str):
849)             param = params_by_str[param]
850)         assert isinstance(param, click.Parameter)
851)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

852)             return
853)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

856)             assert isinstance(other, click.Parameter)
857)             if other != param and is_param_set(other):
858)                 opt_str = param.opts[0]
859)                 other_str = other.opts[0]
860)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

864)     def err(msg: str) -> NoReturn:
865)         click.echo(f'{PROG_NAME}: {msg}', err=True)
866)         ctx.exit(1)
867) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

869)         try:
870)             return _load_config()
871)         except FileNotFoundError:
872)             return {'services': {}}
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

876)             err(f'Cannot load config: {e}')
877) 
878)     def put_config(config: _types.VaultConfig, /) -> None:
879)         try:
880)             _save_config(config)
Marco Ricci Document and handle other e...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

887) 
888)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

893)                     opt, *options_in_group[PasswordGenerationOption]
894)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

899)                 opt,
900)                 *options_in_group[ConfigurationOption],
901)                 *options_in_group[StorageManagementOption],
902)             )
903)     sv_options = options_in_group[PasswordGenerationOption] + [
904)         params_by_str['--notes'],
905)         params_by_str['--delete'],
906)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

907)     sv_options.remove(params_by_str['--key'])
908)     sv_options.remove(params_by_str['--phrase'])
909)     for param in sv_options:
910)         if is_param_set(param) and not service:
911)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

912)             msg = f'{opt_str} requires a SERVICE'
913)             raise click.UsageError(msg)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

921)     no_sv_options = [
922)         params_by_str['--delete-globals'],
923)         params_by_str['--clear'],
924)         *options_in_group[StorageManagementOption],
925)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

931) 
932)     if edit_notes:
933)         assert service is not None
934)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

938)         notes_value = click.edit(text=text)
939)         if notes_value is not None:
940)             notes_lines = collections.deque(notes_value.splitlines(True))
941)             while notes_lines:
942)                 line = notes_lines.popleft()
943)                 if line.startswith(DEFAULT_NOTES_MARKER):
944)                     notes_value = ''.join(notes_lines)
945)                     break
946)             else:
947)                 if not notes_value.strip():
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

950)                 notes_value.strip('\n')
951)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

953)     elif delete_service_settings:
954)         assert service is not None
955)         configuration = get_config()
956)         if service in configuration['services']:
957)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

959)     elif delete_globals:
960)         configuration = get_config()
961)         if 'global' in configuration:
962)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

966)     elif import_settings:
967)         try:
968)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

969)             infile = (
970)                 cast(TextIO, import_settings)
971)                 if hasattr(import_settings, 'close')
972)                 else click.open_file(os.fspath(import_settings), 'rt')
973)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

974)             with infile:
975)                 maybe_config = json.load(infile)
976)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

980)         if _types.is_vault_config(maybe_config):
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

984)     elif export_settings:
985)         configuration = get_config()
986)         try:
987)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

988)             outfile = (
989)                 cast(TextIO, export_settings)
990)                 if hasattr(export_settings, 'close')
991)                 else click.open_file(os.fspath(export_settings), 'wt')
992)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

993)             with outfile:
994)                 json.dump(configuration, outfile)
995)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1004)         service_keys = {
1005)             'key',
1006)             'phrase',
1007)             'length',
1008)             'repeat',
1009)             'lower',
1010)             'upper',
1011)             'number',
1012)             'space',
1013)             'dash',
1014)             'symbol',
1015)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1017)             {
1018)                 k: v
1019)                 for k, v in locals().items()
1020)                 if k in service_keys and v is not None
1021)             },
1022)             cast(
1023)                 dict[str, Any],
1024)                 configuration['services'].get(service or '', {}),
1025)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1026)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1028)         )
1029)         if use_key:
1030)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1031)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1032)                     'ASCII'
1033)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1034)             except IndexError:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1036)             except KeyError:
1037)                 err('cannot find running SSH agent; check SSH_AUTH_SOCK')
1038)             except OSError as e:
1039)                 err(
1040)                     f'Cannot connect to SSH agent: {e.strerror}: '
1041)                     f'{e.filename!r}'
1042)                 )
Marco Ricci Add a specific error class...

Marco Ricci authored 1 month ago

1043)             except (
1044)                 LookupError,
1045)                 RuntimeError,
1046)                 ssh_agent.SSHAgentFailedError,
1047)             ) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1049)         elif use_phrase:
1050)             maybe_phrase = _prompt_for_passphrase()
1051)             if not maybe_phrase:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

1052)                 err('no passphrase given')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1053)             else:
1054)                 phrase = maybe_phrase
1055)         if store_config_only:
1056)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1057)             view = (
1058)                 collections.ChainMap(*settings.maps[:2])
1059)                 if service
1060)                 else settings.parents.parents
1061)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1062)             if use_key:
1063)                 view['key'] = key
1064)                 for m in view.maps:
1065)                     m.pop('phrase', '')
1066)             elif use_phrase:
1067)                 view['phrase'] = phrase
1068)                 for m in view.maps:
1069)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1072)                 msg = (
1073)                     f'cannot update {settings_type} settings without '
1074)                     f'actual settings'
1075)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1077)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1079)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1082)                 configuration
1083)             ), f'invalid vault configuration: {configuration!r}'
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1084)             _save_config(configuration)
1085)         else:
1086)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1089)             kwargs: dict[str, Any] = {
1090)                 k: v
1091)                 for k, v in settings.items()
1092)                 if k in service_keys and v is not None
1093)             }
1094) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 1 month ago

1095)             def key_to_phrase(
1096)                 key: str | bytes | bytearray,
1097)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1099)                     base64.standard_b64decode(key)
1100)                 )
1101) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1102)             # If either --key or --phrase are given, use that setting.
1103)             # Otherwise, if both key and phrase are set in the config,
1104)             # one must be global (ignore it) and one must be
1105)             # service-specific (use that one). Otherwise, if only one of
1106)             # key and phrase is set in the config, use that one.  In all
1107)             # these above cases, set the phrase via
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1117)             elif kwargs.get('phrase'):
1118)                 pass
1119)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1120)                 msg = (
1121)                     'no passphrase or key given on command-line '
1122)                     'or in configuration'
1123)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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