99863c4d7b2a3ee8f7b0d0d3e3f924afe7d40fb8
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,
19)     TextIO,
20)     cast,
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

29) import derivepassphrase as dpp
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

30) import ssh_agent_client
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

31) from derivepassphrase import types as dpp_types
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) 
71) def _load_config() -> dpp_types.VaultConfig:
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)
93)     if not dpp_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) 
98) def _save_config(config: dpp_types.VaultConfig, /) -> None:
99)     """Save a vault(1)-compatbile config to the application directory.
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)     """
116)     if not dpp_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 Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

120)         json.dump(config, fileobj)
121) 
122) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

123) def _get_suitable_ssh_keys(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

125) ) -> Iterator[ssh_agent_client.types.KeyCommentPair]:
126)     """Yield all SSH keys suitable for passphrase derivation.
127) 
128)     Suitable SSH keys are queried from the running SSH agent (see
129)     [`ssh_agent_client.SSHAgentClient.list_keys`][]).
130) 
131)     Args:
132)         conn:
133)             An optional connection hint to the SSH agent; specifically,
134)             an SSH agent client, or a socket connected to an SSH agent.
135) 
136)             If an existing SSH agent client, then this client will be
137)             queried for the SSH keys, and otherwise left intact.
138) 
139)             If a socket, then a one-shot client will be constructed
140)             based on the socket to query the agent, and deconstructed
141)             afterwards.
142) 
143)             If neither are given, then the agent's socket location is
144)             looked up in the `SSH_AUTH_SOCK` environment variable, and
145)             used to construct/deconstruct a one-shot client, as in the
146)             previous case.
147) 
148)     Yields:
149)         :
150)             Every SSH key from the SSH agent that is suitable for
151)             passphrase derivation.
152) 
153)     Raises:
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

159) 
160)     """
161)     client: ssh_agent_client.SSHAgentClient
162)     client_context: contextlib.AbstractContextManager
163)     match conn:
164)         case ssh_agent_client.SSHAgentClient():
165)             client = conn
166)             client_context = contextlib.nullcontext()
167)         case socket.socket() | None:
168)             client = ssh_agent_client.SSHAgentClient(socket=conn)
169)             client_context = client
170)         case _:  # pragma: no cover
171)             assert_never(conn)
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

174)     with client_context:
175)         try:
176)             all_key_comment_pairs = list(client.list_keys())
177)         except EOFError as e:  # pragma: no cover
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

181)         key, _comment = pair
182)         if dpp.Vault._is_suitable_ssh_key(key):  # noqa: SLF001
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

186) 
187) 
188) def _prompt_for_selection(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

231)             err=True,
232)             type=choices,
233)             show_choices=False,
234)             show_default=False,
235)             default='',
236)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

240)     prompt_suffix = (
241)         ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
242)     )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

243)     try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

244)         click.confirm(
245)             single_choice_prompt,
246)             prompt_suffix=prompt_suffix,
247)             err=True,
248)             abort=True,
249)             default=False,
250)             show_default=False,
251)         )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

252)     except click.Abort:
253)         raise IndexError(_EMPTY_SELECTION) from None
254)     return 0
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

255) 
256) 
257) def _select_ssh_key(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

259) ) -> bytes | bytearray:
260)     """Interactively select an SSH key for passphrase derivation.
261) 
262)     Suitable SSH keys are queried from the running SSH agent (see
263)     [`ssh_agent_client.SSHAgentClient.list_keys`][]), then the user is
264)     prompted interactively (see [`click.prompt`][]) for a selection.
265) 
266)     Args:
267)         conn:
268)             An optional connection hint to the SSH agent; specifically,
269)             an SSH agent client, or a socket connected to an SSH agent.
270) 
271)             If an existing SSH agent client, then this client will be
272)             queried for the SSH keys, and otherwise left intact.
273) 
274)             If a socket, then a one-shot client will be constructed
275)             based on the socket to query the agent, and deconstructed
276)             afterwards.
277) 
278)             If neither are given, then the agent's socket location is
279)             looked up in the `SSH_AUTH_SOCK` environment variable, and
280)             used to construct/deconstruct a one-shot client, as in the
281)             previous case.
282) 
283)     Returns:
284)         The selected SSH key.
285) 
286)     Raises:
287)         IndexError:
288)             The user made an invalid or empty selection, or requested an
289)             abort.
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

295)     """
296)     suitable_keys = list(_get_suitable_ssh_keys(conn))
297)     key_listing: list[str] = []
298)     unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix
299)     for key, comment in suitable_keys:
300)         keytype = unstring_prefix(key)[0].decode('ASCII')
301)         key_str = base64.standard_b64encode(key).decode('ASCII')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

