Coverage for src\derivepassphrase\cli.py: 100.000%

1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>

2#

3# SPDX-License-Identifier: Zlib

4

5# ruff: noqa: TRY400

6

7"""Command-line interface for derivepassphrase."""

8

9 from __future__ import annotations (empty)

10

11 import base64 (empty)

12 import collections (empty)

13 import contextlib (empty)

14 import json (empty)

15 import logging (empty)

16 import os (empty)

17 from typing import ( (empty)

18 TYPE_CHECKING,

19 Final,

20 Literal,

21 NoReturn,

22 TextIO,

23 cast,

24)

25

26 import click (empty)

27 import click.shell_completion (empty)

28 from typing_extensions import ( (empty)

29 Any,

30)

31

32 from derivepassphrase import _internals, _types, exporter, vault (empty)

33 from derivepassphrase._internals import cli_helpers, cli_machinery (empty)

34 from derivepassphrase._internals import cli_messages as _msg (empty)

35

36 if TYPE_CHECKING: (empty)

37 from collections.abc import Sequence

38 from collections.abc import Set as AbstractSet

39 from contextlib import AbstractContextManager

40

41 __all__ = ('derivepassphrase',) (empty)

42

43 PROG_NAME = _internals.PROG_NAME (empty)

44 VERSION = _internals.VERSION (empty)

45

46

47@click.group( (empty)

48 context_settings={

49 'help_option_names': ['-h', '--help'],

50 'ignore_unknown_options': True,

51 'allow_interspersed_args': False,

52 },

53 epilog=_msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EPILOG_01),

54 invoke_without_command=True,

55 cls=cli_machinery.TopLevelCLIEntryPoint,

56 help=(

57 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_01),

58 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_02),

59 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_03),

60 ),

61)

62@cli_machinery.version_option( (empty)

63 cli_machinery.derivepassphrase_version_option_callback

64)

65@cli_machinery.color_forcing_pseudo_option (empty)

66@cli_machinery.standard_logging_options (empty)

67@click.pass_context (empty)

68 def derivepassphrase(ctx: click.Context, /) -> None: (empty)

69 """Derive a strong passphrase, deterministically, from a master secret.

70

71 This is a [`click`][CLICK]-powered command-line interface function,

72 and not intended for programmatic use. See the derivepassphrase(1)

73 manpage for full documentation of the interface. (See also

74 [`click.testing.CliRunner`][] for controlled, programmatic

75 invocation.)

76

77 [CLICK]: https://pypi.org/package/click/

78

79 """

80 # TODO(the-13th-letter): Turn this callback into a no-op in v1.0.

81 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands

82 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 13 ctx 1:-;.q=/?W9+wO

83 if ctx.invoked_subcommand is None: 13 ctx 1:-;.q=/?W9+wO

84 deprecation.warning( 1 ctx 1O

85 _msg.TranslatedString(

86 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED

87 ),

88 extra={'color': ctx.color},

89 )

90 # See definition of click.Group.invoke, non-chained case.

91 with ctx: 1 ctx 1O

92 sub_ctx = derivepassphrase_vault.make_context( 1 ctx 1O

93 'vault', ctx.args, parent=ctx

94 )

95 with sub_ctx: 1 ctx 1O

96 return derivepassphrase_vault.invoke(sub_ctx) 1 ctx 1O

97 return None 12 ctx 1:-;.q=/?W9+w

98

99

100# Exporter

101# ========

102

103

104@derivepassphrase.group( (empty)

105 'export',

106 context_settings={

107 'help_option_names': ['-h', '--help'],

108 'ignore_unknown_options': True,

109 'allow_interspersed_args': False,

110 },

111 invoke_without_command=True,

112 cls=cli_machinery.DefaultToVaultGroup,

113 help=(

114 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_01),

115 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_02),

116 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_03),

117 ),

118)

119@cli_machinery.version_option(cli_machinery.export_version_option_callback) (empty)

120@cli_machinery.color_forcing_pseudo_option (empty)

121@cli_machinery.standard_logging_options (empty)

122@click.pass_context (empty)

123 def derivepassphrase_export(ctx: click.Context, /) -> None: (empty)

124 """Export a foreign configuration to standard output.

125

126 This is a [`click`][CLICK]-powered command-line interface function,

127 and not intended for programmatic use. See the

128 derivepassphrase-export(1) manpage for full documentation of the

129 interface. (See also [`click.testing.CliRunner`][] for controlled,

130 programmatic invocation.)

131

132 [CLICK]: https://pypi.org/package/click/

133

134 """

135 # TODO(the-13th-letter): Turn this callback into a no-op in v1.0.

136 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands

137 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 5 ctx 1-./9+

138 if ctx.invoked_subcommand is None: 5 ctx 1-./9+

139 deprecation.warning( 1 ctx 1+

140 _msg.TranslatedString(

141 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED

142 ),

143 extra={'color': ctx.color},

144 )

145 # See definition of click.Group.invoke, non-chained case.

146 with ctx: 1 ctx 1+

147 sub_ctx = derivepassphrase_export_vault.make_context( 1 ctx 1+

148 'vault', ctx.args, parent=ctx

149 )

150 # Constructing the subcontext above will usually already

151 # lead to a click.UsageError, so this block typically won't

152 # actually be called.

153 with sub_ctx: # pragma: no cover

154 return derivepassphrase_export_vault.invoke(sub_ctx) 1 ctx 1+

155 return None 4 ctx 1-./9

156

157

158@derivepassphrase_export.command( (empty)

159 'vault',

160 context_settings={'help_option_names': ['-h', '--help']},

161 cls=cli_machinery.CommandWithHelpGroups,

162 help=(

163 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_01),

164 _msg.TranslatedString(

165 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_02,

166 path_metavar=_msg.TranslatedString(

167 _msg.Label.EXPORT_VAULT_METAVAR_PATH,

168 ),

169 ),

170 _msg.TranslatedString(

171 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_03,

172 path_metavar=_msg.TranslatedString(

173 _msg.Label.EXPORT_VAULT_METAVAR_PATH,

174 ),

175 ),

176 ),

177)

178@click.option( (empty)

179 '-f',

180 '--format',

181 'formats',

182 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT),

183 multiple=True,

184 default=('v0.3', 'v0.2', 'storeroom'),

185 type=click.Choice(['v0.2', 'v0.3', 'storeroom']),

186 help=_msg.TranslatedString(

187 _msg.Label.EXPORT_VAULT_FORMAT_HELP_TEXT,

188 defaults_hint=_msg.TranslatedString(

189 _msg.Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT,

190 ),

191 metavar=_msg.TranslatedString(

192 _msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT,

193 ),

194 ),

195 cls=cli_machinery.StandardOption,

196)

197@click.option( (empty)

198 '-k',

199 '--key',

200 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K),

201 help=_msg.TranslatedString(

202 _msg.Label.EXPORT_VAULT_KEY_HELP_TEXT,

203 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K),

204 defaults_hint=_msg.TranslatedString(

205 _msg.Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT,

206 ),

207 ),

208 cls=cli_machinery.StandardOption,

209)

210@cli_machinery.version_option( (empty)

211 cli_machinery.export_vault_version_option_callback

212)

213@cli_machinery.color_forcing_pseudo_option (empty)

214@cli_machinery.standard_logging_options (empty)

215@click.argument( (empty)

216 'path',

217 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH),

218 required=True,

219 shell_complete=cli_helpers.shell_complete_path,

220)

221@click.pass_context (empty)

222 def derivepassphrase_export_vault( (empty)

223 ctx: click.Context,

224 /,

225 *,

226 path: str | bytes | os.PathLike[str] | None,

227 formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (),

228 key: str | bytes | None = None,

229) -> None:

230 """Export a vault-native configuration to standard output.

231

232 This is a [`click`][CLICK]-powered command-line interface function,

233 and not intended for programmatic use. See the

234 derivepassphrase-export-vault(1) manpage for full documentation of

235 the interface. (See also [`click.testing.CliRunner`][] for

236 controlled, programmatic invocation.)

237

238 [CLICK]: https://pypi.org/package/click/

239

240 """

241 logger = logging.getLogger(PROG_NAME) 10 ctx 19'(#)$*%,!

242 if path in {'VAULT_PATH', b'VAULT_PATH'}: 10 ctx 19'(#)$*%,!

243 path = None 4 ctx 19'#!

244 if isinstance(key, str): # pragma: no branch 10 ctx 19'(#)$*%,!

245 key = key.encode('utf-8') 2 ctx 1(#

246 for fmt in formats: 10 ctx 19'(#)$*%,!

247 try: 10 ctx 19'(#)$*%,!

248 config = exporter.export_vault_config_data(path, key, format=fmt) 10 ctx 19'(#)$*%,!

249 except ( 5 ctx 1)$*%!

250 IsADirectoryError,

251 NotADirectoryError,

252 exporter.NotAVaultConfigError,

253 RuntimeError,

254 ):

