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

469 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 21:34 +0200

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

2# 

3# SPDX-License-Identifier: Zlib 

4 

5# ruff: noqa: TRY400 

6 

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

8 

9from __future__ import annotations 

10 

11import base64 

12import collections 

13import contextlib 

14import json 

15import logging 

16import os 

17from typing import ( 

18 TYPE_CHECKING, 

19 Final, 

20 Literal, 

21 NoReturn, 

22 TextIO, 

23 cast, 

24) 

25 

26import click 

27from typing_extensions import ( 

28 Any, 

29) 

30 

31from derivepassphrase import _internals, _types, exporter, vault 

32from derivepassphrase._internals import cli_helpers, cli_machinery 

33from derivepassphrase._internals import cli_messages as _msg 

34 

35if TYPE_CHECKING: 

36 from collections.abc import Sequence 

37 from collections.abc import Set as AbstractSet 

38 from contextlib import AbstractContextManager 

39 

40__all__ = ("derivepassphrase",) 

41 

42PROG_NAME = _internals.PROG_NAME 

43VERSION = _internals.VERSION 

44 

45 

46@click.group( 

47 context_settings={ 

48 "help_option_names": ["-h", "--help"], 

49 "ignore_unknown_options": True, 

50 "allow_interspersed_args": False, 

51 }, 

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

53 invoke_without_command=True, 

54 cls=cli_machinery.TopLevelCLIEntryPoint, 

55 help=( 

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

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

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

59 ), 

60) 

61@cli_machinery.version_option( 

62 cli_machinery.derivepassphrase_version_option_callback 

63) 

64@cli_machinery.color_forcing_pseudo_option 

65@cli_machinery.standard_logging_options 

66@click.pass_context 

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

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

69 

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

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

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

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

74 invocation.) 

75 

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

77 

78 """ 

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

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

81 deprecation = logging.getLogger(f"{PROG_NAME}.deprecation") 1q=?@[]:'uW0

82 if ctx.invoked_subcommand is None: 1q=?@[]:'uW0

83 deprecation.warning( 1W

84 _msg.TranslatedString( 

85 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED 

86 ), 

87 extra={"color": ctx.color}, 

88 ) 

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

90 with ctx: 1W

91 sub_ctx = derivepassphrase_vault.make_context( 1W

92 "vault", ctx.args, parent=ctx 

93 ) 

94 with sub_ctx: 1W

95 return derivepassphrase_vault.invoke(sub_ctx) 1W

96 return None 1q=?@[]:'u0

97 

98 

99# Exporter 

100# ======== 

101 

102 

103@derivepassphrase.group( 

104 "export", 

105 context_settings={ 

106 "help_option_names": ["-h", "--help"], 

107 "ignore_unknown_options": True, 

108 "allow_interspersed_args": False, 

109 }, 

110 invoke_without_command=True, 

111 cls=cli_machinery.DefaultToVaultGroup, 

112 help=( 

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

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

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

116 ), 

117) 

118@cli_machinery.version_option(cli_machinery.export_version_option_callback) 

119@cli_machinery.color_forcing_pseudo_option 

120@cli_machinery.standard_logging_options 

121@click.pass_context 

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

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

124 

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

126 and not intended for programmatic use. See the 

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

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

129 programmatic invocation.) 

130 

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

132 

133 """ 

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

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

136 deprecation = logging.getLogger(f"{PROG_NAME}.deprecation") 1=?@:'

137 if ctx.invoked_subcommand is None: 1=?@:'

138 deprecation.warning( 1:

139 _msg.TranslatedString( 

140 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED 

141 ), 

142 extra={"color": ctx.color}, 

143 ) 

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

145 with ctx: 1:

146 sub_ctx = derivepassphrase_export_vault.make_context( 1:

147 "vault", ctx.args, parent=ctx 

148 ) 

149 # Constructing the subcontext above will usually already 

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

151 # actually be called. 

152 with sub_ctx: # pragma: no cover [unused] 

153 return derivepassphrase_export_vault.invoke(sub_ctx) 1:

154 return None 1=?@'

155 

156 

157@derivepassphrase_export.command( 

158 "vault", 

159 context_settings={"help_option_names": ["-h", "--help"]}, 

160 cls=cli_machinery.CommandWithHelpGroups, 

161 help=( 

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

163 _msg.TranslatedString( 

164 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_02, 

165 path_metavar=_msg.TranslatedString( 

166 _msg.Label.EXPORT_VAULT_METAVAR_PATH, 

167 ), 

168 ), 

169 _msg.TranslatedString( 

170 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_03, 

171 path_metavar=_msg.TranslatedString( 

172 _msg.Label.EXPORT_VAULT_METAVAR_PATH, 

173 ), 

174 ), 

175 ), 

176) 

177@click.option( 

178 "-f", 

179 "--format", 

180 "formats", 

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

182 multiple=True, 

183 default=("v0.3", "v0.2", "storeroom"), 

184 type=click.Choice(["v0.2", "v0.3", "storeroom"]), 

185 help=_msg.TranslatedString( 

186 _msg.Label.EXPORT_VAULT_FORMAT_HELP_TEXT, 

187 defaults_hint=_msg.TranslatedString( 

188 _msg.Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT, 

189 ), 

190 metavar=_msg.TranslatedString( 

191 _msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT, 

192 ), 

193 ), 

194 cls=cli_machinery.StandardOption, 

195) 

196@click.option( 

197 "-k", 

198 "--key", 

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

200 help=_msg.TranslatedString( 

201 _msg.Label.EXPORT_VAULT_KEY_HELP_TEXT, 

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

203 defaults_hint=_msg.TranslatedString( 

204 _msg.Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT, 

205 ), 

206 ), 

207 cls=cli_machinery.StandardOption, 

208) 

209@cli_machinery.version_option( 

210 cli_machinery.export_vault_version_option_callback 

211) 

212@cli_machinery.color_forcing_pseudo_option 

213@cli_machinery.standard_logging_options 

214@click.argument( 

215 "path", 

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

217 required=True, 

218 shell_complete=cli_helpers.shell_complete_path, 

219) 

220@click.pass_context 

221def derivepassphrase_export_vault( 

222 ctx: click.Context, 

223 /, 

224 *, 

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

226 formats: Sequence[Literal["v0.2", "v0.3", "storeroom"]] = (), 

227 key: str | bytes | None = None, 

228) -> None: 

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

230 

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

232 and not intended for programmatic use. See the 

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

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

235 controlled, programmatic invocation.) 

236 

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

238 

