Marco Ricci commited on 2024-09-01 13:43:33
Zeige 13 geänderte Dateien mit 351 Einfügungen und 136 Löschungen.
Results in many cosmetical code changes, and many documentation fixes.
(In particular, the docstring for
`derivepassphrase.cli.derivepassphrase` contained typos in the option
descriptions.) Furthermore, three other changes are made:
- Ignore `FURB101` and `FURB103` as well, which deal with similar
functionality to the deselected `PTH` rules.
- Change the attributes of the `VaultNativeConfigParser` class (and
its subclasses) to private, instead of public. Change the
`DummyModule` class in `derivepassphrase.exporter.storeroom` and
`derivepassphrase.exporter.vault_v03_and_below` to private as well.
- The modules `derivepassphrase.exporter.storeroom` and
`derivepassphrase.exporter.vault_v03_and_below` are now no longer
executable files.
| ... | ... |
@@ -189,27 +189,30 @@ quote-style = 'single' |
| 189 | 189 |
[tool.ruff.lint] |
| 190 | 190 |
ignore = [ |
| 191 | 191 |
# Suggested ignore by ruff when also using ruff to format. We *do* |
| 192 |
- # check for E501, because this usually only happens when there is a text |
|
| 193 |
- # string that should be manually broken. |
|
| 194 |
- 'W191', 'E111', 'E114', 'E117', 'D206', 'D300', 'Q000', 'Q001', 'Q002', |
|
| 195 |
- 'Q003', 'COM812', 'COM819', 'ISC001', 'ISC002', |
|
| 192 |
+ # check for E501, because this usually only happens when there is |
|
| 193 |
+ # a text string that should be manually broken. |
|
| 194 |
+ 'W191', 'E111', 'E114', 'E117', 'D206', 'D300', 'Q000', 'Q001', |
|
| 195 |
+ 'Q002', 'Q003', 'COM812', 'COM819', 'ISC001', 'ISC002', |
|
| 196 | 196 |
# We use `assert` regularly to appease the type checker, and because |
| 197 | 197 |
# it is the right language tool for this job. |
| 198 | 198 |
'S101', |
| 199 | 199 |
# The formatter takes care of trailing commas and docstring code |
| 200 | 200 |
# automatically. |
| 201 | 201 |
'COM812', 'W505', |
| 202 |
- # We document transitive exceptions as well (if we feel they would be |
|
| 203 |
- # surprising to the user otherwise). |
|
| 202 |
+ # We document transitive exceptions as well (if we feel they would |
|
| 203 |
+ # be surprising to the user otherwise). |
|
| 204 | 204 |
'DOC502', |
| 205 |
- # We currently don't have issues for every TODO. Forcing an issue also |
|
| 206 |
- # goes against the philosophy of TODOs as low-overhead markers for |
|
| 207 |
- # future work; see |
|
| 205 |
+ # We currently don't have issues for every TODO. Forcing an issue |
|
| 206 |
+ # also goes against the philosophy of TODOs as low-overhead markers |
|
| 207 |
+ # for future work; see |
|
| 208 | 208 |
# https://gist.github.com/dmnd/ed5d8ef8de2e4cfea174bd5dafcda382 . |
| 209 | 209 |
'TD003', |
| 210 |
- # We somewhat regularly use loops where each iteration needs a separate |
|
| 211 |
- # try-except block. |
|
| 210 |
+ # We somewhat regularly use loops where each iteration needs |
|
| 211 |
+ # a separate try-except block. |
|
| 212 | 212 |
'PERF203', |
| 213 |
+ # We do not currently use pathlib. The PTH rules are unselected, |
|
| 214 |
+ # but FURB includes several pathlib-related rules. |
|
| 215 |
+ 'FURB101', 'FURB103', |
|
| 213 | 216 |
# We catch type-ignore comments without specific code via the mypy |
| 214 | 217 |
# configuration, not via ruff. |
| 215 | 218 |
'PGH003', |
| ... | ... |
@@ -226,13 +229,13 @@ select = [ |
| 226 | 229 |
'INT', 'ARG', |
| 227 | 230 |
# We currently do not use pathlib. Disable 'PTH'. |
| 228 | 231 |
'TD', |
| 229 |
- # We use TODOs and FIXMEs as notes for later, and don't want the linter |
|
| 230 |
- # to nag about every occurrence. Disable 'FIX'. |
|
| 232 |
+ # We use TODOs and FIXMEs as notes for later, and don't want the |
|
| 233 |
+ # linter to nag about every occurrence. Disable 'FIX'. |
|
| 231 | 234 |
# |
| 232 |
- # The "eradicate" rule is prone to a lot of false positives, and it is |
|
| 233 |
- # unclear to me, and probably confusing to read, where to apply a noqa |
|
| 234 |
- # marker. Instead, disable 'ERA', and if necessary, specify it on the |
|
| 235 |
- # command-line. |
|
| 235 |
+ # The "eradicate" rule is prone to a lot of false positives, and it |
|
| 236 |
+ # is unclear to me, and probably confusing to read, where to apply |
|
| 237 |
+ # a noqa marker. Instead, disable 'ERA', and if necessary, specify |
|
| 238 |
+ # it on the command-line. |
|
| 236 | 239 |
'PD', 'PGH', 'PL', 'TRY', 'FLY', 'NPY', 'FAST', |
| 237 | 240 |
'AIR', 'PERF', 'FURB', 'DOC', 'RUF', |
| 238 | 241 |
] |
| ... | ... |
@@ -251,8 +254,9 @@ select = [ |
| 251 | 254 |
'PLC1901', |
| 252 | 255 |
# Suggested by hatch, assumingly because tests may use "magic values". |
| 253 | 256 |
'PLR2004', |
| 254 |
- # Suggested by hatch, because tests are typically organized as classes and |
|
| 255 |
- # instance methods but may not really be using the `self` argument. |
|
| 257 |
+ # Suggested by hatch, because tests are typically organized as classes |
|
| 258 |
+ # and instance methods but may not really be using the `self` |
|
| 259 |
+ # argument. |
|
| 256 | 260 |
'PLR6301', |
| 257 | 261 |
# Suggested by hatch, because these warnings may be precisely what the |
| 258 | 262 |
# tests are supposed to test. |
| ... | ... |
@@ -274,8 +278,8 @@ select = [ |
| 274 | 278 |
# Importing this from the tests directory would then automatically |
| 275 | 279 |
# trigger `PLC2701`. |
| 276 | 280 |
'PLC2701', |
| 277 |
- # Too many public methods/arguments/returns/branches/locals doesn't really |
|
| 278 |
- # apply here. |
|
| 281 |
+ # Too many public methods/arguments/returns/branches/locals doesn't |
|
| 282 |
+ # really apply here. |
|
| 279 | 283 |
'PLR0904', 'PLR0911', 'PLR0912', 'PLR0913', 'PLR0914', 'PLR0915', |
| 280 | 284 |
'PLR0916', 'PLR0917', |
| 281 | 285 |
# To fully test the `derivepassphrase.cli` module (and a couple other |
| ... | ... |
@@ -2,7 +2,7 @@ |
| 2 | 2 |
# |
| 3 | 3 |
# SPDX-License-Identifier: MIT |
| 4 | 4 |
|
| 5 |
-"""Work-alike of vault(1) – a deterministic, stateless password manager""" # noqa: RUF002 |
|
| 5 |
+"""Work-alike of vault(1) – a deterministic, stateless password manager""" # noqa: D415,RUF002 |
|
| 6 | 6 |
|
| 7 | 7 |
__author__ = 'Marco Ricci <m@the13thletter.info>' |
| 8 | 8 |
__version__ = '0.1.3' |
| ... | ... |
@@ -114,7 +114,7 @@ class VaultConfig(TypedDict, _VaultConfig, total=False): |
| 114 | 114 |
services: Required[dict[str, VaultConfigServicesSettings]] |
| 115 | 115 |
|
| 116 | 116 |
|
| 117 |
-def is_vault_config(obj: Any) -> TypeGuard[VaultConfig]: |
|
| 117 |
+def is_vault_config(obj: Any) -> TypeGuard[VaultConfig]: # noqa: ANN401,C901,PLR0911,PLR0912 |
|
| 118 | 118 |
"""Check if `obj` is a valid vault config, according to typing. |
| 119 | 119 |
|
| 120 | 120 |
Args: |
| ... | ... |
@@ -123,7 +123,7 @@ def _save_config(config: _types.VaultConfig, /) -> None: |
| 123 | 123 |
os.makedirs(filedir, exist_ok=False) |
| 124 | 124 |
except FileExistsError: |
| 125 | 125 |
if not os.path.isdir(filedir): |
| 126 |
- raise |
|
| 126 |
+ raise # noqa: DOC501 |
|
| 127 | 127 |
with open(filename, 'w', encoding='UTF-8') as fileobj: |
| 128 | 128 |
json.dump(config, fileobj) |
| 129 | 129 |
|
| ... | ... |
@@ -186,7 +186,7 @@ def _get_suitable_ssh_keys( |
| 186 | 186 |
case _: # pragma: no cover |
| 187 | 187 |
assert_never(conn) |
| 188 | 188 |
msg = f'invalid connection hint: {conn!r}'
|
| 189 |
- raise TypeError(msg) |
|
| 189 |
+ raise TypeError(msg) # noqa: DOC501 |
|
| 190 | 190 |
with client_context: |
| 191 | 191 |
try: |
| 192 | 192 |
all_key_comment_pairs = list(client.list_keys()) |
| ... | ... |
@@ -215,6 +215,8 @@ def _prompt_for_selection( |
| 215 | 215 |
instead, and return the correct index. |
| 216 | 216 |
|
| 217 | 217 |
Args: |
| 218 |
+ items: |
|
| 219 |
+ The list of items to choose from. |
|
| 218 | 220 |
heading: |
| 219 | 221 |
A heading for the list of items, to print immediately |
| 220 | 222 |
before. Defaults to a reasonable standard heading. If |
| ... | ... |
@@ -408,7 +410,7 @@ class OptionGroupOption(click.Option): |
| 408 | 410 |
option_group_name: str = '' |
| 409 | 411 |
epilog: str = '' |
| 410 | 412 |
|
| 411 |
- def __init__(self, *args: Any, **kwargs: Any) -> None: |
|
| 413 |
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 |
|
| 412 | 414 |
if self.__class__ == __class__: # type: ignore[name-defined] |
| 413 | 415 |
raise NotImplementedError |
| 414 | 416 |
super().__init__(*args, **kwargs) |
| ... | ... |
@@ -449,10 +451,6 @@ class CommandWithHelpGroups(click.Command): |
| 449 | 451 |
formatter: |
| 450 | 452 |
The formatter for the `--help` listing. |
| 451 | 453 |
|
| 452 |
- Returns: |
|
| 453 |
- Nothing. Output is generated by calling appropriate methods |
|
| 454 |
- on `formatter` instead. |
|
| 455 |
- |
|
| 456 | 454 |
""" |
| 457 | 455 |
help_records: dict[str, list[tuple[str, str]]] = {}
|
| 458 | 456 |
epilogs: dict[str, str] = {}
|
| ... | ... |
@@ -519,9 +517,22 @@ class StorageManagementOption(OptionGroupOption): |
| 519 | 517 |
def _validate_occurrence_constraint( |
| 520 | 518 |
ctx: click.Context, |
| 521 | 519 |
param: click.Parameter, |
| 522 |
- value: Any, |
|
| 520 |
+ value: Any, # noqa: ANN401 |
|
| 523 | 521 |
) -> int | None: |
| 524 |
- """Check that the occurrence constraint is valid (int, 0 or larger).""" |
|
| 522 |
+ """Check that the occurrence constraint is valid (int, 0 or larger). |
|
| 523 |
+ |
|
| 524 |
+ Args: |
|
| 525 |
+ ctx: The `click` context. |
|
| 526 |
+ param: The current command-line parameter. |
|
| 527 |
+ value: The parameter value to be checked. |
|
| 528 |
+ |
|
| 529 |
+ Returns: |
|
| 530 |
+ The parsed parameter value. |
|
| 531 |
+ |
|
| 532 |
+ Raises: |
|
| 533 |
+ click.BadParameter: The parameter value is invalid. |
|
| 534 |
+ |
|
| 535 |
+ """ |
|
| 525 | 536 |
del ctx # Unused. |
| 526 | 537 |
del param # Unused. |
| 527 | 538 |
if value is None: |
| ... | ... |
@@ -543,9 +554,22 @@ def _validate_occurrence_constraint( |
| 543 | 554 |
def _validate_length( |
| 544 | 555 |
ctx: click.Context, |
| 545 | 556 |
param: click.Parameter, |
| 546 |
- value: Any, |
|
| 557 |
+ value: Any, # noqa: ANN401 |
|
| 547 | 558 |
) -> int | None: |
| 548 |
- """Check that the length is valid (int, 1 or larger).""" |
|
| 559 |
+ """Check that the length is valid (int, 1 or larger). |
|
| 560 |
+ |
|
| 561 |
+ Args: |
|
| 562 |
+ ctx: The `click` context. |
|
| 563 |
+ param: The current command-line parameter. |
|
| 564 |
+ value: The parameter value to be checked. |
|
| 565 |
+ |
|
| 566 |
+ Returns: |
|
| 567 |
+ The parsed parameter value. |
|
| 568 |
+ |
|
| 569 |
+ Raises: |
|
| 570 |
+ click.BadParameter: The parameter value is invalid. |
|
| 571 |
+ |
|
| 572 |
+ """ |
|
| 549 | 573 |
del ctx # Unused. |
| 550 | 574 |
del param # Unused. |
| 551 | 575 |
if value is None: |
| ... | ... |
@@ -729,7 +753,7 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
| 729 | 753 |
@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
| 730 | 754 |
@click.argument('service', required=False)
|
| 731 | 755 |
@click.pass_context |
| 732 |
-def derivepassphrase( |
|
| 756 |
+def derivepassphrase( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915 |
|
| 733 | 757 |
ctx: click.Context, |
| 734 | 758 |
/, |
| 735 | 759 |
*, |
| ... | ... |
@@ -807,13 +831,13 @@ def derivepassphrase( |
| 807 | 831 |
Command-line argument `--number`. Same as `lower`, but for |
| 808 | 832 |
ASCII digits. |
| 809 | 833 |
space: |
| 810 |
- Command-line argument `--number`. Same as `lower`, but for |
|
| 834 |
+ Command-line argument `--space`. Same as `lower`, but for |
|
| 811 | 835 |
the space character. |
| 812 | 836 |
dash: |
| 813 |
- Command-line argument `--number`. Same as `lower`, but for |
|
| 837 |
+ Command-line argument `--dash`. Same as `lower`, but for |
|
| 814 | 838 |
the hyphen-minus and underscore characters. |
| 815 | 839 |
symbol: |
| 816 |
- Command-line argument `--number`. Same as `lower`, but for |
|
| 840 |
+ Command-line argument `--symbol`. Same as `lower`, but for |
|
| 817 | 841 |
all other ASCII printable characters (except backquote). |
| 818 | 842 |
edit_notes: |
| 819 | 843 |
Command-line argument `-n`/`--notes`. If given, spawn an |
| ... | ... |
@@ -843,8 +867,7 @@ def derivepassphrase( |
| 843 | 867 |
a filename to open for reading. Using `-` for standard |
| 844 | 868 |
input is supported. |
| 845 | 869 |
|
| 846 |
- """ |
|
| 847 |
- |
|
| 870 |
+ """ # noqa: D301 |
|
| 848 | 871 |
options_in_group: dict[type[click.Option], list[click.Option]] = {}
|
| 849 | 872 |
params_by_str: dict[str, click.Parameter] = {}
|
| 850 | 873 |
for param in ctx.command.params: |
| ... | ... |
@@ -858,7 +881,7 @@ def derivepassphrase( |
| 858 | 881 |
case StorageManagementOption(): |
| 859 | 882 |
group = StorageManagementOption |
| 860 | 883 |
case OptionGroupOption(): |
| 861 |
- raise AssertionError( # noqa: TRY003 |
|
| 884 |
+ raise AssertionError( # noqa: DOC501,TRY003 |
|
| 862 | 885 |
f'Unknown option group for {param!r}' # noqa: EM102
|
| 863 | 886 |
) |
| 864 | 887 |
case _: |
| ... | ... |
@@ -940,7 +963,7 @@ def derivepassphrase( |
| 940 | 963 |
if is_param_set(param) and not service: |
| 941 | 964 |
opt_str = param.opts[0] |
| 942 | 965 |
msg = f'{opt_str} requires a SERVICE'
|
| 943 |
- raise click.UsageError(msg) |
|
| 966 |
+ raise click.UsageError(msg) # noqa: DOC501 |
|
| 944 | 967 |
for param in [params_by_str['--key'], params_by_str['--phrase']]: |
| 945 | 968 |
if is_param_set(param) and not ( |
| 946 | 969 |
service or is_param_set(params_by_str['--config']) |
| ... | ... |
@@ -967,7 +990,7 @@ def derivepassphrase( |
| 967 | 990 |
).get('notes', '')
|
| 968 | 991 |
notes_value = click.edit(text=text) |
| 969 | 992 |
if notes_value is not None: |
| 970 |
- notes_lines = collections.deque(notes_value.splitlines(True)) |
|
| 993 |
+ notes_lines = collections.deque(notes_value.splitlines(True)) # noqa: FBT003 |
|
| 971 | 994 |
while notes_lines: |
| 972 | 995 |
line = notes_lines.popleft() |
| 973 | 996 |
if line.startswith(DEFAULT_NOTES_MARKER): |
| ... | ... |
@@ -995,7 +1018,8 @@ def derivepassphrase( |
| 995 | 1018 |
put_config({'services': {}})
|
| 996 | 1019 |
elif import_settings: |
| 997 | 1020 |
try: |
| 998 |
- # TODO: keep track of auto-close; try os.dup if feasible |
|
| 1021 |
+ # TODO(the-13th-letter): keep track of auto-close; try |
|
| 1022 |
+ # os.dup if feasible |
|
| 999 | 1023 |
infile = ( |
| 1000 | 1024 |
cast(TextIO, import_settings) |
| 1001 | 1025 |
if hasattr(import_settings, 'close') |
| ... | ... |
@@ -1032,7 +1056,8 @@ def derivepassphrase( |
| 1032 | 1056 |
elif export_settings: |
| 1033 | 1057 |
configuration = get_config() |
| 1034 | 1058 |
try: |
| 1035 |
- # TODO: keep track of auto-close; try os.dup if feasible |
|
| 1059 |
+ # TODO(the-13th-letter): keep track of auto-close; try |
|
| 1060 |
+ # os.dup if feasible |
|
| 1036 | 1061 |
outfile = ( |
| 1037 | 1062 |
cast(TextIO, export_settings) |
| 1038 | 1063 |
if hasattr(export_settings, 'close') |
| ... | ... |
@@ -1,4 +1,8 @@ |
| 1 |
-#!/usr/bin/python3 |
|
| 1 |
+# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: MIT |
|
| 4 |
+ |
|
| 5 |
+"""Exporter for the vault "storeroom" configuration format.""" |
|
| 2 | 6 |
|
| 3 | 7 |
from __future__ import annotations |
| 4 | 8 |
|
| ... | ... |
@@ -31,18 +35,18 @@ else: |
| 31 | 35 |
from cryptography.hazmat.primitives.kdf import pbkdf2 |
| 32 | 36 |
except ModuleNotFoundError as exc: |
| 33 | 37 |
|
| 34 |
- class DummyModule: # pragma: no cover |
|
| 38 |
+ class _DummyModule: # pragma: no cover |
|
| 35 | 39 |
def __init__(self, exc: type[Exception]) -> None: |
| 36 | 40 |
self.exc = exc |
| 37 | 41 |
|
| 38 |
- def __getattr__(self, name: str) -> Any: |
|
| 39 |
- def func(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001 |
|
| 42 |
+ def __getattr__(self, name: str) -> Any: # noqa: ANN401 |
|
| 43 |
+ def func(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401,ARG001 |
|
| 40 | 44 |
raise self.exc |
| 41 | 45 |
|
| 42 | 46 |
return func |
| 43 | 47 |
|
| 44 |
- ciphers = hashes = hmac = padding = DummyModule(exc) |
|
| 45 |
- algorithms = modes = pbkdf2 = DummyModule(exc) |
|
| 48 |
+ ciphers = hashes = hmac = padding = _DummyModule(exc) |
|
| 49 |
+ algorithms = modes = pbkdf2 = _DummyModule(exc) |
|
| 46 | 50 |
STUBBED = True |
| 47 | 51 |
else: |
| 48 | 52 |
STUBBED = False |
| ... | ... |
@@ -58,11 +62,36 @@ logger = logging.getLogger(__name__) |
| 58 | 62 |
|
| 59 | 63 |
|
| 60 | 64 |
class KeyPair(TypedDict): |
| 65 |
+ """A pair of AES256 keys, one for encryption and one for signing. |
|
| 66 |
+ |
|
| 67 |
+ Attributes: |
|
| 68 |
+ encryption_key: |
|
| 69 |
+ AES256 key, used for encryption with AES256-CBC (with PKCS#7 |
|
| 70 |
+ padding). |
|
| 71 |
+ signing_key: |
|
| 72 |
+ AES256 key, used for signing with HMAC-SHA256. |
|
| 73 |
+ |
|
| 74 |
+ """ |
|
| 75 |
+ |
|
| 61 | 76 |
encryption_key: bytes |
| 62 | 77 |
signing_key: bytes |
| 63 | 78 |
|
| 64 | 79 |
|
| 65 | 80 |
class MasterKeys(TypedDict): |
| 81 |
+ """A triple of AES256 keys, for encryption, signing and hashing. |
|
| 82 |
+ |
|
| 83 |
+ Attributes: |
|
| 84 |
+ hashing_key: |
|
| 85 |
+ AES256 key, used for hashing with HMAC-SHA256 to derive |
|
| 86 |
+ a hash table slot for an item. |
|
| 87 |
+ encryption_key: |
|
| 88 |
+ AES256 key, used for encryption with AES256-CBC (with PKCS#7 |
|
| 89 |
+ padding). |
|
| 90 |
+ signing_key: |
|
| 91 |
+ AES256 key, used for signing with HMAC-SHA256. |
|
| 92 |
+ |
|
| 93 |
+ """ |
|
| 94 |
+ |
|
| 66 | 95 |
hashing_key: bytes |
| 67 | 96 |
encryption_key: bytes |
| 68 | 97 |
signing_key: bytes |
| ... | ... |
@@ -94,7 +123,7 @@ def derive_master_keys_keys(password: str | bytes, iterations: int) -> KeyPair: |
| 94 | 123 |
if isinstance(password, str): |
| 95 | 124 |
password = password.encode('ASCII')
|
| 96 | 125 |
master_keys_keys_blob = pbkdf2.PBKDF2HMAC( |
| 97 |
- algorithm=hashes.SHA1(), # noqa: S303 |
|
| 126 |
+ algorithm=hashes.SHA1(), |
|
| 98 | 127 |
length=2 * KEY_SIZE, |
| 99 | 128 |
salt=STOREROOM_MASTER_KEYS_UUID, |
| 100 | 129 |
iterations=iterations, |
| ... | ... |
@@ -157,6 +186,15 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
| 157 | 186 |
Returns: |
| 158 | 187 |
The master encryption, signing and hashing keys. |
| 159 | 188 |
|
| 189 |
+ Raises: |
|
| 190 |
+ cryptography.exceptions.InvalidSignature: |
|
| 191 |
+ The data does not contain a valid signature under the given |
|
| 192 |
+ key. |
|
| 193 |
+ ValueError: |
|
| 194 |
+ The format is invalid, in a non-cryptographic way. (For |
|
| 195 |
+ example, it contains an unsupported version marker, or |
|
| 196 |
+ unexpected extra contents, or invalid padding.) |
|
| 197 |
+ |
|
| 160 | 198 |
""" |
| 161 | 199 |
ciphertext, claimed_mac = struct.unpack( |
| 162 | 200 |
f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
|
| ... | ... |
@@ -195,7 +233,7 @@ def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys: |
| 195 | 233 |
f'Expecting 3 encrypted keys at {3 * KEY_SIZE} bytes total, '
|
| 196 | 234 |
f'but found {len(plaintext)} instead'
|
| 197 | 235 |
) |
| 198 |
- raise RuntimeError(msg) |
|
| 236 |
+ raise ValueError(msg) |
|
| 199 | 237 |
hashing_key, encryption_key, signing_key = struct.unpack( |
| 200 | 238 |
f'{KEY_SIZE}s {KEY_SIZE}s {KEY_SIZE}s', plaintext
|
| 201 | 239 |
) |
| ... | ... |
@@ -238,8 +276,16 @@ def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair: |
| 238 | 276 |
Returns: |
| 239 | 277 |
The bucket item's encryption and signing keys. |
| 240 | 278 |
|
| 241 |
- """ |
|
| 279 |
+ Raises: |
|
| 280 |
+ cryptography.exceptions.InvalidSignature: |
|
| 281 |
+ The data does not contain a valid signature under the given |
|
| 282 |
+ key. |
|
| 283 |
+ ValueError: |
|
| 284 |
+ The format is invalid, in a non-cryptographic way. (For |
|
| 285 |
+ example, it contains an unsupported version marker, or |
|
| 286 |
+ unexpected extra contents, or invalid padding.) |
|
| 242 | 287 |
|
| 288 |
+ """ |
|
| 243 | 289 |
ciphertext, claimed_mac = struct.unpack( |
| 244 | 290 |
f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
|
| 245 | 291 |
) |
| ... | ... |
@@ -336,8 +382,16 @@ def decrypt_contents(data: bytes, session_keys: KeyPair) -> bytes: |
| 336 | 382 |
Returns: |
| 337 | 383 |
The bucket item's payload. |
| 338 | 384 |
|
| 339 |
- """ |
|
| 385 |
+ Raises: |
|
| 386 |
+ cryptography.exceptions.InvalidSignature: |
|
| 387 |
+ The data does not contain a valid signature under the given |
|
| 388 |
+ key. |
|
| 389 |
+ ValueError: |
|
| 390 |
+ The format is invalid, in a non-cryptographic way. (For |
|
| 391 |
+ example, it contains an unsupported version marker, or |
|
| 392 |
+ unexpected extra contents, or invalid padding.) |
|
| 340 | 393 |
|
| 394 |
+ """ |
|
| 341 | 395 |
ciphertext, claimed_mac = struct.unpack( |
| 342 | 396 |
f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
|
| 343 | 397 |
) |
| ... | ... |
@@ -389,6 +443,30 @@ def decrypt_contents(data: bytes, session_keys: KeyPair) -> bytes: |
| 389 | 443 |
|
| 390 | 444 |
|
| 391 | 445 |
def decrypt_bucket_item(bucket_item: bytes, master_keys: MasterKeys) -> bytes: |
| 446 |
+ """Decrypt a bucket item. |
|
| 447 |
+ |
|
| 448 |
+ Args: |
|
| 449 |
+ bucket_item: |
|
| 450 |
+ The encrypted bucket item. |
|
| 451 |
+ master_keys: |
|
| 452 |
+ The master keys. Presumably these have previously been |
|
| 453 |
+ obtained via the |
|
| 454 |
+ [`derivepassphrase.exporter.storeroom.decrypt_master_keys_data`][] |
|
| 455 |
+ function. |
|
| 456 |
+ |
|
| 457 |
+ Returns: |
|
| 458 |
+ The decrypted bucket item. |
|
| 459 |
+ |
|
| 460 |
+ Raises: |
|
| 461 |
+ cryptography.exceptions.InvalidSignature: |
|
| 462 |
+ The data does not contain a valid signature under the given |
|
| 463 |
+ key. |
|
| 464 |
+ ValueError: |
|
| 465 |
+ The format is invalid, in a non-cryptographic way. (For |
|
| 466 |
+ example, it contains an unsupported version marker, or |
|
| 467 |
+ unexpected extra contents, or invalid padding.) |
|
| 468 |
+ |
|
| 469 |
+ """ |
|
| 392 | 470 |
logger.debug( |
| 393 | 471 |
( |
| 394 | 472 |
'decrypt_bucket_item: data = bytes.fromhex(%s), ' |
| ... | ... |
@@ -408,7 +486,7 @@ def decrypt_bucket_item(bucket_item: bytes, master_keys: MasterKeys) -> bytes: |
| 408 | 486 |
) |
| 409 | 487 |
if data_version != 1: |
| 410 | 488 |
msg = f'Cannot handle version {data_version} encrypted data'
|
| 411 |
- raise RuntimeError(msg) |
|
| 489 |
+ raise ValueError(msg) |
|
| 412 | 490 |
session_keys = decrypt_session_keys(encrypted_session_keys, master_keys) |
| 413 | 491 |
return decrypt_contents(data_contents, session_keys) |
| 414 | 492 |
|
| ... | ... |
@@ -419,6 +497,34 @@ def decrypt_bucket_file( |
| 419 | 497 |
*, |
| 420 | 498 |
root_dir: str | bytes | os.PathLike = '.', |
| 421 | 499 |
) -> Iterator[bytes]: |
| 500 |
+ """Decrypt a bucket item. |
|
| 501 |
+ |
|
| 502 |
+ Args: |
|
| 503 |
+ filename: |
|
| 504 |
+ The bucket file's filename. |
|
| 505 |
+ master_keys: |
|
| 506 |
+ The master keys. Presumably these have previously been |
|
| 507 |
+ obtained via the |
|
| 508 |
+ [`derivepassphrase.exporter.storeroom.decrypt_master_keys_data`][] |
|
| 509 |
+ function. |
|
| 510 |
+ root_dir: |
|
| 511 |
+ The root directory of the data store. The filename is |
|
| 512 |
+ interpreted relatively to this directory. |
|
| 513 |
+ |
|
| 514 |
+ Yields: |
|
| 515 |
+ : |
|
| 516 |
+ A decrypted bucket item. |
|
| 517 |
+ |
|
| 518 |
+ Raises: |
|
| 519 |
+ cryptography.exceptions.InvalidSignature: |
|
| 520 |
+ The data does not contain a valid signature under the given |
|
| 521 |
+ key. |
|
| 522 |
+ ValueError: |
|
| 523 |
+ The format is invalid, in a non-cryptographic way. (For |
|
| 524 |
+ example, it contains an unsupported version marker, or |
|
| 525 |
+ unexpected extra contents, or invalid padding.) |
|
| 526 |
+ |
|
| 527 |
+ """ |
|
| 422 | 528 |
with open( |
| 423 | 529 |
os.path.join(os.fsdecode(root_dir), filename), 'rb' |
| 424 | 530 |
) as bucket_file: |
| ... | ... |
@@ -427,10 +533,10 @@ def decrypt_bucket_file( |
| 427 | 533 |
header = json.loads(header_line) |
| 428 | 534 |
except ValueError as exc: |
| 429 | 535 |
msg = f'Invalid bucket file: {filename}'
|
| 430 |
- raise RuntimeError(msg) from exc |
|
| 536 |
+ raise ValueError(msg) from exc |
|
| 431 | 537 |
if header != {'version': 1}:
|
| 432 | 538 |
msg = f'Invalid bucket file: {filename}'
|
| 433 |
- raise RuntimeError(msg) from None |
|
| 539 |
+ raise ValueError(msg) from None |
|
| 434 | 540 |
for line in bucket_file: |
| 435 | 541 |
yield decrypt_bucket_item( |
| 436 | 542 |
base64.standard_b64decode(line), master_keys |
| ... | ... |
@@ -467,7 +573,7 @@ def store(config: dict[str, Any], path: str, json_contents: bytes) -> None: |
| 467 | 573 |
config[path_parts[-1]] = contents |
| 468 | 574 |
|
| 469 | 575 |
|
| 470 |
-def export_storeroom_data( |
|
| 576 |
+def export_storeroom_data( # noqa: C901,PLR0912,PLR0914,PLR0915 |
|
| 471 | 577 |
storeroom_path: str | bytes | os.PathLike | None = None, |
| 472 | 578 |
master_keys_key: str | bytes | None = None, |
| 473 | 579 |
) -> dict[str, Any]: |
| ... | ... |
@@ -1,4 +1,8 @@ |
| 1 |
-#!/usr/bin/python3 |
|
| 1 |
+# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: MIT |
|
| 4 |
+ |
|
| 5 |
+"""Exporter for the vault native configuration format (v0.2 or v0.3).""" |
|
| 2 | 6 |
|
| 3 | 7 |
from __future__ import annotations |
| 4 | 8 |
|
| ... | ... |
@@ -36,19 +40,19 @@ else: |
| 36 | 40 |
from cryptography.hazmat.primitives.kdf import pbkdf2 |
| 37 | 41 |
except ModuleNotFoundError as exc: |
| 38 | 42 |
|
| 39 |
- class DummyModule: # pragma: no cover |
|
| 43 |
+ class _DummyModule: # pragma: no cover |
|
| 40 | 44 |
def __init__(self, exc: type[Exception]) -> None: |
| 41 | 45 |
self.exc = exc |
| 42 | 46 |
|
| 43 |
- def __getattr__(self, name: str) -> Any: |
|
| 44 |
- def func(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001 |
|
| 47 |
+ def __getattr__(self, name: str) -> Any: # noqa: ANN401 |
|
| 48 |
+ def func(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401,ARG001 |
|
| 45 | 49 |
raise self.exc |
| 46 | 50 |
|
| 47 | 51 |
return func |
| 48 | 52 |
|
| 49 |
- crypt_exceptions = crypt_utils = DummyModule(exc) |
|
| 50 |
- ciphers = hashes = hmac = padding = DummyModule(exc) |
|
| 51 |
- algorithms = modes = pbkdf2 = DummyModule(exc) |
|
| 53 |
+ crypt_exceptions = crypt_utils = _DummyModule(exc) |
|
| 54 |
+ ciphers = hashes = hmac = padding = _DummyModule(exc) |
|
| 55 |
+ algorithms = modes = pbkdf2 = _DummyModule(exc) |
|
| 52 | 56 |
STUBBED = True |
| 53 | 57 |
else: |
| 54 | 58 |
STUBBED = False |
| ... | ... |
@@ -61,26 +65,66 @@ def _h(bs: bytes | bytearray) -> str: |
| 61 | 65 |
|
| 62 | 66 |
|
| 63 | 67 |
class VaultNativeConfigParser(abc.ABC): |
| 68 |
+ """A base parser for vault's native configuration format. |
|
| 69 |
+ |
|
| 70 |
+ Certain details are specific to the respective vault versions, and |
|
| 71 |
+ are abstracted out. This class by itself is not instantiable |
|
| 72 |
+ because of this. |
|
| 73 |
+ |
|
| 74 |
+ """ |
|
| 75 |
+ |
|
| 64 | 76 |
def __init__(self, contents: Buffer, password: str | Buffer) -> None: |
| 77 |
+ """Initialize the parser. |
|
| 78 |
+ |
|
| 79 |
+ Args: |
|
| 80 |
+ contents: |
|
| 81 |
+ The binary contents of the encrypted configuration file. |
|
| 82 |
+ |
|
| 83 |
+ Note: On disk, these are usually stored in |
|
| 84 |
+ base64-encoded form, not in the "raw" form as needed |
|
| 85 |
+ here. |
|
| 86 |
+ |
|
| 87 |
+ password: |
|
| 88 |
+ The vault master key/master passphrase the file is |
|
| 89 |
+ encrypted with. Must be non-empty. See |
|
| 90 |
+ [`derivepassphrase.exporter.get_vault_key`][] for |
|
| 91 |
+ details. |
|
| 92 |
+ |
|
| 93 |
+ If this is a text string, then the UTF-8 encoding of the |
|
| 94 |
+ string is used as the binary password. |
|
| 95 |
+ |
|
| 96 |
+ """ |
|
| 65 | 97 |
if not password: |
| 66 | 98 |
msg = 'Password must not be empty' |
| 67 |
- raise ValueError(msg) |
|
| 99 |
+ raise ValueError(msg) # noqa: DOC501 |
|
| 68 | 100 |
self._contents = bytes(contents) |
| 69 |
- self.iv_size = 0 |
|
| 70 |
- self.mac_size = 0 |
|
| 71 |
- self.encryption_key = b'' |
|
| 72 |
- self.encryption_key_size = 0 |
|
| 73 |
- self.signing_key = b'' |
|
| 74 |
- self.signing_key_size = 0 |
|
| 75 |
- self.message = b'' |
|
| 76 |
- self.message_tag = b'' |
|
| 77 |
- self.iv = b'' |
|
| 78 |
- self.payload = b'' |
|
| 101 |
+ self._iv_size = 0 |
|
| 102 |
+ self._mac_size = 0 |
|
| 103 |
+ self._encryption_key = b'' |
|
| 104 |
+ self._encryption_key_size = 0 |
|
| 105 |
+ self._signing_key = b'' |
|
| 106 |
+ self._signing_key_size = 0 |
|
| 107 |
+ self._message = b'' |
|
| 108 |
+ self._message_tag = b'' |
|
| 109 |
+ self._iv = b'' |
|
| 110 |
+ self._payload = b'' |
|
| 79 | 111 |
self._password = password |
| 80 | 112 |
self._sentinel: object = object() |
| 81 | 113 |
self._data: Any = self._sentinel |
| 82 | 114 |
|
| 83 |
- def __call__(self) -> Any: |
|
| 115 |
+ def __call__(self) -> Any: # noqa: ANN401 |
|
| 116 |
+ """Return the decrypted and parsed vault configuration. |
|
| 117 |
+ |
|
| 118 |
+ Raises: |
|
| 119 |
+ cryptography.exceptions.InvalidSignature: |
|
| 120 |
+ The encrypted configuration does not contain a valid |
|
| 121 |
+ signature. |
|
| 122 |
+ ValueError: |
|
| 123 |
+ The format is invalid, in a non-cryptographic way. (For |
|
| 124 |
+ example, it contains an unsupported version marker, or |
|
| 125 |
+ unexpected extra contents, or invalid padding.) |
|
| 126 |
+ |
|
| 127 |
+ """ |
|
| 84 | 128 |
if self._data is self._sentinel: |
| 85 | 129 |
self._parse_contents() |
| 86 | 130 |
self._derive_keys() |
| ... | ... |
@@ -89,13 +133,13 @@ class VaultNativeConfigParser(abc.ABC): |
| 89 | 133 |
return self._data |
| 90 | 134 |
|
| 91 | 135 |
@staticmethod |
| 92 |
- def pbkdf2( |
|
| 136 |
+ def _pbkdf2( |
|
| 93 | 137 |
password: str | Buffer, key_size: int, iterations: int |
| 94 | 138 |
) -> bytes: |
| 95 | 139 |
if isinstance(password, str): |
| 96 | 140 |
password = password.encode('utf-8')
|
| 97 | 141 |
raw_key = pbkdf2.PBKDF2HMAC( |
| 98 |
- algorithm=hashes.SHA1(), # noqa: S303 |
|
| 142 |
+ algorithm=hashes.SHA1(), |
|
| 99 | 143 |
length=key_size // 2, |
| 100 | 144 |
salt=vault.Vault._UUID, # noqa: SLF001 |
| 101 | 145 |
iterations=iterations, |
| ... | ... |
@@ -115,35 +159,35 @@ class VaultNativeConfigParser(abc.ABC): |
| 115 | 159 |
def _parse_contents(self) -> None: |
| 116 | 160 |
logger.info('Parsing IV, payload and signature from the file contents')
|
| 117 | 161 |
|
| 118 |
- if len(self._contents) < self.iv_size + 16 + self.mac_size: |
|
| 162 |
+ if len(self._contents) < self._iv_size + 16 + self._mac_size: |
|
| 119 | 163 |
msg = 'Invalid vault configuration file: file is truncated' |
| 120 | 164 |
raise ValueError(msg) |
| 121 | 165 |
|
| 122 | 166 |
def cut(buffer: bytes, cutpoint: int) -> tuple[bytes, bytes]: |
| 123 | 167 |
return buffer[:cutpoint], buffer[cutpoint:] |
| 124 | 168 |
|
| 125 |
- cutpos1 = len(self._contents) - self.mac_size |
|
| 126 |
- cutpos2 = self.iv_size |
|
| 169 |
+ cutpos1 = len(self._contents) - self._mac_size |
|
| 170 |
+ cutpos2 = self._iv_size |
|
| 127 | 171 |
|
| 128 |
- self.message, self.message_tag = cut(self._contents, cutpos1) |
|
| 129 |
- self.iv, self.payload = cut(self.message, cutpos2) |
|
| 172 |
+ self._message, self._message_tag = cut(self._contents, cutpos1) |
|
| 173 |
+ self._iv, self._payload = cut(self._message, cutpos2) |
|
| 130 | 174 |
|
| 131 | 175 |
logger.debug( |
| 132 | 176 |
'buffer %s = [[%s, %s], %s]', |
| 133 | 177 |
_h(self._contents), |
| 134 |
- _h(self.iv), |
|
| 135 |
- _h(self.payload), |
|
| 136 |
- _h(self.message_tag), |
|
| 178 |
+ _h(self._iv), |
|
| 179 |
+ _h(self._payload), |
|
| 180 |
+ _h(self._message_tag), |
|
| 137 | 181 |
) |
| 138 | 182 |
|
| 139 | 183 |
def _derive_keys(self) -> None: |
| 140 | 184 |
logger.info('Deriving an encryption and signing key')
|
| 141 | 185 |
self._generate_keys() |
| 142 | 186 |
assert ( |
| 143 |
- len(self.encryption_key) == self.encryption_key_size |
|
| 187 |
+ len(self._encryption_key) == self._encryption_key_size |
|
| 144 | 188 |
), 'Derived encryption key is invalid' |
| 145 | 189 |
assert ( |
| 146 |
- len(self.signing_key) == self.signing_key_size |
|
| 190 |
+ len(self._signing_key) == self._signing_key_size |
|
| 147 | 191 |
), 'Derived signing key is invalid' |
| 148 | 192 |
|
| 149 | 193 |
@abc.abstractmethod |
| ... | ... |
@@ -152,16 +196,16 @@ class VaultNativeConfigParser(abc.ABC): |
| 152 | 196 |
|
| 153 | 197 |
def _check_signature(self) -> None: |
| 154 | 198 |
logger.info('Checking HMAC signature')
|
| 155 |
- mac = hmac.HMAC(self.signing_key, hashes.SHA256()) |
|
| 199 |
+ mac = hmac.HMAC(self._signing_key, hashes.SHA256()) |
|
| 156 | 200 |
mac_input = self._hmac_input() |
| 157 | 201 |
logger.debug( |
| 158 | 202 |
'mac_input = %s, expected_tag = %s', |
| 159 | 203 |
_h(mac_input), |
| 160 |
- _h(self.message_tag), |
|
| 204 |
+ _h(self._message_tag), |
|
| 161 | 205 |
) |
| 162 | 206 |
mac.update(mac_input) |
| 163 | 207 |
try: |
| 164 |
- mac.verify(self.message_tag) |
|
| 208 |
+ mac.verify(self._message_tag) |
|
| 165 | 209 |
except crypt_exceptions.InvalidSignature: |
| 166 | 210 |
msg = 'File does not contain a valid signature' |
| 167 | 211 |
raise ValueError(msg) from None |
| ... | ... |
@@ -170,13 +214,13 @@ class VaultNativeConfigParser(abc.ABC): |
| 170 | 214 |
def _hmac_input(self) -> bytes: |
| 171 | 215 |
raise AssertionError |
| 172 | 216 |
|
| 173 |
- def _decrypt_payload(self) -> Any: |
|
| 217 |
+ def _decrypt_payload(self) -> Any: # noqa: ANN401 |
|
| 174 | 218 |
decryptor = self._make_decryptor() |
| 175 | 219 |
padded_plaintext = bytearray() |
| 176 |
- padded_plaintext.extend(decryptor.update(self.payload)) |
|
| 220 |
+ padded_plaintext.extend(decryptor.update(self._payload)) |
|
| 177 | 221 |
padded_plaintext.extend(decryptor.finalize()) |
| 178 | 222 |
logger.debug('padded plaintext = %s', _h(padded_plaintext))
|
| 179 |
- unpadder = padding.PKCS7(self.iv_size * 8).unpadder() |
|
| 223 |
+ unpadder = padding.PKCS7(self._iv_size * 8).unpadder() |
|
| 180 | 224 |
plaintext = bytearray() |
| 181 | 225 |
plaintext.extend(unpadder.update(padded_plaintext)) |
| 182 | 226 |
plaintext.extend(unpadder.finalize()) |
| ... | ... |
@@ -189,40 +233,52 @@ class VaultNativeConfigParser(abc.ABC): |
| 189 | 233 |
|
| 190 | 234 |
|
| 191 | 235 |
class VaultNativeV03ConfigParser(VaultNativeConfigParser): |
| 236 |
+ """A parser for vault's native configuration format (v0.3). |
|
| 237 |
+ |
|
| 238 |
+ This is the modern, pre-storeroom configuration format. |
|
| 239 |
+ |
|
| 240 |
+ """ |
|
| 241 |
+ |
|
| 192 | 242 |
KEY_SIZE = 32 |
| 193 | 243 |
|
| 194 |
- def __init__(self, *args: Any, **kwargs: Any) -> None: |
|
| 244 |
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401,D107 |
|
| 195 | 245 |
super().__init__(*args, **kwargs) |
| 196 |
- self.iv_size = 16 |
|
| 197 |
- self.mac_size = 32 |
|
| 246 |
+ self._iv_size = 16 |
|
| 247 |
+ self._mac_size = 32 |
|
| 198 | 248 |
|
| 199 |
- def __call__(self) -> Any: |
|
| 249 |
+ def __call__(self) -> Any: # noqa: ANN401,D102 |
|
| 200 | 250 |
if self._data is self._sentinel: |
| 201 | 251 |
logger.info('Attempting to parse as v0.3 configuration')
|
| 202 | 252 |
return super().__call__() |
| 203 | 253 |
return self._data |
| 204 | 254 |
|
| 205 | 255 |
def _generate_keys(self) -> None: |
| 206 |
- self.encryption_key = self.pbkdf2(self._password, self.KEY_SIZE, 100) |
|
| 207 |
- self.signing_key = self.pbkdf2(self._password, self.KEY_SIZE, 200) |
|
| 208 |
- self.encryption_key_size = self.signing_key_size = self.KEY_SIZE |
|
| 256 |
+ self._encryption_key = self._pbkdf2(self._password, self.KEY_SIZE, 100) |
|
| 257 |
+ self._signing_key = self._pbkdf2(self._password, self.KEY_SIZE, 200) |
|
| 258 |
+ self._encryption_key_size = self._signing_key_size = self.KEY_SIZE |
|
| 209 | 259 |
|
| 210 | 260 |
def _hmac_input(self) -> bytes: |
| 211 |
- return self.message.hex().lower().encode('ASCII')
|
|
| 261 |
+ return self._message.hex().lower().encode('ASCII')
|
|
| 212 | 262 |
|
| 213 | 263 |
def _make_decryptor(self) -> ciphers.CipherContext: |
| 214 | 264 |
return ciphers.Cipher( |
| 215 |
- algorithms.AES256(self.encryption_key), modes.CBC(self.iv) |
|
| 265 |
+ algorithms.AES256(self._encryption_key), modes.CBC(self._iv) |
|
| 216 | 266 |
).decryptor() |
| 217 | 267 |
|
| 218 | 268 |
|
| 219 | 269 |
class VaultNativeV02ConfigParser(VaultNativeConfigParser): |
| 220 |
- def __init__(self, *args: Any, **kwargs: Any) -> None: |
|
| 270 |
+ """A parser for vault's native configuration format (v0.3). |
|
| 271 |
+ |
|
| 272 |
+ This is the modern, pre-storeroom configuration format. |
|
| 273 |
+ |
|
| 274 |
+ """ |
|
| 275 |
+ |
|
| 276 |
+ def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401,D107 |
|
| 221 | 277 |
super().__init__(*args, **kwargs) |
| 222 |
- self.iv_size = 16 |
|
| 223 |
- self.mac_size = 64 |
|
| 278 |
+ self._iv_size = 16 |
|
| 279 |
+ self._mac_size = 64 |
|
| 224 | 280 |
|
| 225 |
- def __call__(self) -> Any: |
|
| 281 |
+ def __call__(self) -> Any: # noqa: ANN401,D102 |
|
| 226 | 282 |
if self._data is self._sentinel: |
| 227 | 283 |
logger.info('Attempting to parse as v0.2 configuration')
|
| 228 | 284 |
return super().__call__() |
| ... | ... |
@@ -231,17 +287,17 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser): |
| 231 | 287 |
def _parse_contents(self) -> None: |
| 232 | 288 |
super()._parse_contents() |
| 233 | 289 |
logger.debug('Decoding payload (base64) and message tag (hex)')
|
| 234 |
- self.payload = base64.standard_b64decode(self.payload) |
|
| 235 |
- self.message_tag = bytes.fromhex(self.message_tag.decode('ASCII'))
|
|
| 290 |
+ self._payload = base64.standard_b64decode(self._payload) |
|
| 291 |
+ self._message_tag = bytes.fromhex(self._message_tag.decode('ASCII'))
|
|
| 236 | 292 |
|
| 237 | 293 |
def _generate_keys(self) -> None: |
| 238 |
- self.encryption_key = self.pbkdf2(self._password, 8, 16) |
|
| 239 |
- self.signing_key = self.pbkdf2(self._password, 16, 16) |
|
| 240 |
- self.encryption_key_size = 8 |
|
| 241 |
- self.signing_key_size = 16 |
|
| 294 |
+ self._encryption_key = self._pbkdf2(self._password, 8, 16) |
|
| 295 |
+ self._signing_key = self._pbkdf2(self._password, 16, 16) |
|
| 296 |
+ self._encryption_key_size = 8 |
|
| 297 |
+ self._signing_key_size = 16 |
|
| 242 | 298 |
|
| 243 | 299 |
def _hmac_input(self) -> bytes: |
| 244 |
- return base64.standard_b64encode(self.message) |
|
| 300 |
+ return base64.standard_b64encode(self._message) |
|
| 245 | 301 |
|
| 246 | 302 |
def _make_decryptor(self) -> ciphers.CipherContext: |
| 247 | 303 |
def evp_bytestokey_md5_one_iteration_no_salt( |
| ... | ... |
@@ -268,7 +324,7 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser): |
| 268 | 324 |
warnings.simplefilter( |
| 269 | 325 |
'ignore', crypt_utils.CryptographyDeprecationWarning |
| 270 | 326 |
) |
| 271 |
- block = hashes.Hash(hashes.MD5()) # noqa: S303 |
|
| 327 |
+ block = hashes.Hash(hashes.MD5()) |
|
| 272 | 328 |
block.update(last_block) |
| 273 | 329 |
block.update(data) |
| 274 | 330 |
block.update(salt) |
| ... | ... |
@@ -284,7 +340,7 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser): |
| 284 | 340 |
) |
| 285 | 341 |
return bytes(buffer[:key_size]), bytes(buffer[key_size:total_size]) |
| 286 | 342 |
|
| 287 |
- data = base64.standard_b64encode(self.iv + self.encryption_key) |
|
| 343 |
+ data = base64.standard_b64encode(self._iv + self._encryption_key) |
|
| 288 | 344 |
encryption_key, iv = evp_bytestokey_md5_one_iteration_no_salt( |
| 289 | 345 |
data, key_size=32, iv_size=16 |
| 290 | 346 |
) |
| ... | ... |
@@ -64,7 +64,7 @@ class Sequin: |
| 64 | 64 |
/, |
| 65 | 65 |
*, |
| 66 | 66 |
is_bitstring: bool = False, |
| 67 |
- ): |
|
| 67 |
+ ) -> None: |
|
| 68 | 68 |
"""Initialize the Sequin. |
| 69 | 69 |
|
| 70 | 70 |
Args: |
| ... | ... |
@@ -176,6 +176,9 @@ class Sequin: |
| 176 | 176 |
digits: A sequence of integers to evaluate. |
| 177 | 177 |
base: The number base to evaluate those integers in. |
| 178 | 178 |
|
| 179 |
+ Returns: |
|
| 180 |
+ The number value of the integer sequence. |
|
| 181 |
+ |
|
| 179 | 182 |
Raises: |
| 180 | 183 |
ValueError: `base` is an invalid base. |
| 181 | 184 |
ValueError: Not all integers are valid base `base` digits. |
| ... | ... |
@@ -204,7 +207,7 @@ class Sequin: |
| 204 | 207 |
x = digits[i] |
| 205 | 208 |
if not isinstance(x, int): |
| 206 | 209 |
msg = f'not an integer: {x!r}'
|
| 207 |
- raise TypeError(msg) |
|
| 210 |
+ raise TypeError(msg) # noqa: DOC501 |
|
| 208 | 211 |
if x not in allowed_range: |
| 209 | 212 |
msg = f'invalid base {base!r} digit: {x!r}'
|
| 210 | 213 |
raise ValueError(msg) |
| ... | ... |
@@ -387,5 +390,5 @@ class SequinExhaustedError(Exception): |
| 387 | 390 |
|
| 388 | 391 |
""" |
| 389 | 392 |
|
| 390 |
- def __init__(self) -> None: |
|
| 393 |
+ def __init__(self) -> None: # noqa: D107 |
|
| 391 | 394 |
super().__init__('Sequin is exhausted')
|
| ... | ... |
@@ -115,7 +115,12 @@ class SSHAgentClient: |
| 115 | 115 |
self._connection.connect(ssh_auth_sock) |
| 116 | 116 |
|
| 117 | 117 |
def __enter__(self) -> Self: |
| 118 |
- """Close socket connection upon context manager completion.""" |
|
| 118 |
+ """Close socket connection upon context manager completion. |
|
| 119 |
+ |
|
| 120 |
+ Returns: |
|
| 121 |
+ Self. |
|
| 122 |
+ |
|
| 123 |
+ """ |
|
| 119 | 124 |
self._connection.__enter__() |
| 120 | 125 |
return self |
| 121 | 126 |
|
| ... | ... |
@@ -125,7 +130,18 @@ class SSHAgentClient: |
| 125 | 130 |
exc_val: BaseException | None, |
| 126 | 131 |
exc_tb: TracebackType | None, |
| 127 | 132 |
) -> bool: |
| 128 |
- """Close socket connection upon context manager completion.""" |
|
| 133 |
+ """Close socket connection upon context manager completion. |
|
| 134 |
+ |
|
| 135 |
+ Args: |
|
| 136 |
+ exc_type: An optional exception type. |
|
| 137 |
+ exc_val: An optional exception value. |
|
| 138 |
+ exc_tb: An optional exception traceback. |
|
| 139 |
+ |
|
| 140 |
+ Returns: |
|
| 141 |
+ True if the exception was handled, false if it should |
|
| 142 |
+ propagate. |
|
| 143 |
+ |
|
| 144 |
+ """ |
|
| 129 | 145 |
return bool( |
| 130 | 146 |
self._connection.__exit__(exc_type, exc_val, exc_tb) # type: ignore[func-returns-value] |
| 131 | 147 |
) |
| ... | ... |
@@ -173,7 +189,7 @@ class SSHAgentClient: |
| 173 | 189 |
ret.extend(payload) |
| 174 | 190 |
except Exception as e: |
| 175 | 191 |
msg = 'invalid payload type' |
| 176 |
- raise TypeError(msg) from e |
|
| 192 |
+ raise TypeError(msg) from e # noqa: DOC501 |
|
| 177 | 193 |
return ret |
| 178 | 194 |
|
| 179 | 195 |
@classmethod |
| ... | ... |
@@ -2,7 +2,7 @@ |
| 2 | 2 |
# |
| 3 | 3 |
# SPDX-License-Identifier: MIT |
| 4 | 4 |
|
| 5 |
-"""Python port of the vault(1) password generation scheme""" |
|
| 5 |
+"""Python port of the vault(1) password generation scheme.""" |
|
| 6 | 6 |
|
| 7 | 7 |
from __future__ import annotations |
| 8 | 8 |
|
| ... | ... |
@@ -71,7 +71,7 @@ class Vault: |
| 71 | 71 |
|
| 72 | 72 |
""" |
| 73 | 73 |
|
| 74 |
- def __init__( |
|
| 74 |
+ def __init__( # noqa: PLR0913 |
|
| 75 | 75 |
self, |
| 76 | 76 |
*, |
| 77 | 77 |
phrase: bytes | bytearray | str = b'', |
| ... | ... |
@@ -113,6 +113,11 @@ class Vault: |
| 113 | 113 |
Same as `lower`, but for all other hitherto unlisted |
| 114 | 114 |
ASCII printable characters (except backquote). |
| 115 | 115 |
|
| 116 |
+ Raises: |
|
| 117 |
+ ValueError: |
|
| 118 |
+ Conflicting passphrase constraints. Permit more |
|
| 119 |
+ characters, or increase the desired passphrase length. |
|
| 120 |
+ |
|
| 116 | 121 |
""" |
| 117 | 122 |
self._phrase = self._get_binary_string(phrase) |
| 118 | 123 |
self._length = length |
| ... | ... |
@@ -204,10 +209,10 @@ class Vault: |
| 204 | 209 |
safety_factor = float(safety_factor) |
| 205 | 210 |
except TypeError as e: |
| 206 | 211 |
msg = f'invalid safety factor: not a float: {safety_factor!r}'
|
| 207 |
- raise TypeError(msg) from e |
|
| 212 |
+ raise TypeError(msg) from e # noqa: DOC501 |
|
| 208 | 213 |
if not math.isfinite(safety_factor) or safety_factor < 1.0: |
| 209 | 214 |
msg = f'invalid safety factor {safety_factor!r}'
|
| 210 |
- raise ValueError(msg) |
|
| 215 |
+ raise ValueError(msg) # noqa: DOC501 |
|
| 211 | 216 |
# Ensure the bound is strictly positive. |
| 212 | 217 |
entropy_bound = max(1, self._entropy()) |
| 213 | 218 |
return int(math.ceil(safety_factor * entropy_bound / 8)) |
| ... | ... |
@@ -392,7 +392,7 @@ class TestCLI: |
| 392 | 392 |
key_index: int, |
| 393 | 393 |
) -> None: |
| 394 | 394 |
def sign( |
| 395 |
- _, key: bytes | bytearray, message: bytes | bytearray |
|
| 395 |
+ _: Any, key: bytes | bytearray, message: bytes | bytearray |
|
| 396 | 396 |
) -> bytes: |
| 397 | 397 |
del message # Unused. |
| 398 | 398 |
for value in tests.SUPPORTED_KEYS.values(): |
| ... | ... |
@@ -1299,7 +1299,7 @@ contents go here |
| 1299 | 1299 |
'services': {
|
| 1300 | 1300 |
DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(), |
| 1301 | 1301 |
'weird entry name': {'phrase': 'D\u00fcsseldorf'},
|
| 1302 |
- } |
|
| 1302 |
+ }, |
|
| 1303 | 1303 |
}), |
| 1304 | 1304 |
( |
| 1305 | 1305 |
"the services.'weird entry name' passphrase " |
| ... | ... |
@@ -1342,7 +1342,7 @@ class TestCLIUtils: |
| 1342 | 1342 |
), |
| 1343 | 1343 |
pytest.raises(ValueError, match='Invalid vault config'), |
| 1344 | 1344 |
): |
| 1345 |
- cli._save_config(None) # type: ignore |
|
| 1345 |
+ cli._save_config(None) # type: ignore[arg-type] |
|
| 1346 | 1346 |
|
| 1347 | 1347 |
def test_101_prompt_for_selection_multiple(self) -> None: |
| 1348 | 1348 |
@click.command() |
| ... | ... |
@@ -235,7 +235,7 @@ class TestStoreroom: |
| 235 | 235 |
'signing_key': bytes(storeroom.KEY_SIZE), |
| 236 | 236 |
'hashing_key': bytes(storeroom.KEY_SIZE), |
| 237 | 237 |
} |
| 238 |
- with pytest.raises(RuntimeError, match='Cannot handle version 255'): |
|
| 238 |
+ with pytest.raises(ValueError, match='Cannot handle version 255'): |
|
| 239 | 239 |
storeroom.decrypt_bucket_item(bucket_item, master_keys) |
| 240 | 240 |
|
| 241 | 241 |
@pytest.mark.parametrize('config', ['xxx', 'null', '{"version": 255}'])
|
| ... | ... |
@@ -259,7 +259,7 @@ class TestStoreroom: |
| 259 | 259 |
): |
| 260 | 260 |
with open('.vault/20', 'w', encoding='UTF-8') as outfile:
|
| 261 | 261 |
print(config, file=outfile) |
| 262 |
- with pytest.raises(RuntimeError, match='Invalid bucket file: '): |
|
| 262 |
+ with pytest.raises(ValueError, match='Invalid bucket file: '): |
|
| 263 | 263 |
list(storeroom.decrypt_bucket_file('.vault/20', master_keys))
|
| 264 | 264 |
|
| 265 | 265 |
@pytest.mark.parametrize( |
| ... | ... |
@@ -317,7 +317,7 @@ class TestVaultNativeConfig: |
| 317 | 317 |
) |
| 318 | 318 |
def test_200_pbkdf2_manually(self, iterations: int, result: bytes) -> None: |
| 319 | 319 |
assert ( |
| 320 |
- vault_v03_and_below.VaultNativeConfigParser.pbkdf2( |
|
| 320 |
+ vault_v03_and_below.VaultNativeConfigParser._pbkdf2( |
|
| 321 | 321 |
tests.VAULT_MASTER_KEY.encode('utf-8'), 32, iterations
|
| 322 | 322 |
) |
| 323 | 323 |
== result |
| 324 | 324 |