302)         key_prefix = (
303)             key_str
304)             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
305)             else key_str[:KEY_DISPLAY_LENGTH] + '...'
306)         )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

310)         key_listing,
311)         heading='Suitable SSH keys:',
312)         single_choice_prompt='Use this key?',
313)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

314)     return suitable_keys[choice].key
315) 
316) 
317) def _prompt_for_passphrase() -> str:
318)     """Interactively prompt for the passphrase.
319) 
320)     Calls [`click.prompt`][] internally.  Moved into a separate function
321)     mainly for testing/mocking purposes.
322) 
323)     Returns:
324)         The user input.
325) 
326)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

327)     return click.prompt(
328)         'Passphrase', default='', hide_input=True, show_default=False, err=True
329)     )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

330) 
331) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

333)     """A [`click.Option`][] with an associated group name and group epilog.
334) 
335)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
336)     help sections.  Each subclass contains its own group name and
337)     epilog.
338) 
339)     Attributes:
340)         option_group_name:
341)             The name of the option group.  Used as a heading on the help
342)             text for options in this section.
343)         epilog:
344)             An epilog to print after listing the options in this
345)             section.
346) 
347)     """
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

348) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

349)     option_group_name: str = ''
350)     epilog: str = ''
351) 
352)     def __init__(self, *args, **kwargs):  # type: ignore
353)         if self.__class__ == __class__:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

357) 
358) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

359)     """A [`click.Command`][] with support for help/option groups.
360) 
361)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
362)     further modified to support group epilogs.
363) 
364)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
365) 
366)     """
367) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

368)     def format_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

369)         self,
370)         ctx: click.Context,
371)         formatter: click.HelpFormatter,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

375)         This is a callback for [`click.Command.get_help`][] that
376)         implements the `--help` listing, by calling appropriate methods
377)         of the `formatter`.  We list all options (like the base
378)         implementation), but grouped into sections according to the
379)         concrete [`click.Option`][] subclass being used.  If the option
380)         is an instance of some subclass `X` of
381)         [`derivepassphrase.cli.OptionGroupOption`][], then the section
382)         heading and the epilog are taken from `X.option_group_name` and
383)         `X.epilog`; otherwise, the section heading is "Options" (or
384)         "Other options" if there are other option groups) and the epilog
385)         is empty.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

386) 
387)         Args:
388)             ctx:
389)                 The click context.
390)             formatter:
391)                 The formatter for the `--help` listing.
392) 
Marco Ricci Add minor documentation rew...

Marco Ricci authored 2 months ago

393)         Returns:
394)             Nothing.  Output is generated by calling appropriate methods
395)             on `formatter` instead.
396) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

398)         help_records: dict[str, list[tuple[str, str]]] = {}
399)         epilogs: dict[str, str] = {}
400)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

401)         if (  # pragma: no branch
402)             (help_opt := self.get_help_option(ctx)) is not None
403)             and help_opt not in params
404)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

405)             params.append(help_opt)
406)         for param in params:
407)             rec = param.get_help_record(ctx)
408)             if rec is not None:
409)                 if isinstance(param, OptionGroupOption):
410)                     group_name = param.option_group_name
411)                     epilogs.setdefault(group_name, param.epilog)
412)                 else:
413)                     group_name = ''
414)                 help_records.setdefault(group_name, []).append(rec)
415)         default_group = help_records.pop('')
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

416)         default_group_name = (
417)             'Other Options' if len(default_group) > 1 else 'Options'
418)         )
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

419)         help_records[default_group_name] = default_group
420)         for group_name, records in help_records.items():
421)             with formatter.section(group_name):
422)                 formatter.write_dl(records)
423)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
424)             if epilog:
425)                 formatter.write_paragraph()
426)                 with formatter.indentation():
427)                     formatter.write_text(epilog)
428) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

440) 
441) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

449) 
450) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

459) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

460) def _validate_occurrence_constraint(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

461)     ctx: click.Context,
462)     param: click.Parameter,
463)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

468)     if value is None:
469)         return value
470)     if isinstance(value, int):
471)         int_value = value
472)     else:
473)         try:
474)             int_value = int(value, 10)
475)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

481)     return int_value
482) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

483) 
484) def _validate_length(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

485)     ctx: click.Context,
486)     param: click.Parameter,
487)     value: Any,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

492)     if value is None:
493)         return value
494)     if isinstance(value, int):
495)         int_value = value
496)     else:
497)         try:
498)             int_value = int(value, 10)
499)         except ValueError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

505)     return int_value
506) 
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

507) 
508) DEFAULT_NOTES_TEMPLATE = """\
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

