a1763e8b5dedbf123856a79ddb0e8395cddd6f88
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 Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

163)         RuntimeError:
164)             There was an error communicating with the SSH agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

165) 
166)     """
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

171)             client = conn
172)             client_context = contextlib.nullcontext()
173)         case socket.socket() | None:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

175)             client_context = client
176)         case _:  # pragma: no cover
177)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

180)     with client_context:
181)         try:
182)             all_key_comment_pairs = list(client.list_keys())
183)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

189)             yield pair
190)     if not suitable_keys:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

191)         raise IndexError(_NO_USABLE_KEYS)
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

192) 
193) 
194) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

237)             err=True,
238)             type=choices,
239)             show_choices=False,
240)             show_default=False,
241)             default='',
242)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

246)     prompt_suffix = (
247)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
248)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

249)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

250)         click.confirm(
251)             single_choice_prompt,
252)             prompt_suffix=prompt_suffix,
253)             err=True,
254)             abort=True,
255)             default=False,
256)             show_default=False,
257)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

258)     except click.Abort:
259)         raise IndexError(_EMPTY_SELECTION) from None
260)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

261) 
262) 
263) def _select_ssh_key(
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

270)     prompted interactively (see [`click.prompt`][]) for a selection.
271) 
272)     Args:
273)         conn:
274)             An optional connection hint to the SSH agent; specifically,
275)             an SSH agent client, or a socket connected to an SSH agent.
276) 
277)             If an existing SSH agent client, then this client will be
278)             queried for the SSH keys, and otherwise left intact.
279) 
280)             If a socket, then a one-shot client will be constructed
281)             based on the socket to query the agent, and deconstructed
282)             afterwards.
283) 
284)             If neither are given, then the agent's socket location is
285)             looked up in the `SSH_AUTH_SOCK` environment variable, and
286)             used to construct/deconstruct a one-shot client, as in the
287)             previous case.
288) 
289)     Returns:
290)         The selected SSH key.
291) 
292)     Raises:
293)         IndexError:
294)             The user made an invalid or empty selection, or requested an
295)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

299)         RuntimeError:
300)             There was an error communicating with the SSH agent.
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

301)     """
302)     suitable_keys = list(_get_suitable_ssh_keys(conn))
303)     key_listing: list[str] = []
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

308)         key_prefix = (
309)             key_str
310)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
311)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
312)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

316)         key_listing,
317)         heading='Suitable SSH keys:',
318)         single_choice_prompt='Use this key?',
319)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

320)     return suitable_keys[choice].key
321) 
322) 
323) def _prompt_for_passphrase() -> str:
324)     """Interactively prompt for the passphrase.
325) 
326)     Calls [`click.prompt`][] internally.  Moved into a separate function
327)     mainly for testing/mocking purposes.
328) 
329)     Returns:
330)         The user input.
331) 
332)     """
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

333)     return cast(
334)         str,
335)         click.prompt(
336)             'Passphrase',
337)             default='',
338)             hide_input=True,
339)             show_default=False,
340)             err=True,
341)         ),
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

343) 
344) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