239 """ 

240 logger = logging.getLogger(PROG_NAME) 1'*-;+(.,/)

241 if path in {"VAULT_PATH", b"VAULT_PATH"}: 1'*-;+(.,/)

242 path = None 1',/)

243 if isinstance(key, str): # pragma: no branch 1'*-;+(.,/)

244 key = key.encode("utf-8") 1.,

245 for fmt in formats: 1'*-;+(.,/)

246 try: 1'*-;+(.,/)

247 config = exporter.export_vault_config_data(path, key, format=fmt) 1'*-;+(.,/)

248 except ( 1*-+()

249 IsADirectoryError, 

250 NotADirectoryError, 

251 exporter.NotAVaultConfigError, 

252 RuntimeError, 

253 ): 

254 logger.info( 1*+(

255 _msg.TranslatedString( 

256 _msg.InfoMsgTemplate.CANNOT_LOAD_AS_VAULT_CONFIG, 

257 path=path or exporter.get_vault_path(), 

258 fmt=fmt, 

259 ), 

260 extra={"color": ctx.color}, 

261 ) 

262 continue 1*+(

263 except OSError as exc: 1-()

264 logger.error( 1-(

265 _msg.TranslatedString( 

266 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR, 

267 path=path, 

268 error=exc.strerror, 

269 filename=exc.filename, 

270 ).maybe_without_filename(), 

271 extra={"color": ctx.color}, 

272 ) 

273 ctx.exit(1) 1-(

274 except ModuleNotFoundError: 1)

275 logger.error( 1)

276 _msg.TranslatedString( 

277 _msg.ErrMsgTemplate.MISSING_MODULE, 

278 module="cryptography", 

279 ), 

280 extra={"color": ctx.color}, 

281 ) 

282 logger.info( 1)

283 _msg.TranslatedString( 

284 _msg.InfoMsgTemplate.PIP_INSTALL_EXTRA, 

285 extra_name="export", 

286 ), 

287 extra={"color": ctx.color}, 

288 ) 

289 ctx.exit(1) 1)

290 else: 

291 if not _types.is_vault_config(config): 1';.,/

292 logger.error( 1;

293 _msg.TranslatedString( 

294 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG, 

295 config=config, 

296 ), 

297 extra={"color": ctx.color}, 

298 ) 

299 ctx.exit(1) 1;

300 click.echo( 1'.,/

301 json.dumps( 

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

303 ), 

304 color=ctx.color, 

305 ) 

306 break 1'.,/

307 else: 

308 logger.error( 1*+(

309 _msg.TranslatedString( 

310 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG, 

311 path=path, 

312 ).maybe_without_filename(), 

313 extra={"color": ctx.color}, 

314 ) 

315 ctx.exit(1) 1*+(

316 

317 

318class _VaultContext: # noqa: PLR0904 

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

320 

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

322 encapsulates a single call to the `derivepassphrase vault` 

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

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

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

326 

327 Attributes: 

328 logger: 

329 The logger used for warnings and error messages. 

330 deprecation: 

331 The logger used for deprecation warnings. 

332 ctx: 

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

334 command-line settings and parameter values are queried. 

335 all_ops: 

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

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

338 final entry is the default operation, and does not 

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

340 parameter names. 

341 readwrite_ops: 

342 A set of operations which modify the `derivepassphrase` 

343 state. All other operations are read-only. 

344 options_in_group: 

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

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

347 line. 

348 params_by_str: 

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

350 option objects. Used during the validation of the command 

351 line. 

352 

353 """ 

354 

355 logger: Final = logging.getLogger(PROG_NAME) 

356 """""" 

357 deprecation: Final = logging.getLogger(PROG_NAME + ".deprecation") 

358 """""" 

359 ctx: Final[click.Context] 

360 """""" 

361 all_ops: tuple[str, ...] = ( 

362 "delete_service_settings", 

363 "delete_globals", 

364 "clear_all_settings", 

365 "import_settings", 

366 "export_settings", 

367 "store_config_only", 

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

369 "derive_passphrase", 

370 ) 

371 readwrite_ops: AbstractSet[str] = frozenset({ 

372 "delete_service_settings", 

373 "delete_globals", 

374 "clear_all_settings", 

375 "import_settings", 

376 "store_config_only", 

377 }) 

378 options_in_group: dict[type[click.Option], list[click.Option]] 

379 params_by_str: dict[str, click.Parameter] 

380 

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

382 """Initialize the vault context. 

383 

384 Args: 

385 ctx: 

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

387 command-line settings and parameter values are queried. 

388 

389 """ 

390 self.ctx = ctx 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

391 self.params_by_str = {} 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

392 self.options_in_group = {} 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

393 for param in ctx.command.params: 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

394 if isinstance(param, click.Option): 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

395 group: type[click.Option] 

396 known_option_groups = [ 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

397 cli_machinery.PassphraseGenerationOption, 

398 cli_machinery.ConfigurationOption, 

399 cli_machinery.StorageManagementOption, 

400 cli_machinery.LoggingOption, 

401 cli_machinery.CompatibilityOption, 

402 cli_machinery.StandardOption, 

403 ] 

404 if isinstance(param, cli_machinery.OptionGroupOption): 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

405 for class_ in known_option_groups: 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

406 if isinstance(param, class_): 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

407 group = class_ 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

408 break 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

409 else: # pragma: no cover [failsafe] 

410 assert False, f"Unknown option group for {param!r}" # noqa: B011,PT015 

411 else: 

412 group = click.Option 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

413 self.options_in_group.setdefault(group, []).append(param) 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

414 self.params_by_str[param.human_readable_name] = param 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

415 for name in param.opts + param.secondary_opts: 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

416 self.params_by_str[name] = param 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

417 

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

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

420 return bool(self.ctx.params.get(param.human_readable_name)) 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

421 

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

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

424 

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

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

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

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

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

430 option names. 

431 

432 Args: 

433 param: The parameter whose option name is requested. 

434 

435 Raises: 

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

437 

438 """ 

439 param = self.params_by_str[param] if isinstance(param, str) else param 1%

440 names = [param.human_readable_name, *param.opts, *param.secondary_opts] 1%

441 option_names = [n for n in names if n.startswith("--")] 1%

442 return min(option_names, key=len) 1%

443 

444 def check_incompatible_options( 

445 self, 

446 param1: click.Parameter | str, 

447 param2: click.Parameter | str, 

448 ) -> None: 

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

450 

451 Raises: 

452 click.BadOptionUsage: The given options are incompatible. 

453 

454 """ 

455 param1 = ( 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

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

457 ) 

458 param2 = ( 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

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

460 ) 

461 if param1 == param2: 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

462 return 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

463 if not self.is_param_set(param1): 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

464 return 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

465 if self.is_param_set(param2): 1adicH56qfuXY7hDexyzg%bnE8!#90$413ApQRStvTOCUVrowsmBjlk

466 param1_str = self.option_name(param1) 1%

467 param2_str = self.option_name(param2) 1%

468 raise click.BadOptionUsage( 1%

469 param1_str, 

470 str( 

471 _msg.TranslatedString( 

472 _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE, 

473 param1=param1_str, 

474 param2=param2_str, 

475 ) 

476 ), 

477 ctx=self.ctx, 

478 ) 

479 return 1adicH56qfuXY7hDexyzg%bnE8!#90$413ApQRStvTOCUVrowsmBjlk

480 

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

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

483 

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

485 is logged. 

486 

487 """ 

488 stacklevel = kwargs.pop("stacklevel", 1) 1iH56P8!#9413pQRStvTCUVmj

489 stacklevel += 1 1iH56P8!#9413pQRStvTCUVmj

490 extra = kwargs.pop("extra", {}) 1iH56P8!#9413pQRStvTCUVmj

491 extra.setdefault("color", self.ctx.color) 1iH56P8!#9413pQRStvTCUVmj

492 self.logger.error(msg, stacklevel=stacklevel, extra=extra, **kwargs) 1iH56P8!#9413pQRStvTCUVmj

493 self.ctx.exit(1) 1iH56P8!#9413pQRStvTCUVmj

494 

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

496 """Log a warning. 

497 

498 We ensure that color handling is done properly before the 

499 warning is logged. 

500 

501 """ 

502 stacklevel = kwargs.pop("stacklevel", 1) 1HqfYhDexgnAowBjk

503 stacklevel += 1 1HqfYhDexgnAowBjk

504 extra = kwargs.pop("extra", {}) 1HqfYhDexgnAowBjk

505 extra.setdefault("color", self.ctx.color) 1HqfYhDexgnAowBjk

506 self.logger.warning(msg, stacklevel=stacklevel, extra=extra, **kwargs) 1HqfYhDexgnAowBjk

507 

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

509 """Log a deprecation warning. 

510 

511 We ensure that color handling is done properly before the 

512 warning is logged. 

513 

514 """ 

515 stacklevel = kwargs.pop("stacklevel", 1) 1XY

516 stacklevel += 1 1XY

517 extra = kwargs.pop("extra", {}) 1XY

518 extra.setdefault("color", self.ctx.color) 1XY

519 self.deprecation.warning( 1XY

520 msg, stacklevel=stacklevel, extra=extra, **kwargs 

521 ) 

522 

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

524 """Log a deprecation info message. 

525 