509) # Enter notes below the line with the cut mark (ASCII scissors and
510) # dashes).  Lines above the cut mark (such as this one) will be ignored.
511) #
512) # If you wish to clear the notes, leave everything beyond the cut mark
513) # blank.  However, if you leave the *entire* file blank, also removing
514) # the cut mark, then the edit is aborted, and the old notes contents are
515) # retained.
516) #
517) # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

519) DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
520) 
521) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

531) 
532)         Configuration is stored in a directory according to the
533)         DERIVEPASSPHRASE_PATH variable, which defaults to
534)         `~/.derivepassphrase` on UNIX-like systems and
535)         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
536)         The configuration is NOT encrypted, and you are STRONGLY
537)         discouraged from using a stored passphrase.
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

671) @click.argument('service', required=False)
672) @click.pass_context
673) def derivepassphrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

674)     ctx: click.Context,
675)     /,
676)     *,
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

677)     service: str | None = None,
678)     use_phrase: bool = False,
679)     use_key: bool = False,
680)     length: int | None = None,
681)     repeat: int | None = None,
682)     lower: int | None = None,
683)     upper: int | None = None,
684)     number: int | None = None,
685)     space: int | None = None,
686)     dash: int | None = None,
687)     symbol: int | None = None,
688)     edit_notes: bool = False,
689)     store_config_only: bool = False,
690)     delete_service_settings: bool = False,
691)     delete_globals: bool = False,
692)     clear_all_settings: bool = False,
693)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
694)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