255 logger.info( 2 ctx 1$%

256 _msg.TranslatedString(

257 _msg.InfoMsgTemplate.CANNOT_LOAD_AS_VAULT_CONFIG,

258 path=path or exporter.get_vault_path(),

259 fmt=fmt,

260 ),

261 extra={'color': ctx.color},

262 )

263 continue 2 ctx 1$%

264 except OSError as exc: 3 ctx 1)*!

265 logger.error( 2 ctx 1)*

266 _msg.TranslatedString(

267 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR,

268 path=path,

269 error=exc.strerror,

270 filename=exc.filename,

271 ).maybe_without_filename(),

272 extra={'color': ctx.color},

273 )

274 ctx.exit(1) 2 ctx 1)*

275 except ModuleNotFoundError: 1 ctx 1!

276 logger.error( 1 ctx 1!

277 _msg.TranslatedString(

278 _msg.ErrMsgTemplate.MISSING_MODULE,

279 module='cryptography',

280 ),

281 extra={'color': ctx.color},

282 )

283 logger.info( 1 ctx 1!

284 _msg.TranslatedString(

285 _msg.InfoMsgTemplate.PIP_INSTALL_EXTRA,

286 extra_name='export',

287 ),

288 extra={'color': ctx.color},

289 )

290 ctx.exit(1) 1 ctx 1!

291 else:

292 if not _types.is_vault_config(config): 5 ctx 19'(#,

293 logger.error( 1 ctx 1,

294 _msg.TranslatedString(

295 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG,

296 config=config,

297 ),

298 extra={'color': ctx.color},

299 )

300 ctx.exit(1) 1 ctx 1,

301 click.echo( 4 ctx 19'(#

302 json.dumps(

303 config, ensure_ascii=False, indent=2, sort_keys=True

304 ),

305 color=ctx.color,

306 )

307 break 4 ctx 19'(#

308 else:

309 logger.error( 2 ctx 1$%

310 _msg.TranslatedString(

311 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG,

312 path=path,

313 ).maybe_without_filename(),

314 extra={'color': ctx.color},

315 )

316 ctx.exit(1) 2 ctx 1$%

317

318

319 class _VaultContext: # noqa: PLR0904 (empty)

320 """The context for the "vault" command-line interface.

321

322 This context object -- wrapping a [`click.Context`][] object --

323 encapsulates a single call to the `derivepassphrase vault`

324 command-line. Although fully documented, this class is an

325 implementation detail of the `derivepassphrase vault` command-line

326 and should not be instantiated directly by users or API clients.

327

328 Attributes:

329 logger:

330 The logger used for warnings and error messages.

331 deprecation:

332 The logger used for deprecation warnings.

333 ctx:

334 The underlying [`click.Context`][] from which the

335 command-line settings and parameter values are queried.

336 all_ops:

337 A list of operations supported by the CLI, in the

338 order that they are queried in the `click` context. The

339 final entry is the default operation, and does not

340 correspond to a `click` parameter; all others are `click`

341 parameter names.

342 readwrite_ops:

343 A set of operations which modify the `derivepassphrase`

344 state. All other operations are read-only.

345 options_in_group:

346 A mapping of option group names to lists of known options

347 from this group. Used during the validation of the command

348 line.

349 params_by_str:

350 A mapping of option names (long names, short names, etc.) to

351 option objects. Used during the validation of the command

352 line.

353

354 """

355

356 logger: Final = logging.getLogger(PROG_NAME) (empty)

357 """""" (empty)

358 deprecation: Final = logging.getLogger(PROG_NAME + '.deprecation') (empty)

359 """""" (empty)

360 ctx: Final[click.Context] (empty)

361 """""" (empty)

362 all_ops: tuple[str, ...] = ( (empty)

363 'delete_service_settings',

364 'delete_globals',

365 'clear_all_settings',

366 'import_settings',

367 'export_settings',

368 'store_config_only',

369 # The default op "derive_passphrase" must be last!

370 'derive_passphrase',

371 )

372 readwrite_ops: AbstractSet[str] = frozenset({ (empty)

373 'delete_service_settings',

374 'delete_globals',

375 'clear_all_settings',

376 'import_settings',

377 'store_config_only',

378 })

379 options_in_group: dict[type[click.Option], list[click.Option]] (empty)

380 params_by_str: dict[str, click.Parameter] (empty)

381

382 def __init__(self, ctx: click.Context, /) -> None: (empty)

383 """Initialize the vault context.

384

385 Args:

386 ctx:

387 The underlying [`click.Context`][] from which the

388 command-line settings and parameter values are queried.

389

390 """ # noqa: DOC501

391 self.ctx = ctx 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

392 self.params_by_str = {} 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

393 self.options_in_group = {} 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

394 for param in ctx.command.params: 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

395 if isinstance(param, click.Option): 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

396 group: type[click.Option]

397 known_option_groups = [ 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

398 cli_machinery.PassphraseGenerationOption,

399 cli_machinery.ConfigurationOption,

400 cli_machinery.StorageManagementOption,

401 cli_machinery.LoggingOption,

402 cli_machinery.CompatibilityOption,

403 cli_machinery.StandardOption,

404 ]

405 if isinstance(param, cli_machinery.OptionGroupOption): 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

406 for class_ in known_option_groups: 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

407 if isinstance(param, class_): 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

408 group = class_ 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

409 break 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

410 else:

411 raise AssertionError( # noqa: TRY003

412 f'Unknown option group for {param! r}' # noqa: EM102

413 )

414 else:

415 group = click.Option 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

416 self.options_in_group.setdefault(group, []).append(param) 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

417 self.params_by_str[param.human_readable_name] = param 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

418 for name in param.opts + param.secondary_opts: 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

419 self.params_by_str[name] = param 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

420

421 def is_param_set(self, param: click.Parameter, /) -> bool: (empty)

422 """Return true if the parameter is set."""

423 return bool(self.ctx.params.get(param.human_readable_name)) 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

424

425 def option_name(self, param: click.Parameter | str, /) -> str: (empty)

426 """Return the option name of a parameter.

427

428 Annoyingly, `param.human_readable_name` contains the *function*

429 parameter name, not the list of option names. *Those* are

430 stashed in the `.opts` and `.secondary_opts` attributes, which

431 are visible in the `.to_info_dict()` output, but not otherwise

432 documented. We return the shortest one among the long-form

433 option names.

434

435 Args:

436 param: The parameter whose option name is requested.

437

438 Raises:

439 ValueError: The parameter has no long-form option names.

440

441 """

442 param = self.params_by_str[param] if isinstance(param, str) else param 1 ctx 18

443 names = [param.human_readable_name, *param.opts, *param.secondary_opts] 1 ctx 18

444 option_names = [n for n in names if n.startswith('--')] 1 ctx 18

445 return min(option_names, key=len) 1 ctx 18

446

447 def check_incompatible_options( (empty)

448 self,

449 param1: click.Parameter | str,

450 param2: click.Parameter | str,

451 ) -> None:

452 """Raise an error if the two options are incompatible.

453

454 Raises:

455 click.BadOptionUsage: The given options are incompatible.

456

457 """

458 param1 = ( 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

459 self.params_by_str[param1] if isinstance(param1, str) else param1

460 )

461 param2 = ( 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

462 self.params_by_str[param2] if isinstance(param2, str) else param2

463 )

464 if param1 == param2: 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

465 return 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

466 if not self.is_param_set(param1): 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

467 return 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

468 if self.is_param_set(param2): 52 ctx 1adqABpbf8zYZ17W360ntDPxuKQRSsmlcTr24JwUV5ghEeyvikjoC

469 param1_str = self.option_name(param1) 1 ctx 18

470 param2_str = self.option_name(param2) 1 ctx 18

471 raise click.BadOptionUsage( 1 ctx 18

472 param1_str,

473 str(

474 _msg.TranslatedString(

475 _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE,

476 param1=param1_str,

477 param2=param2_str,

478 )

479 ),

480 ctx=self.ctx,

481 )

482 return 52 ctx 1adqABpbf8zYZ17W360ntDPxuKQRSsmlcTr24JwUV5ghEeyvikjoC

483

484 def err(self, msg: Any, /, **kwargs: Any) -> NoReturn: # noqa: ANN401 (empty)

485 """Log an error, then abort the function call.

486

487 We ensure that color handling is done properly before the error

488 is logged.

489

490 """

491 stacklevel = kwargs.pop('stacklevel', 1) 23 ctx 1YZ1360nDPxuQRSmlTr24Jko

492 stacklevel += 1 23 ctx 1YZ1360nDPxuQRSmlTr24Jko

493 extra = kwargs.pop('extra', {}) 23 ctx 1YZ1360nDPxuQRSmlTr24Jko

494 extra.setdefault('color', self.ctx.color) 23 ctx 1YZ1360nDPxuQRSmlTr24Jko

495 self.logger.error(msg, stacklevel=stacklevel, extra=extra, **kwargs) 23 ctx 1YZ1360nDPxuQRSmlTr24Jko

496 self.ctx.exit(1) 23 ctx 1YZ1360nDPxuQRSmlTr24Jko

497

498 def warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401 (empty)