526 We ensure that color handling is done properly before the 

527 warning is logged. 

528 

529 """ 

530 stacklevel = kwargs.pop("stacklevel", 1) 1X

531 stacklevel += 1 1X

532 extra = kwargs.pop("extra", {}) 1X

533 extra.setdefault("color", self.ctx.color) 1X

534 self.deprecation.info( 1X

535 msg, stacklevel=stacklevel, extra=extra, **kwargs 

536 ) 

537 

538 def get_config(self) -> _types.VaultConfig: 

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

540 

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

542 

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

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

545 

546 """ 

547 try: 1adicHqfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$ApQRStvTOCUVrowsmBjlFGk

548 return cli_helpers.load_config() 1adicHqfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$ApQRStvTOCUVrowsmBjlFGk

549 except FileNotFoundError: 1quWXYyz8!#0r

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

551 # configuration directly. 

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

553 try: 1quWXYyz0r

554 backup_config, exc = cli_helpers.migrate_and_load_old_config() 1quWXYyz0r

555 except FileNotFoundError: 1quWyz0r

556 return {"services": {}} 1quWyz0r

557 old_name = cli_helpers.config_filename( 1XY

558 subsystem="old settings.json" 

559 ).name 

560 new_name = cli_helpers.config_filename(subsystem="vault").name 1XY

561 self.deprecation_warning( 1XY

562 _msg.TranslatedString( 

563 _msg.WarnMsgTemplate.V01_STYLE_CONFIG, 

564 old=old_name, 

565 new=new_name, 

566 ), 

567 ) 

568 if isinstance(exc, OSError): 1XY

569 self.warning( 1Y

570 _msg.TranslatedString( 

571 _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG, 

572 path=new_name, 

573 error=exc.strerror, 

574 filename=exc.filename, 

575 ).maybe_without_filename(), 

576 ) 

577 else: 

578 self.deprecation_info( 1X

579 _msg.TranslatedString( 

580 _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED, 

581 path=new_name, 

582 ), 

583 ) 

584 return backup_config 1XY

585 except OSError as exc: 18!#

586 self.err( 1!#

587 _msg.TranslatedString( 

588 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS, 

589 error=exc.strerror, 

590 filename=exc.filename, 

591 ).maybe_without_filename(), 

592 ) 

593 except Exception as exc: # noqa: BLE001 18

594 self.err( 18

595 _msg.TranslatedString( 

596 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS, 

597 error=str(exc), 

598 filename=None, 

599 ).maybe_without_filename(), 

600 exc_info=exc, 

601 ) 

602 

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

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

605 try: 1adcqf7hDexgbnAptvrowsjlk

606 cli_helpers.save_config(config) 1adcqf7hDexgbnAptvrowsjlk

607 except OSError as exc: 1ptv

608 self.err( 1pv

609 _msg.TranslatedString( 

610 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS, 

611 error=exc.strerror, 

612 filename=exc.filename, 

613 ).maybe_without_filename(), 

614 ) 

615 except Exception as exc: # noqa: BLE001 1t

616 self.err( 1t

617 _msg.TranslatedString( 

618 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS, 

619 error=str(exc), 

620 filename=None, 

621 ).maybe_without_filename(), 

622 exc_info=exc, 

623 ) 

624 

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

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

627 

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

629 

630 """ 

631 try: 1adicH56qfuWhDexyzg2ZbMIJPNKnEL413ApQRStvTOCUVrowsmBjlFGk

632 return cli_helpers.load_user_config() 1adicH56qfuWhDexyzg2ZbMIJPNKnEL413ApQRStvTOCUVrowsmBjlFGk

633 except FileNotFoundError: 1adH56qfuWhDexyzg2ZbMIJPNKnEL413ApQRStvTOCUVrowsmBjlFGk

634 return {} 1adHqfuWhDexyzg2ZbMIJPNKnEL413ApQRStvTOCUVrowsmBjlFGk

635 except OSError as exc: 156

636 self.err( 16

637 _msg.TranslatedString( 

638 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG, 

639 error=exc.strerror, 

640 filename=exc.filename, 

641 ).maybe_without_filename(), 

642 ) 

643 except Exception as exc: # noqa: BLE001 15

644 self.err( 15

645 _msg.TranslatedString( 

646 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG, 

647 error=str(exc), 

648 filename=None, 

649 ).maybe_without_filename(), 

650 exc_info=exc, 

651 ) 

652 

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

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

655 

656 Raises: 

657 click.UsageError: 

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

659 arguments, or because of conflicting options. The error 

660 message contains further details. 

661 

662 """ 

663 param: click.Parameter 

664 self.check_incompatible_options("--phrase", "--key") 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

665 for group in ( 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

666 cli_machinery.ConfigurationOption, 

667 cli_machinery.StorageManagementOption, 

668 ): 

669 for opt in self.options_in_group[group]: 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

670 if opt not in { 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

671 self.params_by_str["--config"], 

672 self.params_by_str["--notes"], 

673 }: 

674 for other_opt in self.options_in_group[ 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

675 cli_machinery.PassphraseGenerationOption 

676 ]: 

677 self.check_incompatible_options(opt, other_opt) 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

678 

679 for group in ( 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

680 cli_machinery.ConfigurationOption, 

681 cli_machinery.StorageManagementOption, 

682 ): 

683 for opt in self.options_in_group[group]: 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

684 for other_opt in self.options_in_group[ 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

685 cli_machinery.ConfigurationOption 

686 ]: 

687 if {opt, other_opt} != { 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

688 self.params_by_str["--config"], 

689 self.params_by_str["--notes"], 

690 }: 

691 self.check_incompatible_options(opt, other_opt) 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

692 for other_opt in self.options_in_group[ 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

693 cli_machinery.StorageManagementOption 

694 ]: 

695 self.check_incompatible_options(opt, other_opt) 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

696 service: str | None = self.ctx.params["service"] 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

697 service_metavar = _msg.TranslatedString( 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

698 _msg.Label.VAULT_METAVAR_SERVICE 

699 ) 

700 sv_or_global_options = self.options_in_group[ 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

701 cli_machinery.PassphraseGenerationOption 

702 ] 

703 for param in sv_or_global_options: 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

704 if self.is_param_set(param) and not ( 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

705 service is not None 

706 or self.is_param_set(self.params_by_str["--config"]) 

707 ): 

708 err_msg = _msg.TranslatedString( 1b

709 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG, 

710 param=param.opts[0], 

711 service_metavar=service_metavar, 

712 ) 

713 raise click.UsageError(str(err_msg)) 1b

714 sv_options = [ 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

715 self.params_by_str["--notes"], 

716 self.params_by_str["--delete"], 

717 ] 

718 for param in sv_options: 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

719 if self.is_param_set(param) and not service is not None: 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

720 err_msg = _msg.TranslatedString( 1b

721 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE, 

722 param=param.opts[0], 

723 service_metavar=service_metavar, 

724 ) 

725 raise click.UsageError(str(err_msg)) 1b

726 no_sv_options = [ 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

727 self.params_by_str["--delete-globals"], 

728 self.params_by_str["--clear"], 

729 *self.options_in_group[cli_machinery.StorageManagementOption], 

730 ] 

731 for param in no_sv_options: 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

732 if self.is_param_set(param) and service is not None: 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

733 err_msg = _msg.TranslatedString( 1b

734 _msg.ErrMsgTemplate.PARAMS_NO_SERVICE, 

735 param=param.opts[0], 

736 service_metavar=service_metavar, 

737 ) 

738 raise click.UsageError(str(err_msg)) 1b

739 

740 def get_mutex( 

741 self, 

742 op: str, 

743 ) -> AbstractContextManager[AbstractContextManager | None]: 

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

745 

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

747 and processes attempting to access the configuration in an 

748 incompatible manner. 

749 

750 Returns: 

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

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

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

754 operations, return an actual mutex. 

755 

