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 |