346)     """A [`click.Option`][] with an associated group name and group epilog.
347) 
348)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
349)     help sections.  Each subclass contains its own group name and
350)     epilog.
351) 
352)     Attributes:
353)         option_group_name:
354)             The name of the option group.  Used as a heading on the help
355)             text for options in this section.
356)         epilog:
357)             An epilog to print after listing the options in this
358)             section.
359) 
360)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

361) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

362)     option_group_name: str = ''
363)     epilog: str = ''
364) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

370) 
371) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

372)     """A [`click.Command`][] with support for help/option groups.
373) 
374)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
375)     further modified to support group epilogs.
376) 
377)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
378) 
379)     """
380) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

381)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

382)         self,
383)         ctx: click.Context,
384)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

388)         This is a callback for [`click.Command.get_help`][] that
389)         implements the `--help` listing, by calling appropriate methods
390)         of the `formatter`.  We list all options (like the base
391)         implementation), but grouped into sections according to the
392)         concrete [`click.Option`][] subclass being used.  If the option
393)         is an instance of some subclass `X` of
394)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
395)         heading and the epilog are taken from `X.option_group_name` and
396)         `X.epilog`; otherwise, the section heading is "Options" (or
397)         "Other options" if there are other option groups) and the epilog
398)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

399) 
400)         Args:
401)             ctx:
402)                 The click context.
403)             formatter:
404)                 The formatter for the `--help` listing.
405) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 2 months ago

406)         Returns:
407)             Nothing.  Output is generated by calling appropriate methods
408)             on `formatter` instead.
409) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

411)         help_records: dict[str, list[tuple[str, str]]] = {}
412)         epilogs: dict[str, str] = {}
413)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

414)         if (  # pragma: no branch
415)             (help_opt := self.get_help_option(ctx)) is not None
416)             and help_opt not in params
417)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

418)             params.append(help_opt)
419)         for param in params:
420)             rec = param.get_help_record(ctx)
421)             if rec is not None:
422)                 if isinstance(param, OptionGroupOption):
423)                     group_name = param.option_group_name
424)                     epilogs.setdefault(group_name, param.epilog)
425)                 else:
426)                     group_name = ''
427)                 help_records.setdefault(group_name, []).append(rec)
428)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

429)         default_group_name = (
430)             'Other Options' if len(default_group) > 1 else 'Options'
431)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

432)         help_records[default_group_name] = default_group
433)         for group_name, records in help_records.items():
434)             with formatter.section(group_name):
435)                 formatter.write_dl(records)
436)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
437)             if epilog:
438)                 formatter.write_paragraph()
439)                 with formatter.indentation():
440)                     formatter.write_text(epilog)
441) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

453) 
454) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

462) 
463) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

470)     """
471) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

472) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

473) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

474)     ctx: click.Context,
475)     param: click.Parameter,
476)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

481)     if value is None:
482)         return value
483)     if isinstance(value, int):
484)         int_value = value
485)     else:
486)         try:
487)             int_value = int(value, 10)
488)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

494)     return int_value
495) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

496) 
497) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

498)     ctx: click.Context,
499)     param: click.Parameter,
500)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

505)     if value is None:
506)         return value
507)     if isinstance(value, int):
508)         int_value = value
509)     else:
510)         try:
511)             int_value = int(value, 10)
512)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

518)     return int_value
519) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

520) 
521) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

522) # Enter notes below the line with the cut mark (ASCII scissors and
523) # dashes).  Lines above the cut mark (such as this one) will be ignored.
524) #
525) # If you wish to clear the notes, leave everything beyond the cut mark
526) # blank.  However, if you leave the *entire* file blank, also removing
527) # the cut mark, then the edit is aborted, and the old notes contents are
528) # retained.
529) #
530) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

532) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
533) 
534) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

544) 
545)         Configuration is stored in a directory according to the
546)         DERIVEPASSPHRASE_PATH variable, which defaults to
547)         `~/.derivepassphrase` on UNIX-like systems and
548)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
549)         The configuration is NOT encrypted, and you are STRONGLY
550)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

684) @click.argument('service', required=False)
685) @click.pass_context
686) def derivepassphrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

687)     ctx: click.Context,
688)     /,
689)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

690)     service: str | None = None,
691)     use_phrase: bool = False,
692)     use_key: bool = False,
693)     length: int | None = None,
694)     repeat: int | None = None,
695)     lower: int | None = None,
696)     upper: int | None = None,
697)     number: int | None = None,
698)     space: int | None = None,
699)     dash: int | None = None,
700)     symbol: int | None = None,
701)     edit_notes: bool = False,
702)     store_config_only: bool = False,
703)     delete_service_settings: bool = False,
704)     delete_globals: bool = False,
705)     clear_all_settings: bool = False,
706)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
707)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