756 """ 

757 return ( 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

758 cli_helpers.configuration_mutex() 

759 if op in self.readwrite_ops 

760 else contextlib.nullcontext() 

761 ) 

762 

763 def dispatch_op(self) -> None: 

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

765 

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

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

768 

769 """ 

770 service_metavar = _msg.TranslatedString( 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

771 _msg.Label.VAULT_METAVAR_SERVICE 

772 ) 

773 if self.ctx.params["service"] == "": # noqa: PLC1901 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

774 self.warning( 1g

775 _msg.TranslatedString( 

776 _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED, 

777 service_metavar=service_metavar, 

778 ), 

779 ) 

780 if self.ctx.params.get("edit_notes") and not self.ctx.params.get( 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

781 "store_config_only" 

782 ): 

783 self.warning( 1B

784 _msg.TranslatedString( 

785 _msg.WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG, 

786 service_metavar=service_metavar, 

787 ), 

788 ) 

789 op: str 

790 for candidate_op in self.all_ops[:-1]: 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

791 if self.ctx.params.get(candidate_op): 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

792 op = candidate_op 1adicHqfXY7hDexgbn8!#90$413ApQRStvTOCUVrowsmjlk

793 break 1adicHqfXY7hDexgbn8!#90$413ApQRStvTOCUVrowsmjlk

794 else: 

795 op = self.all_ops[-1] 1ic56uWyz2ZbMIJPNKELBFG

796 with self.get_mutex(op): 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

797 op_func = getattr(self, "run_op_" + op) 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

798 return op_func() 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

799 

800 def run_op_delete_service_settings(self) -> None: 

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

802 service = self.ctx.params["service"] 1ad7

803 assert service is not None 1ad7

804 configuration = self.get_config() 1ad7

805 if service in configuration["services"]: 1ad7

806 del configuration["services"][service] 1ad7

807 self.put_config(configuration) 1ad7

808 

809 def run_op_delete_globals(self) -> None: 

810 """Delete the global settings.""" 

811 configuration = self.get_config() 1ad7b

812 if "global" in configuration: 1ad7b

813 del configuration["global"] 1ad7b

814 self.put_config(configuration) 1ad7b

815 

816 def run_op_clear_all_settings(self) -> None: 

817 """Clear all settings.""" 

818 self.put_config({"services": {}}) 1ad7hDexb

819 

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

821 """Import settings from a given file. 

822 

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

824 normalization issues with stored passphrases or conflicting 

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

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

827 configuration to disk. 

828 

829 """ 

830 service_metavar = _msg.TranslatedString( 1adicqfhDexgb413Aow

831 _msg.Label.VAULT_METAVAR_SERVICE 

832 ) 

833 user_config = self.get_user_config() 1adicqfhDexgb413Aow

834 import_settings = self.ctx.params["import_settings"] 1adicqfhDexgb413Aow

835 overwrite_config = self.ctx.params["overwrite_config"] 1adicqfhDexgb413Aow

836 try: 1adicqfhDexgb413Aow

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

838 # os.dup if feasible 

839 infile = cast( 1adicqfhDexgb413Aow

840 "TextIO", 

841 ( 

842 import_settings 

843 if hasattr(import_settings, "close") 

844 else click.open_file(os.fspath(import_settings), "rt") 

845 ), 

846 ) 

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

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

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

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

851 # error information. 

852 with infile: 1adicqfhDexgb13Aow

853 maybe_config = json.load(infile) 1adicqfhDexgb13Aow

854 except json.JSONDecodeError as exc: 143

855 self.err( 13

856 _msg.TranslatedString( 

857 _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS, 

858 error=exc, 

859 ) 

860 ) 

861 except OSError as exc: 14

862 self.err( 14

863 _msg.TranslatedString( 

864 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS, 

865 error=exc.strerror, 

866 filename=exc.filename, 

867 ).maybe_without_filename() 

868 ) 

869 cleaned = _types.clean_up_falsy_vault_config_values(maybe_config) 1adicqfhDexgb1Aow

870 if not _types.is_vault_config(maybe_config): 1adicqfhDexgb1Aow

871 self.err( 11

872 _msg.TranslatedString( 

873 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS, 

874 error=_msg.TranslatedString( 

875 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG, 

876 config=maybe_config, 

877 ), 

878 filename=None, 

879 ).maybe_without_filename() 

880 ) 

881 assert cleaned is not None 1adicqfhDexgbAow

882 for step in cleaned: 1adicqfhDexgbAow

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

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

885 # but not ill-defined. 

886 if step.action == "replace": 1exow

887 self.warning( 1exow

888 _msg.TranslatedString( 

889 _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE, 

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

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

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

893 ), 

894 ) 

895 else: 

896 self.warning( 1ow

897 _msg.TranslatedString( 

898 _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE, 

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

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

901 ), 

902 ) 

903 if "" in maybe_config["services"]: 1adicqfhDexgbAow

904 self.warning( 1qg

905 _msg.TranslatedString( 

906 _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE, 

907 service_metavar=service_metavar, 

908 PROG_NAME=PROG_NAME, 

909 ), 

910 ) 

911 for service_name in sorted(maybe_config["services"].keys()): 1adicqfhDexgbAow

912 if not cli_helpers.is_completable_item(service_name): 1adicqfexgAow

913 self.warning( 1f

914 _msg.TranslatedString( 

915 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE, 

916 service=service_name, 

917 ), 

918 ) 

919 try: 1adicqfhDexgbAow

920 cli_helpers.check_for_misleading_passphrase( 1adicqfhDexgbAow

921 ("global",), 

922 cast("dict[str, Any]", maybe_config.get("global", {})), 

923 main_config=user_config, 

924 ctx=self.ctx, 

925 ) 

926 for key, value in maybe_config["services"].items(): 1adicqfhDexgbAow

927 cli_helpers.check_for_misleading_passphrase( 1adicqfexgAow

928 ("services", key), 

929 cast("dict[str, Any]", value), 

930 main_config=user_config, 

931 ctx=self.ctx, 

932 ) 

933 except AssertionError as exc: 1i

934 self.err( 1i

935 _msg.TranslatedString( 

936 _msg.ErrMsgTemplate.INVALID_USER_CONFIG, 

937 error=exc, 

938 filename=None, 

939 ).maybe_without_filename(), 

940 ) 

941 global_obj = maybe_config.get("global", {}) 1adcqfhDexgbAow

942 has_key = _types.js_truthiness(global_obj.get("key")) 1adcqfhDexgbAow

943 has_phrase = _types.js_truthiness(global_obj.get("phrase")) 1adcqfhDexgbAow

944 if has_key and has_phrase: 1adcqfhDexgbAow

945 self.warning( 1hDAow

946 _msg.TranslatedString( 

947 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE, 

948 ), 

949 ) 

950 for service_name, service_obj in maybe_config["services"].items(): 1adcqfhDexgbAow

951 has_key = _types.js_truthiness( 1adcqfexgAow

952 service_obj.get("key") 

953 ) or _types.js_truthiness(global_obj.get("key")) 

954 has_phrase = _types.js_truthiness( 1adcqfexgAow

955 service_obj.get("phrase") 

956 ) or _types.js_truthiness(global_obj.get("phrase")) 

957 if has_key and has_phrase: 1adcqfexgAow

958 self.warning( 1exAow

959 _msg.TranslatedString( 

960 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE, 

961 service=json.dumps(service_name), 

962 ), 

963 ) 

964 if overwrite_config: 1adcqfhDexgbAow

965 self.put_config(maybe_config) 1ad

966 else: 

967 configuration = self.get_config() 1adcqfhDexgbAow

968 merged_config: collections.ChainMap[str, Any] = ( 1adcqfhDexgbAow

969 collections.ChainMap( 

970 { 

971 "services": collections.ChainMap( 

972 maybe_config["services"], 

973 configuration["services"], 

974 ), 

975 }, 

976 {"global": maybe_config["global"]} 

977 if "global" in maybe_config 

978 else {}, 

979 {"global": configuration["global"]} 

980 if "global" in configuration 

981 else {}, 

982 ) 

983 ) 