695) ) -> None:
696)     """Derive a strong passphrase, deterministically, from a master secret.
697) 
Marco Ricci Fill out README and documen...

Marco Ricci authored 2 months ago

698)     Using a master passphrase or a master SSH key, derive a passphrase
699)     for SERVICE, subject to length, character and character repetition
700)     constraints.  The derivation is cryptographically strong, meaning
701)     that even if a single passphrase is compromised, guessing the master
702)     passphrase or a different service's passphrase is computationally
703)     infeasible.  The derivation is also deterministic, given the same
704)     inputs, thus the resulting passphrase need not be stored explicitly.
705)     The service name and constraints themselves also need not be kept
706)     secret; the latter are usually stored in a world-readable file.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

716) 
717)     [CLICK]: https://click.palletsprojects.com/
718) 
719)     Parameters:
720)         ctx (click.Context):
721)             The `click` context.
722) 
723)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

776)         export_settings:
777)             Command-line argument `-e`/`--export`.  If a file object,
778)             then it must be open for writing and accept `str` inputs.
779)             Otherwise, a filename to open for writing.  Using `-` for
780)             standard output is supported.
781)         import_settings:
782)             Command-line argument `-i`/`--import`.  If a file object, it
783)             must be open for reading and yield `str` values.  Otherwise,
784)             a filename to open for reading.  Using `-` for standard
785)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

786) 
787)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

794)             match param:
795)                 case PasswordGenerationOption():
796)                     group = PasswordGenerationOption
797)                 case ConfigurationOption():
798)                     group = ConfigurationOption
799)                 case StorageManagementOption():
800)                     group = StorageManagementOption
801)                 case OptionGroupOption():
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

805)                 case _:
806)                     group = click.Option
807)             options_in_group.setdefault(group, []).append(param)
808)         params_by_str[param.human_readable_name] = param
809)         for name in param.opts + param.secondary_opts:
810)             params_by_str[name] = param
811) 
812)     def is_param_set(param: click.Parameter):
813)         return bool(ctx.params.get(param.human_readable_name))
814) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

815)     def check_incompatible_options(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

819)         if isinstance(param, str):
820)             param = params_by_str[param]
821)         assert isinstance(param, click.Parameter)
822)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

823)             return
824)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

827)             assert isinstance(other, click.Parameter)
828)             if other != param and is_param_set(other):
829)                 opt_str = param.opts[0]
830)                 other_str = other.opts[0]
831)                 raise click.BadOptionUsage(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

834) 
835)     def get_config() -> dpp_types.VaultConfig:
836)         try:
837)             return _load_config()
838)         except FileNotFoundError:
839)             return {'services': {}}
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

840)         except Exception as e:  # noqa: BLE001
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

841)             ctx.fail(f'cannot load config: {e}')
842) 
843)     configuration: dpp_types.VaultConfig
844) 
845)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

850)                     opt, *options_in_group[PasswordGenerationOption]
851)                 )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

856)                 opt,
857)                 *options_in_group[ConfigurationOption],
858)                 *options_in_group[StorageManagementOption],
859)             )
860)     sv_options = options_in_group[PasswordGenerationOption] + [
861)         params_by_str['--notes'],
862)         params_by_str['--delete'],
863)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

864)     sv_options.remove(params_by_str['--key'])
865)     sv_options.remove(params_by_str['--phrase'])
866)     for param in sv_options:
867)         if is_param_set(param) and not service:
868)             opt_str = param.opts[0]
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

878)     no_sv_options = [
879)         params_by_str['--delete-globals'],
880)         params_by_str['--clear'],
881)         *options_in_group[StorageManagementOption],
882)     ]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

888) 
889)     if edit_notes:
890)         assert service is not None
891)         configuration = get_config()
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

892)         text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get(
893)             service, cast(dpp_types.VaultConfigServicesSettings, {})
894)         ).get('notes', '')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

895)         notes_value = click.edit(text=text)
896)         if notes_value is not None:
897)             notes_lines = collections.deque(notes_value.splitlines(True))
898)             while notes_lines:
899)                 line = notes_lines.popleft()
900)                 if line.startswith(DEFAULT_NOTES_MARKER):
901)                     notes_value = ''.join(notes_lines)
902)                     break
903)             else:
904)                 if not notes_value.strip():
905)                     ctx.fail('not saving new notes: user aborted request')
906)             configuration['services'].setdefault(service, {})['notes'] = (
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

907)                 notes_value.strip('\n')
908)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

909)             _save_config(configuration)
910)     elif delete_service_settings:
911)         assert service is not None
912)         configuration = get_config()
913)         if service in configuration['services']:
914)             del configuration['services'][service]
915)             _save_config(configuration)
916)     elif delete_globals:
917)         configuration = get_config()
918)         if 'global' in configuration:
919)             del configuration['global']
920)             _save_config(configuration)
921)     elif clear_all_settings:
922)         _save_config({'services': {}})
923)     elif import_settings:
924)         try:
925)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

926)             infile = (
927)                 cast(TextIO, import_settings)
928)                 if hasattr(import_settings, 'close')
929)                 else click.open_file(os.fspath(import_settings), 'rt')
930)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

931)             with infile:
932)                 maybe_config = json.load(infile)
933)         except json.JSONDecodeError as e:
934)             ctx.fail(f'Cannot load config: cannot decode JSON: {e}')
935)         except OSError as e:
936)             ctx.fail(f'Cannot load config: {e.strerror}')
937)         if dpp_types.is_vault_config(maybe_config):
938)             _save_config(maybe_config)
939)         else:
940)             ctx.fail('not a valid config')
941)     elif export_settings:
942)         configuration = get_config()
943)         try:
944)             # TODO: keep track of auto-close; try os.dup if feasible
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

945)             outfile = (
946)                 cast(TextIO, export_settings)
947)                 if hasattr(export_settings, 'close')
948)                 else click.open_file(os.fspath(export_settings), 'wt')
949)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

950)             with outfile:
951)                 json.dump(configuration, outfile)
952)         except OSError as e:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

953)             ctx.fail(f'cannot write config: {e.strerror}')
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

961)         service_keys = {
962)             'key',
963)             'phrase',
964)             'length',
965)             'repeat',
966)             'lower',
967)             'upper',
968)             'number',
969)             'space',
970)             'dash',
971)             'symbol',
972)         }
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 1 month ago

974)             {
975)                 k: v
976)                 for k, v in locals().items()
977)                 if k in service_keys and v is not None
978)             },
979)             cast(
980)                 dict[str, Any],
981)                 configuration['services'].get(service or '', {}),
982)             ),
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

983)             {},
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

985)         )
986)         if use_key:
987)             try:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

988)                 key = base64.standard_b64encode(_select_ssh_key()).decode(
989)                     'ASCII'
990)                 )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

991)             except IndexError:
992)                 ctx.fail('no valid SSH key selected')
Marco Ricci Distinguish between a key l...

Marco Ricci authored 2 months ago

993)             except (LookupError, RuntimeError) as e:
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

994)                 ctx.fail(str(e))
995)         elif use_phrase:
996)             maybe_phrase = _prompt_for_passphrase()
997)             if not maybe_phrase:
998)                 ctx.fail('no passphrase given')
999)             else:
1000)                 phrase = maybe_phrase
1001)         if store_config_only:
1002)             view: collections.ChainMap[str, Any]
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1003)             view = (
1004)                 collections.ChainMap(*settings.maps[:2])
1005)                 if service
1006)                 else settings.parents.parents
1007)             )
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1008)             if use_key:
1009)                 view['key'] = key
1010)                 for m in view.maps:
1011)                     m.pop('phrase', '')
1012)             elif use_phrase:
1013)                 view['phrase'] = phrase
1014)                 for m in view.maps:
1015)                     m.pop('key', '')
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1018)                 msg = (
1019)                     f'cannot update {settings_type} settings without '
1020)                     f'actual settings'
1021)                 )
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1023)             if service:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

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

Marco Ricci authored 2 months ago

1025)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1026)                 configuration.setdefault('global', {}).update(view)  # type: ignore[typeddict-item]
1027)             assert dpp_types.is_vault_config(
1028)                 configuration
1029)             ), f'invalid vault configuration: {configuration!r}'
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1030)             _save_config(configuration)
1031)         else:
1032)             if not service:
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 1 month ago

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

Marco Ricci authored 1 month ago

1035)             kwargs: dict[str, Any] = {
1036)                 k: v
1037)                 for k, v in settings.items()
1038)                 if k in service_keys and v is not None
1039)             }
1040) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1041)             # If either --key or --phrase are given, use that setting.
1042)             # Otherwise, if both key and phrase are set in the config,
1043)             # one must be global (ignore it) and one must be
1044)             # service-specific (use that one). Otherwise, if only one of
1045)             # key and phrase is set in the config, use that one.  In all
1046)             # these above cases, set the phrase via
1047)             # derivepassphrase.Vault.phrase_from_key if a key is
1048)             # given. Finally, if nothing is set, error out.
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

1049)             def key_to_phrase(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1050)                 key: str | bytes | bytearray,
Marco Ricci Fix style issues with ruff...

Marco Ricci authored 2 months ago

1051)             ) -> bytes | bytearray:
1052)                 return dpp.Vault.phrase_from_key(
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1053)                     base64.standard_b64decode(key)
1054)                 )
1055) 
Marco Ricci Add finished command-line i...

Marco Ricci authored 2 months ago

1056)             if use_key or use_phrase:
1057)                 if use_key:
1058)                     kwargs['phrase'] = key_to_phrase(key)
1059)                 else:
1060)                     kwargs['phrase'] = phrase
1061)                     kwargs.pop('key', '')
1062)             elif kwargs.get('phrase') and kwargs.get('key'):
1063)                 if any('key' in m for m in settings.maps[:2]):
1064)                     kwargs['phrase'] = key_to_phrase(kwargs.pop('key'))
1065)                 else:
1066)                     kwargs.pop('key')
1067)             elif kwargs.get('key'):
1068)                 kwargs['phrase'] = key_to_phrase(kwargs.pop('key'))
1069)             elif kwargs.get('phrase'):
1070)                 pass
1071)             else:
Marco Ricci Reformat everything with ruff

Marco Ricci authored 1 month ago

1072)                 msg = (
1073)                     'no passphrase or key given on command-line '
1074)                     'or in configuration'
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)             vault = dpp.Vault(**kwargs)
1078)             result = vault.generate(service)
1079)             click.echo(result.decode('ASCII'))