708) ) -> None:
709)     """Derive a strong passphrase, deterministically, from a master secret.
710) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 2 months ago

711)     Using a master passphrase or a master SSH key, derive a passphrase
712)     for SERVICE, subject to length, character and character repetition
713)     constraints.  The derivation is cryptographically strong, meaning
714)     that even if a single passphrase is compromised, guessing the master
715)     passphrase or a different service's passphrase is computationally
716)     infeasible.  The derivation is also deterministic, given the same
717)     inputs, thus the resulting passphrase need not be stored explicitly.
718)     The service name and constraints themselves also need not be kept
719)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

729) 
730)     [CLICK]: https://click.palletsprojects.com/
731) 
732)     Parameters:
733)         ctx (click.Context):
734)             The `click` context.
735) 
736)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

789)         export_settings:
790)             Command-line argument `-e`/`--export`.  If a file object,
791)             then it must be open for writing and accept `str` inputs.
792)             Otherwise, a filename to open for writing.  Using `-` for
793)             standard output is supported.
794)         import_settings:
795)             Command-line argument `-i`/`--import`.  If a file object, it
796)             must be open for reading and yield `str` values.  Otherwise,
797)             a filename to open for reading.  Using `-` for standard
798)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

799) 
800)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

807)             match param:
808)                 case PasswordGenerationOption():
809)                     group = PasswordGenerationOption
810)                 case ConfigurationOption():
811)                     group = ConfigurationOption
812)                 case StorageManagementOption():
813)                     group = StorageManagementOption
814)                 case OptionGroupOption():
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

818)                 case _:
819)                     group = click.Option
820)             options_in_group.setdefault(group, []).append(param)
821)         params_by_str[param.human_readable_name] = param
822)         for name in param.opts + param.secondary_opts:
823)             params_by_str[name] = param
824) 
Marco Ricci Fix typing issues in mypy s...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

828)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

832)         if isinstance(param, str):
833)             param = params_by_str[param]
834)         assert isinstance(param, click.Parameter)
835)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

836)             return
837)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

840)             assert isinstance(other, click.Parameter)
841)             if other != param and is_param_set(other):
842)                 opt_str = param.opts[0]
843)                 other_str = other.opts[0]
844)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

848)     def err(msg: str) -> NoReturn:
849)         click.echo(f'{PROG_NAME}: {msg}', err=True)
850)         ctx.exit(1)
851) 
Marco Ricci Consolidate `types` submodu...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

853)         try:
854)             return _load_config()
855)         except FileNotFoundError:
856)             return {'services': {}}
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

858)             err(f'Cannot load config: {e}')
859) 
860)     def put_config(config: _types.VaultConfig, /) -> None:
861)         try:
862)             _save_config(config)
863)         except Exception as exc:  # noqa: BLE001
864)             err(f'Cannot store config: {exc}')
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

867) 
868)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

873)                     opt, *options_in_group[PasswordGenerationOption]
874)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

879)                 opt,
880)                 *options_in_group[ConfigurationOption],
881)                 *options_in_group[StorageManagementOption],
882)             )
883)     sv_options = options_in_group[PasswordGenerationOption] + [
884)         params_by_str['--notes'],
885)         params_by_str['--delete'],
886)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

887)     sv_options.remove(params_by_str['--key'])
888)     sv_options.remove(params_by_str['--phrase'])
889)     for param in sv_options:
890)         if is_param_set(param) and not service:
891)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

901)     no_sv_options = [
902)         params_by_str['--delete-globals'],
903)         params_by_str['--clear'],
904)         *options_in_group[StorageManagementOption],
905)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

911) 
912)     if edit_notes:
913)         assert service is not None
914)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

918)         notes_value = click.edit(text=text)
919)         if notes_value is not None:
920)             notes_lines = collections.deque(notes_value.splitlines(True))
921)             while notes_lines:
922)                 line = notes_lines.popleft()
923)                 if line.startswith(DEFAULT_NOTES_MARKER):
924)                     notes_value = ''.join(notes_lines)
925)                     break
926)             else:
927)                 if not notes_value.strip():
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

930)                 notes_value.strip('\n')
931)             )
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

933)     elif delete_service_settings:
934)         assert service is not None
935)         configuration = get_config()
936)         if service in configuration['services']:
937)             del configuration['services'][service]
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

939)     elif delete_globals:
940)         configuration = get_config()
941)         if 'global' in configuration:
942)             del configuration['global']
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

949)             infile = (
950)                 cast(TextIO, import_settings)
951)                 if hasattr(import_settings, 'close')
952)                 else click.open_file(os.fspath(import_settings), 'rt')
953)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

954)             with infile:
955)                 maybe_config = json.load(infile)
956)         except json.JSONDecodeError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

964)     elif export_settings:
965)         configuration = get_config()
966)         try:
967)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

968)             outfile = (
969)                 cast(TextIO, export_settings)
970)                 if hasattr(export_settings, 'close')
971)                 else click.open_file(os.fspath(export_settings), 'wt')
972)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

973)             with outfile:
974)                 json.dump(configuration, outfile)
975)         except OSError as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

984)         service_keys = {
985)             'key',
986)             'phrase',
987)             'length',
988)             'repeat',
989)             'lower',
990)             'upper',
991)             'number',
992)             'space',
993)             'dash',
994)             'symbol',
995)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

997)             {
998)                 k: v
999)                 for k, v in locals().items()
1000)                 if k in service_keys and v is not None
1001)             },
1002)             cast(
1003)                 dict[str, Any],
1004)                 configuration['services'].get(service or '', {}),
1005)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1006)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1008)         )
1009)         if use_key:
1010)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1011)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
1012)                     'ASCII'
1013)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

1015)                 err('no valid SSH key selected')
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

1016)             except (LookupError, RuntimeError) as e:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1018)         elif use_phrase:
1019)             maybe_phrase = _prompt_for_passphrase()
1020)             if not maybe_phrase:
Marco Ricci Use better error message ha...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1022)             else:
1023)                 phrase = maybe_phrase
1024)         if store_config_only:
1025)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1026)             view = (
1027)                 collections.ChainMap(*settings.maps[:2])
1028)                 if service
1029)                 else settings.parents.parents
1030)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1031)             if use_key:
1032)                 view['key'] = key
1033)                 for m in view.maps:
1034)                     m.pop('phrase', '')
1035)             elif use_phrase:
1036)                 view['phrase'] = phrase
1037)                 for m in view.maps:
1038)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1041)                 msg = (
1042)                     f'cannot update {settings_type} settings without '
1043)                     f'actual settings'
1044)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1046)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1048)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1053)             _save_config(configuration)
1054)         else:
1055)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1058)             kwargs: dict[str, Any] = {
1059)                 k: v
1060)                 for k, v in settings.items()
1061)                 if k in service_keys and v is not None
1062)             }
1063) 
Marco Ricci Shift misplaced local function

Marco Ricci authored 1 month ago

1064)             def key_to_phrase(
1065)                 key: str | bytes | bytearray,
1066)             ) -> bytes | bytearray:
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1068)                     base64.standard_b64decode(key)
1069)                 )
1070) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1071)             # If either --key or --phrase are given, use that setting.
1072)             # Otherwise, if both key and phrase are set in the config,
1073)             # one must be global (ignore it) and one must be
1074)             # service-specific (use that one). Otherwise, if only one of
1075)             # key and phrase is set in the config, use that one.  In all
1076)             # these above cases, set the phrase via
Marco Ricci Move `sequin` and `ssh_agen...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1086)             elif kwargs.get('phrase'):
1087)                 pass
1088)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1089)                 msg = (
1090)                     'no passphrase or key given on command-line '
1091)                     'or in configuration'
1092)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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