984 new_config: Any = { 1adcqfhDexgbAow

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

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

987 } 

988 assert _types.is_vault_config(new_config) 1adcqfhDexgbAow

989 self.put_config(new_config) 1adcqfhDexgbAow

990 

991 def run_op_export_settings(self) -> None: 

992 """Export settings to a given file. 

993 

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

995 configuration to the file. 

996 

997 """ 

998 export_settings = self.ctx.params["export_settings"] 1XYb8!#90$o

999 export_as = self.ctx.params["export_as"] 1XYb8!#90$o

1000 configuration = self.get_config() 1XYb8!#90$o

1001 try: 1XYb90$o

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

1003 # os.dup if feasible 

1004 outfile = cast( 1XYb90$o

1005 "TextIO", 

1006 ( 

1007 export_settings 

1008 if hasattr(export_settings, "close") 

1009 else click.open_file(os.fspath(export_settings), "wt") 

1010 ), 

1011 ) 

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

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

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

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

1016 # error information. 

1017 with outfile: 1XYb0$o

1018 if export_as == "sh": 1XYb0$o

1019 this_ctx = self.ctx 10

1020 prog_name_pieces = collections.deque([ 10

1021 this_ctx.info_name or "vault", 

1022 ]) 

1023 while ( 10

1024 this_ctx.parent is not None 

1025 and this_ctx.parent.info_name is not None 

1026 ): 

1027 prog_name_pieces.appendleft(this_ctx.parent.info_name) 10

1028 this_ctx = this_ctx.parent 10

1029 cli_helpers.print_config_as_sh_script( 10

1030 configuration, 

1031 outfile=outfile, 

1032 prog_name_list=prog_name_pieces, 

1033 ) 

1034 else: 

1035 json.dump( 1XYb0$o

1036 configuration, 

1037 outfile, 

1038 ensure_ascii=False, 

1039 indent=2, 

1040 sort_keys=True, 

1041 ) 

1042 except OSError as exc: 19

1043 self.err( 19

1044 _msg.TranslatedString( 

1045 _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS, 

1046 error=exc.strerror, 

1047 filename=exc.filename, 

1048 ).maybe_without_filename(), 

1049 ) 

1050 

1051 def run_subop_query_phrase_or_key_change( 

1052 self, 

1053 *, 

1054 empty_service_permitted: bool, 

1055 configuration: _types.VaultConfig | None = None, 

1056 main_config: dict[str, Any] | None = None, 

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

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

1059 

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

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

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

1063 SSH key, then query the user. 

1064 

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

1066 itself. 

1067 

1068 Args: 

1069 empty_service_permitted: 

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

1071 configuration: 

1072 The vault configuration, parsed from disk. If not 

1073 given, we read the configuration from disk ourselves. 

1074 

1075 The returned effective configuration already contains 

1076 a relevant slice of the vault configuration. However, 

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

1078 need the slice within the effective configuration to 

1079 refer to the same object. 

1080 main_config: 

1081 The user configuration, parsed from disk. If not given, 

1082 we read the configuration from disk ourselves. 

1083 

1084 Returns: 

1085 The effective configuration for the (possibly empty) given 

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

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

1088 effect are stored in the first map. 

1089 

1090 Raises: 

1091 click.UsageError: 

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

1093 was not permitted as per the method parameters. 

1094 

1095 Warning: 

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

1097 entered master passphrase for Unicode normalization issues. 

1098 

1099 """ 

1100 service = self.ctx.params["service"] 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1101 use_key = self.ctx.params["use_key"] 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1102 use_phrase = self.ctx.params["use_phrase"] 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1103 if configuration is None: 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1104 configuration = self.get_config() 1icuWyz2ZbMIJPNKELBFG

1105 if main_config is None: # pragma: no cover [unused] 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1106 main_config = self.get_user_config() 

1107 service_keys_on_commandline = { 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1108 "length", 

1109 "repeat", 

1110 "lower", 

1111 "upper", 

1112 "number", 

1113 "space", 

1114 "dash", 

1115 "symbol", 

1116 } 

1117 settings: collections.ChainMap[str, Any] = collections.ChainMap( 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1118 { 

1119 k: v 

1120 for k in service_keys_on_commandline 

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

1122 }, 

1123 cast( 

1124 "dict[str, Any]", 

1125 configuration["services"].get(service, {}) if service else {}, 

1126 ), 

1127 cast("dict[str, Any]", configuration.get("global", {})), 

1128 ) 

1129 if not service and not empty_service_permitted: 1adicHfuWheyzg2ZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1130 err_msg = _msg.TranslatedString( 1W2

1131 _msg.ErrMsgTemplate.SERVICE_REQUIRED, 

1132 service_metavar=_msg.TranslatedString( 

1133 _msg.Label.VAULT_METAVAR_SERVICE 

1134 ), 

1135 ) 

1136 raise click.UsageError(str(err_msg)) 1W2

1137 if use_key: 1adicHfuheyzgZbMIJPNKnELpQRStvTOCUVrsmBjlFGk

1138 conn = cli_helpers.get_configured_connection_hint( 1HMNQRSTCUVs

1139 self.ctx.params["ssh_agent_socket_provider"], 

1140 main_config=main_config, 

1141 ) 

1142 settings.maps[0]["key"] = base64.standard_b64encode( 1HMNQRSTCUVs

1143 cli_helpers.select_ssh_key( 

1144 conn, 

1145 ctx=self.ctx, 

1146 error_callback=self.err, 

1147 warning_callback=self.warning, 

1148 ) 

1149 ).decode("ASCII") 

1150 elif use_phrase: 1adicfuheyzgZbIJPKnELptvOCrsmBjlFGk

1151 maybe_phrase = cli_helpers.prompt_for_passphrase() 1icuyzbnEpCrs

1152 if not maybe_phrase: 1icuyzbnEpCrs

1153 self.err( 1C

1154 _msg.TranslatedString( 

1155 _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE 

1156 ) 

1157 ) 

1158 else: 

1159 settings.maps[0]["phrase"] = maybe_phrase 1icuyzbnEprs

1160 return settings 1adicfuheyzgZbMIJPNKnELptvOCrsmBjlFGk

1161 

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

1163 """Update the stored vault configuration. 

1164 

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

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

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

1168 the former.) 

1169 

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

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

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

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

1174 passphrases or conflicting stored passphrases and keys. Respect 

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

1176 writing the imported configuration to disk, and the 

1177 `--modern-editor-interface` and 

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

1179 

1180 Raises: 

1181 click.UsageError: 

1182 The user requested that the same setting be both unset 

1183 and set. 

1184 