499 """Log a warning.

500

501 We ensure that color handling is done properly before the

502 warning is logged.

503

504 """

505 stacklevel = kwargs.pop('stacklevel', 1) 15 ctx 1qpfzJVghEeyvijC

506 stacklevel += 1 15 ctx 1qpfzJVghEeyvijC

507 extra = kwargs.pop('extra', {}) 15 ctx 1qpfzJVghEeyvijC

508 extra.setdefault('color', self.ctx.color) 15 ctx 1qpfzJVghEeyvijC

509 self.logger.warning(msg, stacklevel=stacklevel, extra=extra, **kwargs) 15 ctx 1qpfzJVghEeyvijC

510

511 def deprecation_warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401 (empty)

512 """Log a deprecation warning.

513

514 We ensure that color handling is done properly before the

515 warning is logged.

516

517 """

518 stacklevel = kwargs.pop('stacklevel', 1) 2 ctx 1UV

519 stacklevel += 1 2 ctx 1UV

520 extra = kwargs.pop('extra', {}) 2 ctx 1UV

521 extra.setdefault('color', self.ctx.color) 2 ctx 1UV

522 self.deprecation.warning( 2 ctx 1UV

523 msg, stacklevel=stacklevel, extra=extra, **kwargs

524 )

525

526 def deprecation_info(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401 (empty)

527 """Log a deprecation info message.

528

529 We ensure that color handling is done properly before the

530 warning is logged.

531

532 """

533 stacklevel = kwargs.pop('stacklevel', 1) 1 ctx 1U

534 stacklevel += 1 1 ctx 1U

535 extra = kwargs.pop('extra', {}) 1 ctx 1U

536 extra.setdefault('color', self.ctx.color) 1 ctx 1U

537 self.deprecation.info( 1 ctx 1U

538 msg, stacklevel=stacklevel, extra=extra, **kwargs

539 )

540

541 def get_config(self) -> _types.VaultConfig: (empty)

542 """Return the vault configuration stored on disk.

543

544 If no configuration is stored, return an empty configuration.

545

546 If only a v0.1-style configuration is found, attempt to migrate

547 it. This will be removed in v1.0.

548

549 """

550 try: 54 ctx 1adqABHLMIpbfz7W360ntDPxuKQRSXNsmlcrJwOUV5ghEeyGvFikjoC

551 return cli_helpers.load_config() 54 ctx 1adqABHLMIpbfz7W360ntDPxuKQRSXNsmlcrJwOUV5ghEeyGvFikjoC

552 except FileNotFoundError: 16 ctx 1qABW360XNsmlwOUV

553 # TODO(the-13th-letter): Return the empty default

554 # configuration directly.

555 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file

556 try: 14 ctx 1qABW0XNsmlwOUV

557 backup_config, exc = cli_helpers.migrate_and_load_old_config() 14 ctx 1qABW0XNsmlwOUV

558 except FileNotFoundError: 12 ctx 1qABW0XNsmlwO

559 return {'services': {}} 12 ctx 1qABW0XNsmlwO

560 old_name = cli_helpers.config_filename( 2 ctx 1UV

561 subsystem='old settings.json'

562 ).name

563 new_name = cli_helpers.config_filename(subsystem='vault').name 2 ctx 1UV

564 self.deprecation_warning( 2 ctx 1UV

565 _msg.TranslatedString(

566 _msg.WarnMsgTemplate.V01_STYLE_CONFIG,

567 old=old_name,

568 new=new_name,

569 ),

570 )

571 if isinstance(exc, OSError): 2 ctx 1UV

572 self.warning( 1 ctx 1V

573 _msg.TranslatedString(

574 _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG,

575 path=new_name,

576 error=exc.strerror,

577 filename=exc.filename,

578 ).maybe_without_filename(),

579 )

580 else:

581 self.deprecation_info( 1 ctx 1U

582 _msg.TranslatedString(

583 _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED,

584 path=new_name,

585 ),

586 )

587 return backup_config 2 ctx 1UV

588 except OSError as exc: 2 ctx 136

589 self.err( 1 ctx 16

590 _msg.TranslatedString(

591 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,

592 error=exc.strerror,

593 filename=exc.filename,

594 ).maybe_without_filename(),

595 )

596 except Exception as exc: # noqa: BLE001 1 ctx 13

597 self.err( 1 ctx 13

598 _msg.TranslatedString(

599 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS,

600 error=str(exc),

601 filename=None,

602 ).maybe_without_filename(),

603 exc_info=exc,

604 )

605

606 def put_config(self, config: _types.VaultConfig, /) -> None: (empty)

607 """Store the given vault configuration to disk."""

608 try: 24 ctx 1adqpbfztxusmlc5ghEeyvikj

609 cli_helpers.save_config(config) 24 ctx 1adqpbfztxusmlc5ghEeyvikj

610 except OSError as exc: 4 ctx 1xuml

611 self.err( 2 ctx 1xm

612 _msg.TranslatedString(

613 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,

614 error=exc.strerror,

615 filename=exc.filename,

616 ).maybe_without_filename(),

617 )

618 except Exception as exc: # noqa: BLE001 2 ctx 1ul

619 self.err( 2 ctx 1ul

620 _msg.TranslatedString(

621 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS,

622 error=str(exc),

623 filename=None,

624 ).maybe_without_filename(),

625 exc_info=exc,

626 )

627

628 def get_user_config(self) -> dict[str, Any]: (empty)

629 """Return the global user configuration stored on disk.

630

631 If no configuration is stored, return an empty configuration.

632

633 """

634 try: 52 ctx 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlcTr24JwOghEeyGvFikjoC

635 return cli_helpers.load_user_config() 52 ctx 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlcTr24JwOghEeyGvFikjoC

636 except FileNotFoundError: 49 ctx 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsml24JwOghEeyGvFikjoC

637 return {} 47 ctx 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlJwOghEeyGvFikjoC

638 except OSError as exc: 2 ctx 124

639 self.err( 1 ctx 14

640 _msg.TranslatedString(

641 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,

642 error=exc.strerror,

643 filename=exc.filename,

644 ).maybe_without_filename(),

645 )

646 except Exception as exc: # noqa: BLE001 1 ctx 12

647 self.err( 1 ctx 12

648 _msg.TranslatedString(

649 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG,

650 error=str(exc),

651 filename=None,

652 ).maybe_without_filename(),

653 exc_info=exc,

654 )

655

656 def validate_command_line(self) -> None: # noqa: C901,PLR0912 (empty)

657 """Check for missing/extra arguments and conflicting options.

658

659 Raises:

660 click.UsageError:

661 The command-line is invalid because of missing or extra

662 arguments, or because of conflicting options. The error

663 message contains further details.

664

665 """

666 param: click.Parameter

667 self.check_incompatible_options('--phrase', '--key') 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

668 for group in ( 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

669 cli_machinery.ConfigurationOption,

670 cli_machinery.StorageManagementOption,

671 ):

672 for opt in self.options_in_group[group]: 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

673 if opt not in { 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

674 self.params_by_str['--config'],

675 self.params_by_str['--notes'],

676 }:

677 for other_opt in self.options_in_group[ 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

678 cli_machinery.PassphraseGenerationOption

679 ]:

680 self.check_incompatible_options(opt, other_opt) 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

681

682 for group in ( 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

683 cli_machinery.ConfigurationOption,

684 cli_machinery.StorageManagementOption,

685 ):

686 for opt in self.options_in_group[group]: 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

687 for other_opt in self.options_in_group[ 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

688 cli_machinery.ConfigurationOption

689 ]:

690 if {opt, other_opt} != { 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

691 self.params_by_str['--config'],

692 self.params_by_str['--notes'],

693 }:

694 self.check_incompatible_options(opt, other_opt) 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

695 for other_opt in self.options_in_group[ 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

696 cli_machinery.StorageManagementOption

697 ]:

698 self.check_incompatible_options(opt, other_opt) 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

699 service: str | None = self.ctx.params['service'] 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

700 service_metavar = _msg.TranslatedString( 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

701 _msg.Label.VAULT_METAVAR_SERVICE

702 )

703 sv_or_global_options = self.options_in_group[ 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

704 cli_machinery.PassphraseGenerationOption

705 ]

706 for param in sv_or_global_options: 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

707 if self.is_param_set(param) and not ( 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

708 service is not None

709 or self.is_param_set(self.params_by_str['--config'])

710 ):

711 err_msg = _msg.TranslatedString( 1 ctx 1b

712 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG,

713 param=param.opts[0],

714 service_metavar=service_metavar,

715 )

716 raise click.UsageError(str(err_msg)) 1 ctx 1b

717 sv_options = [ 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

718 self.params_by_str['--notes'],

719 self.params_by_str['--delete'],

720 ]

721 for param in sv_options: 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

722 if self.is_param_set(param) and not service is not None: 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

723 err_msg = _msg.TranslatedString( 1 ctx 1b

724 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE,

725 param=param.opts[0],

726 service_metavar=service_metavar,

727 )

728 raise click.UsageError(str(err_msg)) 1 ctx 1b

729 no_sv_options = [ 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

730 self.params_by_str['--delete-globals'],

731 self.params_by_str['--clear'],

732 *self.options_in_group[cli_machinery.StorageManagementOption],

733 ]

734 for param in no_sv_options: 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

735 if self.is_param_set(param) and service is not None: 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

736 err_msg = _msg.TranslatedString( 1 ctx 1b

737 _msg.ErrMsgTemplate.PARAMS_NO_SERVICE,

738 param=param.opts[0],

739 service_metavar=service_metavar,

740 )

741 raise click.UsageError(str(err_msg)) 1 ctx 1b

742

743 def get_mutex( (empty)

744 self,

745 op: str,

746 ) -> AbstractContextManager[AbstractContextManager | None]:

747 """Return a mutex for accessing the configuration on disk.

748

749 The mutex is a context manager, and will lock out other threads

750 and processes attempting to access the configuration in an

751 incompatible manner.

752

753 Returns:

754 If the requested operation is a read-only operation, return

755 a no-op mutex. (Concurrent reads are always allowed, even

756 in the presence of writers.) Otherwise, for read-write

757 operations, return an actual mutex.

758

759 """

760 return ( 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

761 cli_helpers.configuration_mutex()

762 if op in self.readwrite_ops

763 else contextlib.nullcontext()

764 )

765

766 def dispatch_op(self) -> None: (empty)

767 """Dispatch to the handler matching the command-line call.

768

769 Also issue any appropriate warnings about the command-line,

770 e.g., incompatibilities with vault(1) or ineffective options.

771

772 """

773 service_metavar = _msg.TranslatedString( 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

774 _msg.Label.VAULT_METAVAR_SERVICE

775 )

776 if self.ctx.params['service'] == '': # noqa: PLC1901 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

777 self.warning( 1 ctx 1f

778 _msg.TranslatedString(

779 _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED,

780 service_metavar=service_metavar,

781 ),

782 )

783 if self.ctx.params.get('edit_notes') and not self.ctx.params.get( 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

784 'store_config_only'

785 ):

786 self.warning( 1 ctx 1C

787 _msg.TranslatedString(

788 _msg.WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG,

789 service_metavar=service_metavar,

790 ),

791 )

792 op: str

793 for candidate_op in self.all_ops[:-1]: 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

794 if self.ctx.params.get(candidate_op): 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

795 op = candidate_op 45 ctx 1adqpbfzYZ17W360ntDPxuKQRSsmlcTrJUV5ghEeyvikjo

796 break 45 ctx 1adqpbfzYZ17W360ntDPxuKQRSsmlcTrJUV5ghEeyvikjo

797 else:

798 op = self.all_ops[-1] 18 ctx 1ABHLMIbXNcr24wOGFC

799 with self.get_mutex(op): 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

800 op_func = getattr(self, 'run_op_' + op) 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

801 return op_func() 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

802

803 def run_op_delete_service_settings(self) -> None: (empty)

804 """Delete settings for a specific service."""

805 service = self.ctx.params['service'] 3 ctx 1ad5

806 assert service is not None 3 ctx 1ad5

807 configuration = self.get_config() 3 ctx 1ad5

808 if service in configuration['services']: 3 ctx 1ad5

809 del configuration['services'][service] 3 ctx 1ad5

810 self.put_config(configuration) 3 ctx 1ad5

811

812 def run_op_delete_globals(self) -> None: (empty)

813 """Delete the global settings."""

814 configuration = self.get_config() 4 ctx 1adb5

815 if 'global' in configuration: 4 ctx 1adb5

816 del configuration['global'] 4 ctx 1adb5

817 self.put_config(configuration) 4 ctx 1adb5

818

819 def run_op_clear_all_settings(self) -> None: (empty)

820 """Clear all settings."""

821 self.put_config({'services': {}}) 8 ctx 1adb5hEey

822

823 def run_op_import_settings(self) -> None: # noqa: C901,PLR0912 (empty)

824 """Import settings from a given file.

825

826 Issue multiple warnings, if appropriate, e.g. for Unicode

827 normalization issues with stored passphrases or conflicting

828 stored passphrases and keys. Respect the `--overwrite-config`

829 and `--merge-config` options when writing the imported

830 configuration to disk.

831

832 """

833 service_metavar = _msg.TranslatedString( 17 ctx 1adqbfzYZ1cTghEeyv

834 _msg.Label.VAULT_METAVAR_SERVICE

835 )

836 user_config = self.get_user_config() 17 ctx 1adqbfzYZ1cTghEeyv

837 import_settings = self.ctx.params['import_settings'] 17 ctx 1adqbfzYZ1cTghEeyv

838 overwrite_config = self.ctx.params['overwrite_config'] 17 ctx 1adqbfzYZ1cTghEeyv

839 try: 17 ctx 1adqbfzYZ1cTghEeyv

840 # TODO(the-13th-letter): keep track of auto-close; try

841 # os.dup if feasible

842 infile = cast( 17 ctx 1adqbfzYZ1cTghEeyv

843 'TextIO',

844 (

845 import_settings

846 if hasattr(import_settings, 'close')

847 else click.open_file(os.fspath(import_settings), 'rt')

848 ),

849 )

850 # Don't specifically catch TypeError or ValueError here if

851 # the passed-in fileobj is not a readable text stream. This

852 # will never happen on the command-line (thanks to `click`),

853 # and for programmatic use, our caller may want accurate

854 # error information.

855 with infile: 16 ctx 1adqbfzYZcTghEeyv

856 maybe_config = json.load(infile) 16 ctx 1adqbfzYZcTghEeyv

857 except json.JSONDecodeError as exc: 2 ctx 1Z1

858 self.err( 1 ctx 1Z

859 _msg.TranslatedString(

860 _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS,

861 error=exc,

862 )

863 )

864 except OSError as exc: 1 ctx 11

865 self.err( 1 ctx 11

866 _msg.TranslatedString(

867 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,

868 error=exc.strerror,

869 filename=exc.filename,

870 ).maybe_without_filename()

871 )

872 cleaned = _types.clean_up_falsy_vault_config_values(maybe_config) 15 ctx 1adqbfzYcTghEeyv

873 if not _types.is_vault_config(maybe_config): 15 ctx 1adqbfzYcTghEeyv

874 self.err( 1 ctx 1Y

875 _msg.TranslatedString(

876 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS,

877 error=_msg.TranslatedString(

878 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG,

879 config=maybe_config,

880 ),

881 filename=None,

882 ).maybe_without_filename()

883 )

884 assert cleaned is not None 14 ctx 1adqbfzcTghEeyv

885 for step in cleaned: 14 ctx 1adqbfzcTghEeyv

886 # These are never fatal errors, because the semantics of

887 # vault upon encountering these settings are ill-specified,

888 # but not ill-defined.

889 if step.action == 'replace': 3 ctx 1eyv

890 self.warning( 3 ctx 1eyv

891 _msg.TranslatedString(

892 _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE,

893 old=json.dumps(step.old_value),

894 path=_types.json_path(step.path),

895 new=json.dumps(step.new_value),

896 ),

897 )

898 else:

899 self.warning( 1 ctx 1v

900 _msg.TranslatedString(

901 _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE,

902 path=_types.json_path(step.path),

903 old=json.dumps(step.old_value),

904 ),

905 )

906 if '' in maybe_config['services']: 14 ctx 1adqbfzcTghEeyv

907 self.warning( 2 ctx 1qf

908 _msg.TranslatedString(

909 _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE,

910 service_metavar=service_metavar,

911 PROG_NAME=PROG_NAME,

912 ),

913 )

914 for service_name in sorted(maybe_config['services'].keys()): 14 ctx 1adqbfzcTghEeyv

915 if not cli_helpers.is_completable_item(service_name): 11 ctx 1adqfzcTgeyv

916 self.warning( 1 ctx 1g

917 _msg.TranslatedString(

918 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,

919 service=service_name,

920 ),

921 )

922 try: 14 ctx 1adqbfzcTghEeyv

923 cli_helpers.check_for_misleading_passphrase( 14 ctx 1adqbfzcTghEeyv

924 ('global',),

925 cast('dict[str, Any]', maybe_config.get('global', {})),

926 main_config=user_config,

927 ctx=self.ctx,

928 )

929 for key, value in maybe_config['services'].items(): 14 ctx 1adqbfzcTghEeyv

930 cli_helpers.check_for_misleading_passphrase( 11 ctx 1adqfzcTgeyv

931 ('services', key),

932 cast('dict[str, Any]', value),

933 main_config=user_config,

934 ctx=self.ctx,

935 )

936 except AssertionError as exc: 1 ctx 1T

937 self.err( 1 ctx 1T

938 _msg.TranslatedString(

939 _msg.ErrMsgTemplate.INVALID_USER_CONFIG,

940 error=exc,

941 filename=None,

942 ).maybe_without_filename(),

943 )

944 global_obj = maybe_config.get('global', {}) 13 ctx 1adqbfzcghEeyv

945 has_key = _types.js_truthiness(global_obj.get('key')) 13 ctx 1adqbfzcghEeyv

946 has_phrase = _types.js_truthiness(global_obj.get('phrase')) 13 ctx 1adqbfzcghEeyv

947 if has_key and has_phrase: 13 ctx 1adqbfzcghEeyv

948 self.warning( 4 ctx 1zhEv

949 _msg.TranslatedString(

950 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE,

951 ),

952 )

953 for service_name, service_obj in maybe_config['services'].items(): 13 ctx 1adqbfzcghEeyv

954 has_key = _types.js_truthiness( 10 ctx 1adqfzcgeyv

955 service_obj.get('key')

956 ) or _types.js_truthiness(global_obj.get('key'))

957 has_phrase = _types.js_truthiness( 10 ctx 1adqfzcgeyv

958 service_obj.get('phrase')

959 ) or _types.js_truthiness(global_obj.get('phrase'))

960 if has_key and has_phrase: 10 ctx 1adqfzcgeyv

961 self.warning( 3 ctx 1zyv

962 _msg.TranslatedString(

963 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,

964 service=json.dumps(service_name),

965 ),

966 )

967 if overwrite_config: 13 ctx 1adqbfzcghEeyv

968 self.put_config(maybe_config) 2 ctx 1ad

969 else:

970 configuration = self.get_config() 13 ctx 1adqbfzcghEeyv

971 merged_config: collections.ChainMap[str, Any] = ( 13 ctx 1adqbfzcghEeyv

972 collections.ChainMap(

973 {

974 'services': collections.ChainMap(

975 maybe_config['services'],

976 configuration['services'],

977 ),

978 },

979 {'global': maybe_config['global']}

980 if 'global' in maybe_config

981 else {},

982 {'global': configuration['global']}

983 if 'global' in configuration

984 else {},

985 )

986 )

987 new_config: Any = { 13 ctx 1adqbfzcghEeyv

988 k: dict(v) if isinstance(v, collections.ChainMap) else v

989 for k, v in sorted(merged_config.items())

990 }

991 assert _types.is_vault_config(new_config) 13 ctx 1adqbfzcghEeyv

992 self.put_config(new_config) 13 ctx 1adqbfzcghEeyv

993

994 def run_op_export_settings(self) -> None: (empty)

995 """Export settings to a given file.

996

997 Respect the `--export-as` option when writing the exported

998 configuration to the file.

999

1000 """

1001 export_settings = self.ctx.params['export_settings'] 8 ctx 1b7W360UV

1002 export_as = self.ctx.params['export_as'] 8 ctx 1b7W360UV

1003 configuration = self.get_config() 8 ctx 1b7W360UV

1004 try: 6 ctx 1b7W0UV

1005 # TODO(the-13th-letter): keep track of auto-close; try

1006 # os.dup if feasible

1007 outfile = cast( 6 ctx 1b7W0UV

1008 'TextIO',

1009 (

1010 export_settings

1011 if hasattr(export_settings, 'close')

1012 else click.open_file(os.fspath(export_settings), 'wt')

1013 ),

1014 )

1015 # Don't specifically catch TypeError or ValueError here if

1016 # the passed-in fileobj is not a writable text stream. This

1017 # will never happen on the command-line (thanks to `click`),

1018 # and for programmatic use, our caller may want accurate

1019 # error information.

1020 with outfile: 5 ctx 1b7WUV

1021 if export_as == 'sh': 5 ctx 1b7WUV

1022 this_ctx = self.ctx 1 ctx 1W

1023 prog_name_pieces = collections.deque([ 1 ctx 1W

1024 this_ctx.info_name or 'vault',

1025 ])

1026 while ( 1 ctx 1W

1027 this_ctx.parent is not None

1028 and this_ctx.parent.info_name is not None

1029 ):

1030 prog_name_pieces.appendleft(this_ctx.parent.info_name) 1 ctx 1W

1031 this_ctx = this_ctx.parent 1 ctx 1W

1032 cli_helpers.print_config_as_sh_script( 1 ctx 1W

1033 configuration,

1034 outfile=outfile,

1035 prog_name_list=prog_name_pieces,

1036 )

1037 else:

1038 json.dump( 5 ctx 1b7WUV

1039 configuration,

1040 outfile,

1041 ensure_ascii=False,

1042 indent=2,

1043 sort_keys=True,

1044 )

1045 except OSError as exc: 1 ctx 10

1046 self.err( 1 ctx 10

1047 _msg.TranslatedString(

1048 _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS,

1049 error=exc.strerror,

1050 filename=exc.filename,

1051 ).maybe_without_filename(),

1052 )

1053

1054 def run_subop_query_phrase_or_key_change( (empty)

1055 self,

1056 *,

1057 empty_service_permitted: bool,

1058 configuration: _types.VaultConfig | None = None,

1059 ) -> collections.ChainMap[str, Any]:

1060 """Query the master passphrase or master SSH key, if changed.

1061

1062 If the user indicates they want to change the master passphrase

1063 or master SSH key for this call, or if they want to configure

1064 a stored global or service-specific master passphrase or master

1065 SSH key, then query the user.

1066

1067 This is not a complete command-line call operation in and of

1068 itself.

1069

1070 Args:

1071 empty_service_permitted:

1072 True if an empty service name is permitted, False otherwise.

1073 configuration:

1074 The vault configuration, parsed from disk. If not

1075 given, we read the configuration from disk ourselves.

1076

1077 The returned effective configuration already contains

1078 a relevant slice of the vault configuration. However,

1079 some callers need access to the full configuration *and*

1080 need the slice within the effective configuration to

1081 refer to the same object.

1082

1083 Returns:

1084 The effective configuration for the (possibly empty) given

1085 service, as a [chained map][collections.ChainMap]. Any

1086 master passphrase or master SSH key overrides that may be in

1087 effect are stored in the first map.

1088

1089 Raises:

1090 click.UsageError:

1091 The service name was empty, and an empty service name

1092 was not permitted as per the method parameters.

1093

1094 Warning:

1095 It is the caller's responsibility to vet any interactively

1096 entered master passphrase for Unicode normalization issues.

1097

1098 """

1099 service = self.ctx.params['service'] 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1100 use_key = self.ctx.params['use_key'] 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1101 use_phrase = self.ctx.params['use_phrase'] 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1102 if configuration is None: 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1103 configuration = self.get_config() 16 ctx 1ABHLMIbXNcrwOGFC

1104 service_keys_on_commandline = { 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1105 'length',

1106 'repeat',

1107 'lower',

1108 'upper',

1109 'number',

1110 'space',

1111 'dash',

1112 'symbol',

1113 }

1114 settings: collections.ChainMap[str, Any] = collections.ChainMap( 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1115 {

1116 k: v

1117 for k in service_keys_on_commandline

1118 if (v := self.ctx.params.get(k)) is not None

1119 },

1120 cast(

1121 'dict[str, Any]',

1122 configuration['services'].get(service, {}) if service else {},

1123 ),

1124 cast('dict[str, Any]', configuration.get('global', {})),

1125 )

1126 if not service and not empty_service_permitted: 41 ctx 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1127 err_msg = _msg.TranslatedString( 2 ctx 1XO

1128 _msg.ErrMsgTemplate.SERVICE_REQUIRED,

1129 service_metavar=_msg.TranslatedString(

1130 _msg.Label.VAULT_METAVAR_SERVICE

1131 ),

1132 )

1133 raise click.UsageError(str(err_msg)) 2 ctx 1XO

1134 if use_key: 39 ctx 1adABHLMIpbfntDPxuKQRSNsmlcrJwgheGFikjoC

1135 settings.maps[0]['key'] = base64.standard_b64encode( 9 ctx 1LMtDPQRSJ

1136 cli_helpers.select_ssh_key(

1137 ctx=self.ctx,

1138 error_callback=self.err,

1139 warning_callback=self.warning,

1140 )

1141 ).decode('ASCII')

1142 elif use_phrase: 32 ctx 1adABHIpbfntDxuKNsmlcrwgheGFikjoC

1143 maybe_phrase = cli_helpers.prompt_for_passphrase() 12 ctx 1ABpbtDsmlcrw

1144 if not maybe_phrase: 12 ctx 1ABpbtDsmlcrw

1145 self.err( 1 ctx 1D

1146 _msg.TranslatedString(

1147 _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE

1148 )

1149 )

1150 else:

1151 settings.maps[0]['phrase'] = maybe_phrase 11 ctx 1ABpbtsmlcrw

1152 return settings 34 ctx 1adABHLMIpbfntDxuKNsmlcrwgheGFikjoC

1153

1154 def run_op_store_config_only(self) -> None: # noqa: C901,PLR0912,PLR0914,PLR0915 (empty)

1155 """Update the stored vault configuration.

1156

1157 Depending on the presence or the absence of a service name,

1158 update either the service-specific settings, or the global

1159 settings. (An empty service name is respected, i.e., updates

1160 the former.)

1161

1162 Respect the `--unset=SETTING` option to unset the named

1163 settings, and the `--notes` option to edit notes interactively

1164 in a spawned text editor. Issue multiple warnings, if

1165 appropriate, e.g. for Unicode normalization issues with stored

1166 passphrases or conflicting stored passphrases and keys. Respect

1167 the `--overwrite-config` and `--merge-config` options when

1168 writing the imported configuration to disk, and the

1169 `--modern-editor-interface` and

1170 `--vault-legacy-editor-interface` options when editing notes.

1171

1172 Raises:

1173 click.UsageError:

1174 The user requested that the same setting be both unset

1175 and set.

1176

1177 """

1178 service = self.ctx.params['service'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1179 use_key = self.ctx.params['use_key'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1180 use_phrase = self.ctx.params['use_phrase'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1181 unset_settings = self.ctx.params['unset_settings'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1182 overwrite_config = self.ctx.params['overwrite_config'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1183 edit_notes = self.ctx.params['edit_notes'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1184 modern_editor_interface = self.ctx.params['modern_editor_interface'] 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1185 configuration = self.get_config() 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1186 user_config = self.get_user_config() 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1187 settings = self.run_subop_query_phrase_or_key_change( 28 ctx 1adpbfntDPxuKQRSsmlcrJgheikjo

1188 configuration=configuration, empty_service_permitted=True

1189 )

1190 overrides = settings.maps[0] 23 ctx 1adpbfntDxuKsmlcrgheikjo

1191 view: collections.ChainMap[str, Any]

1192 view = ( 23 ctx 1adpbfntDxuKsmlcrgheikjo

1193 collections.ChainMap(*settings.maps[:2])

1194 if service

1195 else collections.ChainMap(settings.maps[0], settings.maps[2])

1196 )

1197 if use_key: 23 ctx 1adpbfntDxuKsmlcrgheikjo

1198 view['key'] = overrides['key'] 1 ctx 1t

1199 elif use_phrase: 23 ctx 1adpbfntDxuKsmlcrgheikjo

1200 view['phrase'] = overrides['phrase'] 8 ctx 1pbtsmlcr

1201 try: 8 ctx 1pbtsmlcr

1202 cli_helpers.check_for_misleading_passphrase( 8 ctx 1pbtsmlcr

1203 ('services', service) if service else ('global',),

1204 overrides,

1205 main_config=user_config,

1206 ctx=self.ctx,

1207 )

1208 except AssertionError as exc: 1 ctx 1r

1209 self.err( 1 ctx 1r

1210 _msg.TranslatedString(

1211 _msg.ErrMsgTemplate.INVALID_USER_CONFIG,

1212 error=exc,

1213 filename=None,

1214 ).maybe_without_filename(),

1215 )

1216 if 'key' in settings: 7 ctx 1pbtsmlc

1217 if service: 1 ctx 1p

1218 w_msg = _msg.TranslatedString( 1 ctx 1p

1219 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE,

1220 service=json.dumps(service),

1221 )

1222 else:

1223 w_msg = _msg.TranslatedString( 1 ctx 1p

1224 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE

1225 )

1226 self.warning(w_msg) 1 ctx 1p

1227 if not view.maps[0] and not unset_settings and not edit_notes: 22 ctx 1adpbfntDxuKsmlcgheikjo

1228 err_msg = _msg.TranslatedString( 1 ctx 1D

1229 _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS,

1230 settings_type=_msg.TranslatedString(

1231 _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE

1232 if service

1233 else _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL # noqa: E501

1234 ),

1235 )

1236 raise click.UsageError(str(err_msg)) 1 ctx 1D

1237 for setting in unset_settings: 21 ctx 1adpbfntxuKsmlcgheikjo

1238 if setting in view.maps[0]: 3 ctx 1adK

1239 err_msg = _msg.TranslatedString( 1 ctx 1K

1240 _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING,

1241 setting=setting,

1242 )

1243 raise click.UsageError(str(err_msg)) 1 ctx 1K

1244 if not cli_helpers.is_completable_item(service): 20 ctx 1adpbfntxusmlcgheikjo

1245 self.warning( 1 ctx 1g

1246 _msg.TranslatedString(

1247 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,

1248 service=service,

1249 ),

1250 )

1251 subtree: dict[str, Any] = ( 20 ctx 1adpbfntxusmlcgheikjo

1252 configuration['services'].setdefault(service, {}) # type: ignore[assignment]

1253 if service

1254 else configuration.setdefault('global', {})

1255 )

1256 if overwrite_config: 20 ctx 1adpbfntxusmlcgheikjo

1257 subtree.clear() 2 ctx 1ad

1258 else:

1259 for setting in unset_settings: 20 ctx 1adpbfntxusmlcgheikjo

1260 subtree.pop(setting, None) 2 ctx 1ad

1261 subtree.update(view) 20 ctx 1adpbfntxusmlcgheikjo

1262 assert _types.is_vault_config(configuration), ( 20 ctx 1adpbfntxusmlcgheikjo

1263 f'Invalid vault configuration: {configuration! r}'

1264 )

1265 if edit_notes: 20 ctx 1adpbfntxusmlcgheikjo

1266 assert service is not None 5 ctx 1nikjo

1267 notes_instructions = _msg.TranslatedString( 5 ctx 1nikjo

1268 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT

1269 )

1270 notes_marker = _msg.TranslatedString( 5 ctx 1nikjo

1271 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER

1272 )

1273 notes_legacy_instructions = _msg.TranslatedString( 5 ctx 1nikjo

1274 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT

1275 )

1276 old_notes_value = subtree.get('notes', '') 5 ctx 1nikjo

1277 if modern_editor_interface: 5 ctx 1nikjo

1278 text = '\n'.join([ 5 ctx 1nikjo

1279 str(notes_instructions),

1280 str(notes_marker),

1281 old_notes_value,

1282 ])

1283 else:

1284 text = old_notes_value or str(notes_legacy_instructions) 3 ctx 1ikj

1285 notes_value = click.edit(text=text, require_save=False) 5 ctx 1nikjo

1286 assert notes_value is not None 5 ctx 1nikjo

1287 if ( 5 ctx 1nikjo

1288 not modern_editor_interface

1289 and notes_value.strip() != old_notes_value.strip()

1290 ):

1291 backup_file = cli_helpers.config_filename( 2 ctx 1ij

1292 subsystem='notes backup'

1293 )

1294 backup_file.write_text(old_notes_value, encoding='UTF-8') 2 ctx 1ij

1295 self.warning( 2 ctx 1ij

1296 _msg.TranslatedString(

1297 _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP,

1298 filename=str(backup_file),

1299 ),

1300 )

1301 subtree['notes'] = notes_value.strip() 2 ctx 1ij

1302 elif ( 5 ctx 1nikjo

1303 modern_editor_interface and notes_value.strip() != text.strip()

1304 ):

1305 notes_lines = collections.deque( 5 ctx 1nikjo

1306 notes_value.splitlines(keepends=True)

1307 )

1308 while notes_lines: 5 ctx 1nikjo

1309 line = notes_lines.popleft() 3 ctx 1ikj

1310 if line.startswith(str(notes_marker)): 3 ctx 1ikj

1311 notes_value = ''.join(notes_lines) 1 ctx 1i

1312 break 1 ctx 1i

1313 else:

1314 if not notes_value.strip(): 4 ctx 1nkjo

1315 self.err( 3 ctx 1nko

1316 _msg.TranslatedString(

1317 _msg.ErrMsgTemplate.USER_ABORTED_EDIT

1318 )

1319 )

1320 subtree['notes'] = notes_value.strip() 3 ctx 1ikj

1321 self.put_config(configuration) 18 ctx 1adpbftxusmlcgheikj

1322

1323 def run_op_derive_passphrase(self) -> None: (empty)

1324 """Derive a passphrase.

1325

1326 Derive a service passphrase using the effective settings from

1327 both the command-line and the stored configuration. If any

1328 service notes are stored for this service, print them as well.

1329

1330 (An empty service name is permitted, though discouraged for

1331 compatibility reasons.)

1332

1333 Issue a warning (if appropriate) for Unicode normalization

1334 issues with the interactive passphrase. Respect the

1335 `--print-notes-before` and `--print-notes-after` options when

1336 printing notes.

1337

1338 Raises:

1339 click.UsageError:

1340 No master passphrase or master SSH key was given on both

1341 the command-line and in the vault configuration on disk.

1342 """

1343 service = self.ctx.params['service'] 18 ctx 1ABHLMIbXNcr24wOGFC

1344 use_key = self.ctx.params['use_key'] 18 ctx 1ABHLMIbXNcr24wOGFC

1345 use_phrase = self.ctx.params['use_phrase'] 18 ctx 1ABHLMIbXNcr24wOGFC

1346 print_notes_before = self.ctx.params['print_notes_before'] 18 ctx 1ABHLMIbXNcr24wOGFC

1347 user_config = self.get_user_config() 18 ctx 1ABHLMIbXNcr24wOGFC

1348 settings = self.run_subop_query_phrase_or_key_change( 16 ctx 1ABHLMIbXNcrwOGFC

1349 empty_service_permitted=False

1350 )

1351 if use_phrase: 14 ctx 1ABHLMIbNcrwGFC

1352 try: 6 ctx 1ABbcrw

1353 cli_helpers.check_for_misleading_passphrase( 6 ctx 1ABbcrw

1354 cli_helpers.ORIGIN.INTERACTIVE,

1355 {'phrase': settings['phrase']},

1356 main_config=user_config,

1357 ctx=self.ctx,

1358 )

1359 except AssertionError as exc: 1 ctx 1r

1360 self.err( 1 ctx 1r

1361 _msg.TranslatedString(

1362 _msg.ErrMsgTemplate.INVALID_USER_CONFIG,

1363 error=exc,

1364 filename=None,

1365 ).maybe_without_filename(),

1366 )

1367 phrase: str | bytes

1368 overrides = cast('dict[str, int | str]', settings.maps[0]) 13 ctx 1ABHLMIbNcwGFC

1369 # If either --key or --phrase are given, use that setting.

1370 # Otherwise, if both key and phrase are set in the config,

1371 # use the key. Otherwise, if only one of key and phrase is

1372 # set in the config, use that one. In all these above

1373 # cases, set the phrase via vault.Vault.phrase_from_key if

1374 # a key is given. Finally, if nothing is set, error out.

1375 if use_key: 13 ctx 1ABHLMIbNcwGFC

1376 phrase = cli_helpers.key_to_phrase( 2 ctx 1LM

1377 cast('str', overrides['key']),

1378 error_callback=self.err,

1379 warning_callback=self.warning,

1380 )

1381 elif use_phrase: 11 ctx 1ABHIbNcwGFC

1382 phrase = cast('str', overrides['phrase']) 5 ctx 1ABbcw

1383 elif settings.get('key'): 7 ctx 1HIbNGFC

1384 phrase = cli_helpers.key_to_phrase( 2 ctx 1HI

1385 cast('str', settings['key']), error_callback=self.err

1386 )

1387 elif settings.get('phrase'): 5 ctx 1bNGFC

1388 phrase = cast('str', settings['phrase']) 4 ctx 1bGFC

1389 else:

1390 err_msg = _msg.TranslatedString( 1 ctx 1N

1391 _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE

1392 )

1393 raise click.UsageError(str(err_msg)) 1 ctx 1N

1394 overrides.pop('key', '') 12 ctx 1ABHLMIbcwGFC

1395 overrides.pop('phrase', '') 12 ctx 1ABHLMIbcwGFC

1396 assert service is not None 12 ctx 1ABHLMIbcwGFC

1397 vault_service_keys = { 12 ctx 1ABHLMIbcwGFC

1398 'length',

1399 'repeat',

1400 'lower',

1401 'upper',

1402 'number',

1403 'space',

1404 'dash',

1405 'symbol',

1406 }

1407 kwargs = { 12 ctx 1ABHLMIbcwGFC

1408 cast('str', k): cast('int', settings[k])

1409 for k in vault_service_keys

1410 if k in settings

1411 }

1412 result = vault.Vault(phrase=phrase, **kwargs).generate(service) 12 ctx 1ABHLMIbcwGFC

1413 service_notes = cast('str', settings.get('notes', '')).strip() 12 ctx 1ABHLMIbcwGFC

1414 if print_notes_before and service_notes.strip(): 12 ctx 1ABHLMIbcwGFC

1415 click.echo(f'{service_notes}\n', err=True, color=self.ctx.color) 1 ctx 1F

1416 click.echo(result.decode('ASCII'), color=self.ctx.color) 12 ctx 1ABHLMIbcwGFC

1417 if not print_notes_before and service_notes.strip(): 12 ctx 1ABHLMIbcwGFC

1418 click.echo(f'\n{service_notes}\n', err=True, color=self.ctx.color) 3 ctx 1GFC

1419

1420

1421@derivepassphrase.command( (empty)

1422 'vault',

1423 context_settings={'help_option_names': ['-h', '--help']},

1424 cls=cli_machinery.CommandWithHelpGroups,

1425 help=(

1426 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_01),

1427 _msg.TranslatedString(

1428 _msg.Label.DERIVEPASSPHRASE_VAULT_02,

1429 service_metavar=_msg.TranslatedString(

1430 _msg.Label.VAULT_METAVAR_SERVICE

1431 ),

1432 ),

1433 ),

1434 epilog=(

1435 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_01),

1436 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_02),

1437 ),

1438)

1439@click.option( (empty)

1440 '-p',

1441 '--phrase',

1442 'use_phrase',

1443 is_flag=True,

1444 help=_msg.TranslatedString(

1445 _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT

1446 ),

1447 cls=cli_machinery.PassphraseGenerationOption,

1448)

1449@click.option( (empty)

1450 '-k',

1451 '--key',

1452 'use_key',

1453 is_flag=True,

1454 help=_msg.TranslatedString(

1455 _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT

1456 ),

1457 cls=cli_machinery.PassphraseGenerationOption,

1458)

1459@click.option( (empty)

1460 '-l',

1461 '--length',

1462 metavar=_msg.TranslatedString(

1463 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1464 ),

1465 callback=cli_machinery.validate_length,

1466 help=_msg.TranslatedString(

1467 _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT,

1468 metavar=_msg.TranslatedString(

1469 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1470 ),

1471 ),

1472 cls=cli_machinery.PassphraseGenerationOption,

1473)

1474@click.option( (empty)

1475 '-r',

1476 '--repeat',

1477 metavar=_msg.TranslatedString(

1478 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1479 ),

1480 callback=cli_machinery.validate_occurrence_constraint,

1481 help=_msg.TranslatedString(

1482 _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT,

1483 metavar=_msg.TranslatedString(

1484 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1485 ),

1486 ),

1487 cls=cli_machinery.PassphraseGenerationOption,

1488)

1489@click.option( (empty)

1490 '--lower',

1491 metavar=_msg.TranslatedString(

1492 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1493 ),

1494 callback=cli_machinery.validate_occurrence_constraint,

1495 help=_msg.TranslatedString(

1496 _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT,

1497 metavar=_msg.TranslatedString(

1498 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1499 ),

1500 ),

1501 cls=cli_machinery.PassphraseGenerationOption,

1502)

1503@click.option( (empty)

1504 '--upper',

1505 metavar=_msg.TranslatedString(

1506 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1507 ),

1508 callback=cli_machinery.validate_occurrence_constraint,

1509 help=_msg.TranslatedString(

1510 _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT,

1511 metavar=_msg.TranslatedString(

1512 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1513 ),

1514 ),

1515 cls=cli_machinery.PassphraseGenerationOption,

1516)

1517@click.option( (empty)

1518 '--number',

1519 metavar=_msg.TranslatedString(

1520 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1521 ),

1522 callback=cli_machinery.validate_occurrence_constraint,

1523 help=_msg.TranslatedString(

1524 _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT,

1525 metavar=_msg.TranslatedString(

1526 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1527 ),

1528 ),

1529 cls=cli_machinery.PassphraseGenerationOption,

1530)

1531@click.option( (empty)

1532 '--space',

1533 metavar=_msg.TranslatedString(

1534 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1535 ),

1536 callback=cli_machinery.validate_occurrence_constraint,

1537 help=_msg.TranslatedString(

1538 _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT,

1539 metavar=_msg.TranslatedString(

1540 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1541 ),

1542 ),

1543 cls=cli_machinery.PassphraseGenerationOption,

1544)

1545@click.option( (empty)

1546 '--dash',

1547 metavar=_msg.TranslatedString(

1548 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1549 ),

1550 callback=cli_machinery.validate_occurrence_constraint,

1551 help=_msg.TranslatedString(

1552 _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT,

1553 metavar=_msg.TranslatedString(

1554 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1555 ),

1556 ),

1557 cls=cli_machinery.PassphraseGenerationOption,

1558)

1559@click.option( (empty)

1560 '--symbol',

1561 metavar=_msg.TranslatedString(

1562 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1563 ),

1564 callback=cli_machinery.validate_occurrence_constraint,

1565 help=_msg.TranslatedString(

1566 _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT,

1567 metavar=_msg.TranslatedString(

1568 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1569 ),

1570 ),

1571 cls=cli_machinery.PassphraseGenerationOption,

1572)

1573@click.option( (empty)

1574 '-n',

1575 '--notes',

1576 'edit_notes',

1577 is_flag=True,

1578 help=_msg.TranslatedString(

1579 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT,

1580 service_metavar=_msg.TranslatedString(

1581 _msg.Label.VAULT_METAVAR_SERVICE

1582 ),

1583 ),

1584 cls=cli_machinery.ConfigurationOption,

1585)

1586@click.option( (empty)

1587 '-c',

1588 '--config',

1589 'store_config_only',

1590 is_flag=True,

1591 help=_msg.TranslatedString(

1592 _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT,

1593 service_metavar=_msg.TranslatedString(

1594 _msg.Label.VAULT_METAVAR_SERVICE

1595 ),

1596 ),

1597 cls=cli_machinery.ConfigurationOption,

1598)

1599@click.option( (empty)

1600 '-x',

1601 '--delete',

1602 'delete_service_settings',

1603 is_flag=True,

1604 help=_msg.TranslatedString(

1605 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT,

1606 service_metavar=_msg.TranslatedString(

1607 _msg.Label.VAULT_METAVAR_SERVICE

1608 ),

1609 ),

1610 cls=cli_machinery.ConfigurationOption,

1611)

1612@click.option( (empty)

1613 '--delete-globals',

1614 is_flag=True,

1615 help=_msg.TranslatedString(

1616 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT,

1617 ),

1618 cls=cli_machinery.ConfigurationOption,

1619)

1620@click.option( (empty)

1621 '-X',

1622 '--clear',

1623 'clear_all_settings',

1624 is_flag=True,

1625 help=_msg.TranslatedString(

1626 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT,

1627 ),

1628 cls=cli_machinery.ConfigurationOption,

1629)

1630@click.option( (empty)

1631 '-e',

1632 '--export',

1633 'export_settings',

1634 metavar=_msg.TranslatedString(

1635 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1636 ),

1637 help=_msg.TranslatedString(

1638 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT,

1639 metavar=_msg.TranslatedString(

1640 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1641 ),

1642 ),

1643 cls=cli_machinery.StorageManagementOption,

1644 shell_complete=cli_helpers.shell_complete_path,

1645)

1646@click.option( (empty)

1647 '-i',

1648 '--import',

1649 'import_settings',

1650 metavar=_msg.TranslatedString(

1651 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1652 ),

1653 help=_msg.TranslatedString(

1654 _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT,

1655 metavar=_msg.TranslatedString(

1656 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER

1657 ),

1658 ),

1659 cls=cli_machinery.StorageManagementOption,

1660 shell_complete=cli_helpers.shell_complete_path,

1661)

1662@click.option( (empty)

1663 '--overwrite-existing/--merge-existing',

1664 'overwrite_config',

1665 default=False,

1666 help=_msg.TranslatedString(

1667 _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT

1668 ),

1669 cls=cli_machinery.CompatibilityOption,

1670)

1671@click.option( (empty)

1672 '--unset',

1673 'unset_settings',

1674 multiple=True,

1675 type=click.Choice([

1676 'phrase',

1677 'key',

1678 'length',

1679 'repeat',

1680 'lower',

1681 'upper',

1682 'number',

1683 'space',

1684 'dash',

1685 'symbol',

1686 'notes',

1687 ]),

1688 help=_msg.TranslatedString(

1689 _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT

1690 ),

1691 cls=cli_machinery.CompatibilityOption,

1692)

1693@click.option( (empty)

1694 '--export-as',

1695 type=click.Choice(['json', 'sh']),

1696 default='json',

1697 help=_msg.TranslatedString(

1698 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT

1699 ),

1700 cls=cli_machinery.CompatibilityOption,

1701)

1702@click.option( (empty)

1703 '--modern-editor-interface/--vault-legacy-editor-interface',

1704 'modern_editor_interface',

1705 default=False,

1706 help=_msg.TranslatedString(

1707 _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT

1708 ),

1709 cls=cli_machinery.CompatibilityOption,

1710)

1711@click.option( (empty)

1712 '--print-notes-before/--print-notes-after',

1713 'print_notes_before',

1714 default=False,

1715 help=_msg.TranslatedString(

1716 _msg.Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT,

1717 service_metavar=_msg.TranslatedString(

1718 _msg.Label.VAULT_METAVAR_SERVICE

1719 ),

1720 ),

1721 cls=cli_machinery.CompatibilityOption,

1722)

1723@cli_machinery.version_option(cli_machinery.vault_version_option_callback) (empty)

1724@cli_machinery.color_forcing_pseudo_option (empty)

1725@cli_machinery.standard_logging_options (empty)

1726@click.argument( (empty)

1727 'service',

1728 metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE),

1729 required=False,

1730 default=None,

1731 shell_complete=cli_helpers.shell_complete_service,

1732)

1733@click.pass_context (empty)

1734 def derivepassphrase_vault( (empty)

1735 ctx: click.Context,

1736 /,

1737 **_kwargs: Any, # noqa: ANN401

1738) -> None:

1739 """Derive a passphrase using the vault(1) derivation scheme.

1740

1741 This is a [`click`][CLICK]-powered command-line interface function,

1742 and not intended for programmatic use. See the

1743 derivepassphrase-vault(1) manpage for full documentation of the

1744 interface. (See also [`click.testing.CliRunner`][] for controlled,

1745 programmatic invocation.)

1746

1747 [CLICK]: https://pypi.org/package/click/

1748

1749 Parameters:

1750 ctx (click.Context):

1751 The `click` context.

1752

1753 Other Parameters:

1754 service (str | None):

1755 A service name. Required, unless operating on global

1756 settings or importing/exporting settings.

1757 use_phrase (bool):

1758 Command-line argument `-p`/`--phrase`. If given, query the

1759 user for a passphrase instead of an SSH key.

1760 use_key (bool):

1761 Command-line argument `-k`/`--key`. If given, query the

1762 user for an SSH key instead of a passphrase.

1763 length (int | None):

1764 Command-line argument `-l`/`--length`. Override the default

1765 length of the generated passphrase.

1766 repeat (int | None):

1767 Command-line argument `-r`/`--repeat`. Override the default

1768 repetition limit if positive, or disable the repetition

1769 limit if 0.

1770 lower (int | None):

1771 Command-line argument `--lower`. Require a given amount of

1772 ASCII lowercase characters if positive, else forbid ASCII

1773 lowercase characters if 0.

1774 upper (int | None):

1775 Command-line argument `--upper`. Same as `lower`, but for

1776 ASCII uppercase characters.

1777 number (int | None):

1778 Command-line argument `--number`. Same as `lower`, but for

1779 ASCII digits.

1780 space (int | None):

1781 Command-line argument `--space`. Same as `lower`, but for

1782 the space character.

1783 dash (int | None):

1784 Command-line argument `--dash`. Same as `lower`, but for

1785 the hyphen-minus and underscore characters.

1786 symbol (int | None):

1787 Command-line argument `--symbol`. Same as `lower`, but for

1788 all other ASCII printable characters except lowercase

1789 characters, uppercase characters, digits, space and

1790 backquote.

1791 edit_notes (bool):

1792 Command-line argument `-n`/`--notes`. If given, spawn an

1793 editor to edit notes for `service`.

1794 store_config_only (bool):

1795 Command-line argument `-c`/`--config`. If given, saves the

1796 other given settings (`--key`, ..., `--symbol`) to the

1797 configuration file, either specifically for `service` or as

1798 global settings.

1799 delete_service_settings (bool):

1800 Command-line argument `-x`/`--delete`. If given, removes

1801 the settings for `service` from the configuration file.

1802 delete_globals (bool):

1803 Command-line argument `--delete-globals`. If given, removes

1804 the global settings from the configuration file.

1805 clear_all_settings (bool):

1806 Command-line argument `-X`/`--clear`. If given, removes all

1807 settings from the configuration file.

1808 export_settings (TextIO | os.PathLike[str] | None):

1809 Command-line argument `-e`/`--export`. If a file object,

1810 then it must be open for writing and accept `str` inputs.

1811 Otherwise, a filename to open for writing. Using `-` for

1812 standard output is supported.

1813 import_settings (TextIO | os.PathLike[str] | None):

1814 Command-line argument `-i`/`--import`. If a file object, it

1815 must be open for reading and yield `str` values. Otherwise,

1816 a filename to open for reading. Using `-` for standard

1817 input is supported.

1818 overwrite_config (bool):

1819 Command-line arguments `--overwrite-existing` (True) and

1820 `--merge-existing` (False). Controls whether config saving

1821 and config importing overwrite existing configurations, or

1822 merge them section-wise instead.

1823 unset_settings (Sequence[str]):

1824 Command-line argument `--unset`. If given together with

1825 `--config`, unsets the specified settings (in addition to

1826 any other changes requested).

1827 export_as (Literal['json', 'sh']):

1828 Command-line argument `--export-as`. If given together with

1829 `--export`, selects the format to export the current

1830 configuration as: JSON ("json", default) or POSIX sh ("sh").

1831 modern_editor_interface (bool):

1832 Command-line arguments `--modern-editor-interface` (True)

1833 and `--vault-legacy-editor-interface` (False). Controls

1834 whether editing notes uses a modern editor interface

1835 (supporting comments and aborting) or a vault(1)-compatible

1836 legacy editor interface (WYSIWYG notes contents).

1837 print_notes_before (bool):

1838 Command-line arguments `--print-notes-before` (True) and

1839 `--print-notes-after` (False). Controls whether the service

1840 notes (if any) are printed before the passphrase, or after.

1841

1842 """

1843 vault_context = _VaultContext(ctx) 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

1844 vault_context.validate_command_line() 61 ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

1845 vault_context.dispatch_op() 60 ctx 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

1846

1847

1848 if __name__ == '__main__': (empty)

1849 derivepassphrase()