1185 """ 

1186 service = self.ctx.params["service"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1187 use_key = self.ctx.params["use_key"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1188 use_phrase = self.ctx.params["use_phrase"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1189 unset_settings = self.ctx.params["unset_settings"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1190 overwrite_config = self.ctx.params["overwrite_config"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1191 edit_notes = self.ctx.params["edit_notes"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1192 modern_editor_interface = self.ctx.params["modern_editor_interface"] 1adicHfhegbnpQRStvTOCUVrsmjlk

1193 configuration = self.get_config() 1adicHfhegbnpQRStvTOCUVrsmjlk

1194 user_config = self.get_user_config() 1adicHfhegbnpQRStvTOCUVrsmjlk

1195 settings = self.run_subop_query_phrase_or_key_change( 1adicHfhegbnpQRStvTOCUVrsmjlk

1196 empty_service_permitted=True, 

1197 configuration=configuration, 

1198 main_config=user_config, 

1199 ) 

1200 overrides = settings.maps[0] 1adicfhegbnptvOCrsmjlk

1201 view: collections.ChainMap[str, Any] 

1202 view = ( 1adicfhegbnptvOCrsmjlk

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

1204 if service 

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

1206 ) 

1207 if use_key: 1adicfhegbnptvOCrsmjlk

1208 view["key"] = overrides["key"] 1s

1209 elif use_phrase: 1adicfhegbnptvOCrsmjlk

1210 view["phrase"] = overrides["phrase"] 1icbnprs

1211 try: 1icbnprs

1212 cli_helpers.check_for_misleading_passphrase( 1icbnprs

1213 ("services", service) if service else ("global",), 

1214 overrides, 

1215 main_config=user_config, 

1216 ctx=self.ctx, 

1217 ) 

1218 except AssertionError as exc: 1i

1219 self.err( 1i

1220 _msg.TranslatedString( 

1221 _msg.ErrMsgTemplate.INVALID_USER_CONFIG, 

1222 error=exc, 

1223 filename=None, 

1224 ).maybe_without_filename(), 

1225 ) 

1226 if "key" in settings: 1cbnprs

1227 if service: 1n

1228 w_msg = _msg.TranslatedString( 1n

1229 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE, 

1230 service=json.dumps(service), 

1231 ) 

1232 else: 

1233 w_msg = _msg.TranslatedString( 1n

1234 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE 

1235 ) 

1236 self.warning(w_msg) 1n

1237 if not view.maps[0] and not unset_settings and not edit_notes: 1adcfhegbnptvOCrsmjlk

1238 err_msg = _msg.TranslatedString( 1C

1239 _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS, 

1240 settings_type=_msg.TranslatedString( 

1241 _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE 

1242 if service 

1243 else _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL # noqa: E501 

1244 ), 

1245 ) 

1246 raise click.UsageError(str(err_msg)) 1C

1247 for setting in unset_settings: 1adcfhegbnptvOrsmjlk

1248 if setting in view.maps[0]: 1adO

1249 err_msg = _msg.TranslatedString( 1O

1250 _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING, 

1251 setting=setting, 

1252 ) 

1253 raise click.UsageError(str(err_msg)) 1O

1254 if not cli_helpers.is_completable_item(service): 1adcfhegbnptvrsmjlk

1255 self.warning( 1f

1256 _msg.TranslatedString( 

1257 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE, 

1258 service=service, 

1259 ), 

1260 ) 

1261 subtree: dict[str, Any] = ( 1adcfhegbnptvrsmjlk

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

1263 if service 

1264 else configuration.setdefault("global", {}) 

1265 ) 

1266 if overwrite_config: 1adcfhegbnptvrsmjlk

1267 subtree.clear() 1ad

1268 else: 

1269 for setting in unset_settings: 1adcfhegbnptvrsmjlk

1270 subtree.pop(setting, None) 1ad

1271 subtree.update(view) 1adcfhegbnptvrsmjlk

1272 assert _types.is_vault_config(configuration), ( 1adcfhegbnptvrsmjlk

1273 f"Invalid vault configuration: {configuration!r}" 

1274 ) 

1275 if edit_notes: 1adcfhegbnptvrsmjlk

1276 assert service is not None 1mjlk

1277 notes_instructions = _msg.TranslatedString( 1mjlk

1278 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT 

1279 ) 

1280 notes_marker = _msg.TranslatedString( 1mjlk

1281 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER 

1282 ) 

1283 notes_legacy_instructions = _msg.TranslatedString( 1mjlk

1284 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT 

1285 ) 

1286 old_notes_value = cast("str", subtree.get("notes", "")) 1mjlk

1287 if modern_editor_interface: 1mjlk

1288 text = "\n".join([ 1mjlk

1289 str(notes_instructions), 

1290 str(notes_marker), 

1291 old_notes_value, 

1292 ]) 

1293 else: 

1294 text = old_notes_value or str(notes_legacy_instructions) 1jlk

1295 # pyrefly: ignore [bad-argument-type] 

1296 notes_value = click.edit(text=text, require_save=False) 1mjlk

1297 assert isinstance(notes_value, str) 1mjlk

1298 if ( 1mjlk

1299 not modern_editor_interface 

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

1301 ): 

1302 backup_file = cli_helpers.config_filename( 1jk

1303 subsystem="notes backup" 

1304 ) 

1305 backup_file.write_text(old_notes_value, encoding="UTF-8") 1jk

1306 self.warning( 1jk

1307 _msg.TranslatedString( 

1308 _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP, 

1309 filename=str(backup_file), 

1310 ), 

1311 ) 

1312 subtree["notes"] = notes_value.strip() 1jk

1313 elif ( 1mjlk

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

1315 ): 

1316 notes_lines = collections.deque( 1mjlk

1317 notes_value.splitlines(keepends=True) 

1318 ) 

1319 while notes_lines: 1mjlk

1320 line = notes_lines.popleft() 1jlk

1321 if line.startswith(str(notes_marker)): 1jlk

1322 notes_value = "".join(notes_lines) 1lk

1323 break 1lk

1324 else: 

1325 if not notes_value.strip(): 1mj

1326 self.err( 1mj

1327 _msg.TranslatedString( 

1328 _msg.ErrMsgTemplate.USER_ABORTED_EDIT 

1329 ) 

1330 ) 

1331 subtree["notes"] = notes_value.strip() 1jlk

1332 self.put_config(configuration) 1adcfhegbnptvrsjlk

1333 

1334 def run_op_derive_passphrase(self) -> None: 

1335 """Derive a passphrase. 

1336 

1337 Derive a service passphrase using the effective settings from 

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

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

1340 

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

1342 compatibility reasons.) 

1343 

1344 Issue a warning (if appropriate) for Unicode normalization 

1345 issues with the interactive passphrase. Respect the 

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

1347 printing notes. 

1348 

1349 Raises: 

1350 click.UsageError: 

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

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

1353 """ 

1354 service = self.ctx.params["service"] 1ic56uWyz2ZbMIJPNKELBFG

1355 use_key = self.ctx.params["use_key"] 1ic56uWyz2ZbMIJPNKELBFG

1356 use_phrase = self.ctx.params["use_phrase"] 1ic56uWyz2ZbMIJPNKELBFG

1357 print_notes_before = self.ctx.params["print_notes_before"] 1ic56uWyz2ZbMIJPNKELBFG

1358 user_config = self.get_user_config() 1ic56uWyz2ZbMIJPNKELBFG

1359 settings = self.run_subop_query_phrase_or_key_change( 1icuWyz2ZbMIJPNKELBFG

1360 empty_service_permitted=False, 

1361 main_config=user_config, 

1362 ) 

1363 if use_phrase: 1icuyzZbMIJPNKELBFG

1364 try: 1icuyzbE

1365 cli_helpers.check_for_misleading_passphrase( 1icuyzbE

1366 cli_helpers.ORIGIN.INTERACTIVE, 

1367 {"phrase": settings["phrase"]}, 

1368 main_config=user_config, 

1369 ctx=self.ctx, 

1370 ) 

1371 except AssertionError as exc: 1i

1372 self.err( 1i

1373 _msg.TranslatedString( 

1374 _msg.ErrMsgTemplate.INVALID_USER_CONFIG, 

1375 error=exc, 

1376 filename=None, 

1377 ).maybe_without_filename(), 

1378 ) 

1379 phrase: str | bytes 

1380 overrides = cast("dict[str, int | str]", settings.maps[0]) 1cuyzZbMIJPNKELBFG

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

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

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

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

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

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

1387 if use_key: 1cuyzZbMIJPNKELBFG

1388 conn = cli_helpers.get_configured_connection_hint( 1MN

1389 self.ctx.params["ssh_agent_socket_provider"], 

1390 main_config=user_config, 

1391 ) 

1392 phrase = cli_helpers.key_to_phrase( 1MN

1393 cast("str", overrides["key"]), 

1394 error_callback=self.err, 

1395 warning_callback=self.warning, 

1396 conn=conn, 

1397 ) 

1398 elif use_phrase: 1cuyzZbIJPKELBFG

1399 phrase = cast("str", overrides["phrase"]) 1cuyzbE

1400 elif settings.get("key"): 1ZbIJPKLBFG

1401 conn = cli_helpers.get_configured_connection_hint( 1IJPK

1402 self.ctx.params["ssh_agent_socket_provider"], 

1403 main_config=user_config, 

1404 ) 

1405 phrase = cli_helpers.key_to_phrase( 1IJPK

1406 cast("str", settings["key"]), 

1407 error_callback=self.err, 

1408 warning_callback=self.warning, 

1409 conn=conn, 

1410 ) 

1411 elif settings.get("phrase"): 1ZbLBFG

1412 phrase = cast("str", settings["phrase"]) 1bLBFG

1413 else: 

1414 err_msg = _msg.TranslatedString( 1Z

1415 _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE 

1416 ) 

1417 raise click.UsageError(str(err_msg)) 1Z

1418 overrides.pop("key", "") 1cuyzbMIJNKELBFG

1419 overrides.pop("phrase", "") 1cuyzbMIJNKELBFG

1420 assert service is not None 1cuyzbMIJNKELBFG

1421 vault_service_keys = { 1cuyzbMIJNKELBFG

1422 "length", 

1423 "repeat", 

1424 "lower", 

1425 "upper", 

1426 "number", 

1427 "space", 

1428 "dash", 

1429 "symbol", 

1430 } 

1431 kwargs = { 1cuyzbMIJNKELBFG

1432 k: cast("int", settings[k]) 

1433 for k in vault_service_keys 

1434 if k in settings 

1435 } 

1436 result = vault.Vault(phrase=phrase, **kwargs).generate(service) 1cuyzbMIJNKELBFG

1437 service_notes = cast("str", settings.get("notes", "")).strip() 1cuyzbMIJNKELBFG

1438 if print_notes_before and service_notes.strip(): 1cuyzbMIJNKELBFG

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

1440 click.echo(result.decode("ASCII"), color=self.ctx.color) 1cuyzbMIJNKELBFG

1441 if not print_notes_before and service_notes.strip(): 1cuyzbMIJNKELBFG

1442 click.echo(f"\n{service_notes}\n", err=True, color=self.ctx.color) 1BFG

1443 

1444 

1445@derivepassphrase.command( 

1446 "vault", 

1447 context_settings={"help_option_names": ["-h", "--help"]}, 

1448 cls=cli_machinery.CommandWithHelpGroups, 

1449 help=( 

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

1451 _msg.TranslatedString( 

1452 _msg.Label.DERIVEPASSPHRASE_VAULT_02, 

1453 service_metavar=_msg.TranslatedString( 

1454 _msg.Label.VAULT_METAVAR_SERVICE 

1455 ), 

1456 ), 

1457 ), 

1458 epilog=( 

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

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

1461 ), 

1462) 

1463@click.option( 

1464 "-p", 

1465 "--phrase", 

1466 "use_phrase", 

1467 is_flag=True, 

1468 help=_msg.TranslatedString( 

1469 _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT 

1470 ), 

1471 cls=cli_machinery.PassphraseGenerationOption, 

1472) 

1473@click.option( 

1474 "-k", 

1475 "--key", 

1476 "use_key", 

1477 is_flag=True, 

1478 help=_msg.TranslatedString( 

1479 _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT 

1480 ), 

1481 cls=cli_machinery.PassphraseGenerationOption, 

1482) 

1483@click.option( 

1484 "-l", 

1485 "--length", 

1486 metavar=_msg.TranslatedString( 

1487 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1488 ), 

1489 callback=cli_machinery.validate_length, 

1490 help=_msg.TranslatedString( 

1491 _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT, 

1492 metavar=_msg.TranslatedString( 

1493 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1494 ), 

1495 ), 

1496 cls=cli_machinery.PassphraseGenerationOption, 

1497) 

1498@click.option( 

1499 "-r", 

1500 "--repeat", 

1501 metavar=_msg.TranslatedString( 

1502 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1503 ), 

1504 callback=cli_machinery.validate_occurrence_constraint, 

1505 help=_msg.TranslatedString( 

1506 _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT, 

1507 metavar=_msg.TranslatedString( 

1508 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1509 ), 

1510 ), 

1511 cls=cli_machinery.PassphraseGenerationOption, 

1512) 

1513@click.option( 

1514 "--lower", 

1515 metavar=_msg.TranslatedString( 

1516 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1517 ), 

1518 callback=cli_machinery.validate_occurrence_constraint, 

1519 help=_msg.TranslatedString( 

1520 _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT, 

1521 metavar=_msg.TranslatedString( 

1522 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1523 ), 

1524 ), 

1525 cls=cli_machinery.PassphraseGenerationOption, 

1526) 

1527@click.option( 

1528 "--upper", 

1529 metavar=_msg.TranslatedString( 

1530 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1531 ), 

1532 callback=cli_machinery.validate_occurrence_constraint, 

1533 help=_msg.TranslatedString( 

1534 _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT, 

1535 metavar=_msg.TranslatedString( 

1536 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1537 ), 

1538 ), 

1539 cls=cli_machinery.PassphraseGenerationOption, 

1540) 

1541@click.option( 

1542 "--number", 

1543 metavar=_msg.TranslatedString( 

1544 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1545 ), 

1546 callback=cli_machinery.validate_occurrence_constraint, 

1547 help=_msg.TranslatedString( 

1548 _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT, 

1549 metavar=_msg.TranslatedString( 

1550 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1551 ), 

1552 ), 

1553 cls=cli_machinery.PassphraseGenerationOption, 

1554) 

1555@click.option( 

1556 "--space", 

1557 metavar=_msg.TranslatedString( 

1558 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1559 ), 

1560 callback=cli_machinery.validate_occurrence_constraint, 

1561 help=_msg.TranslatedString( 

1562 _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT, 

1563 metavar=_msg.TranslatedString( 

1564 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1565 ), 

1566 ), 

1567 cls=cli_machinery.PassphraseGenerationOption, 

1568) 

1569@click.option( 

1570 "--dash", 

1571 metavar=_msg.TranslatedString( 

1572 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1573 ), 

1574 callback=cli_machinery.validate_occurrence_constraint, 

1575 help=_msg.TranslatedString( 

1576 _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT, 

1577 metavar=_msg.TranslatedString( 

1578 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1579 ), 

1580 ), 

1581 cls=cli_machinery.PassphraseGenerationOption, 

1582) 

1583@click.option( 

1584 "--symbol", 

1585 metavar=_msg.TranslatedString( 

1586 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1587 ), 

1588 callback=cli_machinery.validate_occurrence_constraint, 

1589 help=_msg.TranslatedString( 

1590 _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT, 

1591 metavar=_msg.TranslatedString( 

1592 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1593 ), 

1594 ), 

1595 cls=cli_machinery.PassphraseGenerationOption, 

1596) 

1597@click.option( 

1598 "-n", 

1599 "--notes", 

1600 "edit_notes", 

1601 is_flag=True, 

1602 help=_msg.TranslatedString( 

1603 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT, 

1604 service_metavar=_msg.TranslatedString( 

1605 _msg.Label.VAULT_METAVAR_SERVICE 

1606 ), 

1607 ), 

1608 cls=cli_machinery.ConfigurationOption, 

1609) 

1610@click.option( 

1611 "-c", 

1612 "--config", 

1613 "store_config_only", 

1614 is_flag=True, 

1615 help=_msg.TranslatedString( 

1616 _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT, 

1617 service_metavar=_msg.TranslatedString( 

1618 _msg.Label.VAULT_METAVAR_SERVICE 

1619 ), 

1620 ), 

1621 cls=cli_machinery.ConfigurationOption, 

1622) 

1623@click.option( 

1624 "-x", 

1625 "--delete", 

1626 "delete_service_settings", 

1627 is_flag=True, 

1628 help=_msg.TranslatedString( 

1629 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT, 

1630 service_metavar=_msg.TranslatedString( 

1631 _msg.Label.VAULT_METAVAR_SERVICE 

1632 ), 

1633 ), 

1634 cls=cli_machinery.ConfigurationOption, 

1635) 

1636@click.option( 

1637 "--delete-globals", 

1638 is_flag=True, 

1639 help=_msg.TranslatedString( 

1640 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT, 

1641 ), 

1642 cls=cli_machinery.ConfigurationOption, 

1643) 

1644@click.option( 

1645 "-X", 

1646 "--clear", 

1647 "clear_all_settings", 

1648 is_flag=True, 

1649 help=_msg.TranslatedString( 

1650 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT, 

1651 ), 

1652 cls=cli_machinery.ConfigurationOption, 

1653) 

1654@click.option( 

1655 "-e", 

1656 "--export", 

1657 "export_settings", 

1658 metavar=_msg.TranslatedString( 

1659 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1660 ), 

1661 help=_msg.TranslatedString( 

1662 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT, 

1663 metavar=_msg.TranslatedString( 

1664 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1665 ), 

1666 ), 

1667 cls=cli_machinery.StorageManagementOption, 

1668 shell_complete=cli_helpers.shell_complete_path, 

1669) 

1670@click.option( 

1671 "-i", 

1672 "--import", 

1673 "import_settings", 

1674 metavar=_msg.TranslatedString( 

1675 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1676 ), 

1677 help=_msg.TranslatedString( 

1678 _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT, 

1679 metavar=_msg.TranslatedString( 

1680 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1681 ), 

1682 ), 

1683 cls=cli_machinery.StorageManagementOption, 

1684 shell_complete=cli_helpers.shell_complete_path, 

1685) 

1686@click.option( 

1687 "--overwrite-existing/--merge-existing", 

1688 "overwrite_config", 

1689 default=False, 

1690 help=_msg.TranslatedString( 

1691 _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT 

1692 ), 

1693 cls=cli_machinery.CompatibilityOption, 

1694) 

1695@click.option( 

1696 "--unset", 

1697 "unset_settings", 

1698 multiple=True, 

1699 type=click.Choice([ 

1700 "phrase", 

1701 "key", 

1702 "length", 

1703 "repeat", 

1704 "lower", 

1705 "upper", 

1706 "number", 

1707 "space", 

1708 "dash", 

1709 "symbol", 

1710 "notes", 

1711 ]), 

1712 help=_msg.TranslatedString( 

1713 _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT 

1714 ), 

1715 cls=cli_machinery.CompatibilityOption, 

1716) 

1717@click.option( 

1718 "--export-as", 

1719 type=click.Choice(["json", "sh"]), 

1720 default="json", 

1721 help=_msg.TranslatedString( 

1722 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT 

1723 ), 

1724 cls=cli_machinery.CompatibilityOption, 

1725) 

1726@click.option( 

1727 "--modern-editor-interface/--vault-legacy-editor-interface", 

1728 "modern_editor_interface", 

1729 default=False, 

1730 help=_msg.TranslatedString( 

1731 _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT 

1732 ), 

1733 cls=cli_machinery.CompatibilityOption, 

1734) 

1735@click.option( 

1736 "--print-notes-before/--print-notes-after", 

1737 "print_notes_before", 

1738 default=False, 

1739 help=_msg.TranslatedString( 

1740 _msg.Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT, 

1741 service_metavar=_msg.TranslatedString( 

1742 _msg.Label.VAULT_METAVAR_SERVICE 

1743 ), 

1744 ), 

1745 cls=cli_machinery.CompatibilityOption, 

1746) 

1747@click.option( 

1748 "--ssh-agent-socket-provider", 

1749 "ssh_agent_socket_provider", 

1750 metavar=_msg.TranslatedString( 

1751 _msg.Label.VAULT_COMPATIBILITY_METAVAR_SSH_AGENT_SOCKET_PROVIDER 

1752 ), 

1753 help=_msg.TranslatedString( 

1754 _msg.Label.DERIVEPASSPHRASE_VAULT_SSH_AGENT_SOCKET_PROVIDER_HELP_TEXT, 

1755 metavar=_msg.TranslatedString( 

1756 _msg.Label.VAULT_COMPATIBILITY_METAVAR_SSH_AGENT_SOCKET_PROVIDER 

1757 ), 

1758 ), 

1759 cls=cli_machinery.CompatibilityOption, 

1760) 

1761@cli_machinery.version_option(cli_machinery.vault_version_option_callback) 

1762@cli_machinery.color_forcing_pseudo_option 

1763@cli_machinery.standard_logging_options 

1764@click.argument( 

1765 "service", 

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

1767 required=False, 

1768 default=None, 

1769 shell_complete=cli_helpers.shell_complete_service, 

1770) 

1771@click.pass_context 

1772def derivepassphrase_vault( 

1773 ctx: click.Context, 

1774 /, 

1775 **_kwargs: Any, # noqa: ANN401 

1776) -> None: 

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

1778 

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

1780 and not intended for programmatic use. See the 

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

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

1783 programmatic invocation.) 

1784 

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

1786 

1787 Parameters: 

1788 ctx (click.Context): 

1789 The `click` context. 

1790 

1791 Other Parameters: 

1792 service (str | None): 

1793 A service name. Required, unless operating on global 

1794 settings or importing/exporting settings. 

1795 use_phrase (bool): 

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

1797 user for a passphrase instead of an SSH key. 

1798 use_key (bool): 

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

1800 user for an SSH key instead of a passphrase. 

1801 length (int | None): 

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

1803 length of the generated passphrase. 

1804 repeat (int | None): 

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

1806 repetition limit if positive, or disable the repetition 

1807 limit if 0. 

1808 lower (int | None): 

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

1810 ASCII lowercase characters if positive, else forbid ASCII 

1811 lowercase characters if 0. 

1812 upper (int | None): 

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

1814 ASCII uppercase characters. 

1815 number (int | None): 

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

1817 ASCII digits. 

1818 space (int | None): 

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

1820 the space character. 

1821 dash (int | None): 

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

1823 the hyphen-minus and underscore characters. 

1824 symbol (int | None): 

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

1826 all other ASCII printable characters except lowercase 

1827 characters, uppercase characters, digits, space and 

1828 backquote. 

1829 edit_notes (bool): 

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

1831 editor to edit notes for `service`. 

1832 store_config_only (bool): 

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

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

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

1836 global settings. 

1837 delete_service_settings (bool): 

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

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

1840 delete_globals (bool): 

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

1842 the global settings from the configuration file. 

1843 clear_all_settings (bool): 

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

1845 settings from the configuration file. 

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

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

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

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

1850 standard output is supported. 

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

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

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

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

1855 input is supported. 

1856 overwrite_config (bool): 

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

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

1859 and config importing overwrite existing configurations, or 

1860 merge them section-wise instead. 

1861 unset_settings (Sequence[str]): 

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

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

1864 any other changes requested). 

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

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

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

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

1869 modern_editor_interface (bool): 

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

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

1872 whether editing notes uses a modern editor interface 

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

1874 legacy editor interface (WYSIWYG notes contents). 

1875 print_notes_before (bool): 

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

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

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

1879 ssh_agent_socket_provider (str): 

1880 Command-line argument `--ssh-agent-socket-provider`. Names 

1881 an explicit SSH agent socket provider to use, overriding any 

1882 user configuration entries or platform defaults. 

1883 

1884 """ 

1885 vault_context = _VaultContext(ctx) 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

1886 vault_context.validate_command_line() 1adicH56qfuWXY7hDexyzg%2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

1887 vault_context.dispatch_op() 1adicH56qfuWXY7hDexyzg2ZbMIJPNKnEL8!#90$413ApQRStvTOCUVrowsmBjlFGk

1888 

1889 

1890if __name__ == "__main__": 

1891 derivepassphrase()