d58a3abf705eba597a358c323416c325aa649a02
Marco Ricci Update copyright notices to...

Marco Ricci authored 2 months ago

1) # SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

2) #
Marco Ricci Update copyright notices to...

Marco Ricci authored 2 months ago

3) # SPDX-License-Identifier: Zlib
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

4) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

5) """Exporter for the vault "storeroom" configuration format.
6) 
7) The "storeroom" format is the experimental format used in alpha and beta
8) versions of vault beyond v0.3.0.  The configuration is stored as
9) a separate directory, which acts like a hash table (i.e. has named
10) slots) and provides an impure quasi-filesystem interface.  Each hash
11) table entry is separately encrypted and authenticated.  James Coglan
12) designed this format to avoid concurrent write issues when updating or
13) synchronizing the vault configuration with e.g. a cloud service.
14) 
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

15) The public interface is the [`export_storeroom_data`][] function.
16) Multiple *non-public* functions are additionally documented here for
17) didactical and educational reasons, but they are not part of the module
18) API, are subject to change without notice (including removal), and
19) should *not* be used or relied on.
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

20) 
21) """
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

22) 
Marco Ricci Harmonize the interface for...

Marco Ricci authored 2 months ago

23) # ruff: noqa: S303
24) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

25) from __future__ import annotations
26) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

27) import base64
Marco Ricci Add support for Python 3.9

Marco Ricci authored 5 months ago

28) import fnmatch
Marco Ricci Harmonize the interface for...

Marco Ricci authored 2 months ago

29) import importlib
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

30) import json
31) import logging
32) import os
33) import os.path
34) import struct
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

35) from typing import TYPE_CHECKING, Any
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

36) 
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

37) from derivepassphrase import _cli_msg as _msg
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

38) from derivepassphrase import _types, exporter
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

39) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

40) if TYPE_CHECKING:
41)     from collections.abc import Iterator
42) 
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

43)     from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding
44)     from cryptography.hazmat.primitives.ciphers import algorithms, modes
45)     from cryptography.hazmat.primitives.kdf import pbkdf2
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

46)     from typing_extensions import Buffer
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

47) else:
48)     try:
Marco Ricci Harmonize the interface for...

Marco Ricci authored 2 months ago

49)         importlib.import_module('cryptography')
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

50)     except ModuleNotFoundError as exc:
51) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

52)         class _DummyModule:  # pragma: no cover
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

53)             def __init__(self, exc: type[Exception]) -> None:
54)                 self.exc = exc
55) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

56)             def __getattr__(self, name: str) -> Any:  # noqa: ANN401
57)                 def func(*args: Any, **kwargs: Any) -> Any:  # noqa: ANN401,ARG001
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

58)                     raise self.exc
59) 
60)                 return func
61) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

62)         ciphers = hashes = hmac = padding = _DummyModule(exc)
63)         algorithms = modes = pbkdf2 = _DummyModule(exc)
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

64)         STUBBED = True
65)     else:
Marco Ricci Harmonize the interface for...

Marco Ricci authored 2 months ago

66)         from cryptography.hazmat.primitives import (
67)             ciphers,
68)             hashes,
69)             hmac,
70)             padding,
71)         )
72)         from cryptography.hazmat.primitives.ciphers import algorithms, modes
73)         from cryptography.hazmat.primitives.kdf import pbkdf2
74) 
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

75)         STUBBED = False
76) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

77) STOREROOM_MASTER_KEYS_UUID = b'35b7c7ed-f71e-4adf-9051-02fb0f1e0e17'
78) VAULT_CIPHER_UUID = b'73e69e8a-cb05-4b50-9f42-59d76a511299'
79) IV_SIZE = 16
80) KEY_SIZE = MAC_SIZE = 32
81) ENCRYPTED_KEYPAIR_SIZE = 128
82) VERSION_SIZE = 1
83) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

84) __all__ = ('export_storeroom_data',)
85) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

86) logger = logging.getLogger(__name__)
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

87) 
88) 
Marco Ricci Move vault config exporter...

Marco Ricci authored 1 month ago

89) @exporter.register_export_vault_config_data_handler('storeroom')
90) def export_storeroom_data(  # noqa: C901,D417,PLR0912,PLR0914,PLR0915
91)     path: str | bytes | os.PathLike | None = None,
92)     key: str | Buffer | None = None,
93)     *,
94)     format: str = 'storeroom',  # noqa: A002
95) ) -> dict[str, Any]:
96)     """Export the full configuration stored in the storeroom.
97) 
98)     See [`exporter.ExportVaultConfigDataFunction`][] for an explanation
99)     of the call signature, and the exceptions to expect.
100) 
101)     Other Args:
102)         format:
103)             The only supported format is `storeroom`.
104) 
105)     """  # noqa: DOC201,DOC501
106)     # Trigger import errors if necessary.
107)     importlib.import_module('cryptography')
108)     if path is None:
109)         path = exporter.get_vault_path()
110)     if key is None:
111)         key = exporter.get_vault_key()
112)     if format != 'storeroom':  # pragma: no cover
113)         msg = exporter.INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format(
114)             fmt=format
115)         )
116)         raise ValueError(msg)
117)     try:
118)         master_keys_file = open(  # noqa: SIM115
119)             os.path.join(os.fsdecode(path), '.keys'),
120)             encoding='utf-8',
121)         )
122)     except FileNotFoundError as exc:
123)         raise exporter.NotAVaultConfigError(
124)             os.fsdecode(path),
125)             format='storeroom',
126)         ) from exc
127)     with master_keys_file:
128)         header = json.loads(master_keys_file.readline())
129)         if header != {'version': 1}:
130)             msg = 'bad or unsupported keys version header'
131)             raise RuntimeError(msg)
132)         raw_keys_data = base64.standard_b64decode(master_keys_file.readline())
133)         encrypted_keys_params, encrypted_keys = struct.unpack(
134)             f'B {len(raw_keys_data) - 1}s', raw_keys_data
135)         )
136)         if master_keys_file.read():
137)             msg = 'trailing data; cannot make sense of .keys file'
138)             raise RuntimeError(msg)
139)     encrypted_keys_version = encrypted_keys_params >> 4
140)     if encrypted_keys_version != 1:
141)         msg = f'cannot handle version {encrypted_keys_version} encrypted keys'
142)         raise RuntimeError(msg)
143)     logger.info(
144)         _msg.TranslatedString(_msg.InfoMsgTemplate.PARSING_MASTER_KEYS_DATA)
145)     )
146)     encrypted_keys_iterations = 2 ** (10 + (encrypted_keys_params & 0x0F))
147)     master_keys_keys = _derive_master_keys_keys(key, encrypted_keys_iterations)
148)     master_keys = _decrypt_master_keys_data(encrypted_keys, master_keys_keys)
149) 
150)     config_structure: dict[str, Any] = {}
151)     json_contents: dict[str, bytes] = {}
152)     # Use glob.glob(..., root_dir=...) here once Python 3.9 becomes
153)     # unsupported.
154)     storeroom_path_str = os.fsdecode(path)
155)     valid_hashdirs = [
156)         hashdir_name
157)         for hashdir_name in os.listdir(storeroom_path_str)
158)         if fnmatch.fnmatch(hashdir_name, '[01][0-9a-f]')
159)     ]
160)     for file in valid_hashdirs:
161)         logger.info(
162)             _msg.TranslatedString(
163)                 _msg.InfoMsgTemplate.DECRYPTING_BUCKET,
164)                 bucket_number=file,
165)             )
166)         )
167)         bucket_contents = [
168)             bytes(item)
169)             for item in _decrypt_bucket_file(file, master_keys, root_dir=path)
170)         ]
171)         bucket_index = json.loads(bucket_contents.pop(0))
172)         for pos, item in enumerate(bucket_index):
173)             json_contents[item] = bucket_contents[pos]
174)             logger.debug(
175)                 _msg.TranslatedString(
176)                     _msg.DebugMsgTemplate.BUCKET_ITEM_FOUND,
177)                     path=item,
178)                     value=bucket_contents[pos],
179)                 )
180)             )
181)     dirs_to_check: dict[str, list[str]] = {}
182)     json_payload: Any
183)     logger.info(
184)         _msg.TranslatedString(_msg.InfoMsgTemplate.ASSEMBLING_CONFIG_STRUCTURE)
185)     )
186)     for item_path, json_content in sorted(json_contents.items()):
187)         if item_path.endswith('/'):
188)             logger.debug(
189)                 _msg.TranslatedString(
190)                     _msg.DebugMsgTemplate.POSTPONING_DIRECTORY_CONTENTS_CHECK,
191)                     path=item_path,
192)                     contents=json_content.decode('utf-8'),
193)                 )
194)             )
195)             json_payload = json.loads(json_content)
196)             if not isinstance(json_payload, list) or any(
197)                 not isinstance(x, str) for x in json_payload
198)             ):
199)                 msg = (
200)                     f'Directory index is not actually an index: '
201)                     f'{json_content!r}'
202)                 )
203)                 raise RuntimeError(msg)
204)             dirs_to_check[item_path] = json_payload
205)             logger.debug(
206)                 _msg.TranslatedString(
207)                     _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY,
208)                     path=item_path,
209)                 ),
210)             )
211)             _store(config_structure, item_path, b'{}')
212)         else:
213)             logger.debug(
214)                 _msg.TranslatedString(
215)                     _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS,
216)                     path=item_path,
217)                     value=json_content.decode('utf-8'),
218)                 ),
219)             )
220)             _store(config_structure, item_path, json_content)
221)     logger.info(
222)         _msg.TranslatedString(
223)             _msg.InfoMsgTemplate.CHECKING_CONFIG_STRUCTURE_CONSISTENCY,
224)         )
225)     )
226)     # Sorted order is important; see `maybe_obj` below.
227)     for dir_, namelist_ in sorted(dirs_to_check.items()):
228)         namelist = [x.rstrip('/') for x in namelist_]
229)         obj: dict[Any, Any] = config_structure
230)         for part in dir_.split('/'):
231)             if part:
232)                 # Because we iterate paths in sorted order, parent
233)                 # directories are encountered before child directories.
234)                 # So parent directories always exist (lest we would have
235)                 # aborted earlier).
236)                 #
237)                 # Of course, the type checker doesn't necessarily know
238)                 # this, so we need to use assertions anyway.
239)                 maybe_obj = obj.get(part)
240)                 assert isinstance(maybe_obj, dict), (
241)                     f'Cannot traverse storage path {dir_!r}'
242)                 )
243)                 obj = maybe_obj
244)         if set(obj.keys()) != set(namelist):
245)             msg = f'Object key mismatch for path {dir_!r}'
246)             raise RuntimeError(msg)
247)         logger.debug(
248)             _msg.TranslatedString(
249)                 _msg.DebugMsgTemplate.DIRECTORY_CONTENTS_CHECK_OK,
250)                 path=dir_,
251)                 contents=json.dumps(namelist_),
252)             )
253)         )
254)     return config_structure
255) 
256) 
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

257) def _h(bs: Buffer) -> str:
258)     return '<{}>'.format(memoryview(bs).hex(' '))
259) 
260) 
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

261) def _derive_master_keys_keys(
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

262)     password: str | Buffer,
263)     iterations: int,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

264) ) -> _types.StoreroomKeyPair:
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

265)     """Derive encryption and signing keys for the master keys data.
266) 
267)     The master password is run through a key derivation function to
268)     obtain a 64-byte string, which is then split to yield two 32-byte
269)     keys.  The key derivation function is PBKDF2, using HMAC-SHA1 and
270)     salted with the storeroom master keys UUID.
271) 
272)     Args:
273)         password:
274)             A master password for the storeroom instance.  Usually read
275)             from the `VAULT_KEY` environment variable, otherwise
276)             defaults to the username.
277)         iterations:
278)             A count of rounds for the underlying key derivation
279)             function.  Usually stored as a setting next to the encrypted
280)             master keys data.
281) 
282)     Returns:
283)         A 2-tuple of keys, the encryption key and the signing key, to
284)         decrypt and verify the master keys data with.
285) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

286)     Warning:
287)         Non-public function, provided for didactical and educational
288)         purposes only.  Subject to change without notice, including
289)         removal.
290) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

291)     """
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

292)     if isinstance(password, str):
293)         password = password.encode('ASCII')
294)     master_keys_keys_blob = pbkdf2.PBKDF2HMAC(
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

295)         algorithm=hashes.SHA1(),
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

296)         length=2 * KEY_SIZE,
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

297)         salt=STOREROOM_MASTER_KEYS_UUID,
298)         iterations=iterations,
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

299)     ).derive(bytes(password))
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

300)     encryption_key, signing_key = struct.unpack(
301)         f'{KEY_SIZE}s {KEY_SIZE}s', master_keys_keys_blob
302)     )
303)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

304)         _msg.TranslatedString(
305)             _msg.DebugMsgTemplate.DERIVED_MASTER_KEYS_KEYS,
306)             enc_key=_h(encryption_key),
307)             sign_key=_h(signing_key),
308)             pw_bytes=_h(password),
309)             algorithm='SHA256',
310)             length=64,
311)             salt=STOREROOM_MASTER_KEYS_UUID,
312)             iterations=iterations,
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

313)         ),
314)     )
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

315)     return _types.StoreroomKeyPair(
316)         encryption_key=encryption_key,
317)         signing_key=signing_key,
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

318)     ).toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

319) 
320) 
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

321) def _decrypt_master_keys_data(
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

322)     data: Buffer,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

323)     keys: _types.StoreroomKeyPair,
324) ) -> _types.StoreroomMasterKeys:
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

325)     r"""Decrypt the master keys data.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

326) 
327)     The master keys data contains:
328) 
329)     - a 16-byte IV,
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

330)     - a 96-byte AES256-CBC-encrypted payload, plus 16 further bytes of
331)       PKCS7 padding, and
332)     - a 32-byte MAC of the preceding 128 bytes.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

333) 
334)     The decrypted payload itself consists of three 32-byte keys: the
335)     hashing, encryption and signing keys, in that order.
336) 
337)     The encrypted payload is encrypted with the encryption key, and the
338)     MAC is created based on the signing key.  As per standard
339)     cryptographic procedure, the MAC can be verified before attempting
340)     to decrypt the payload.
341) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

342)     Because the payload size is both fixed and a multiple of the cipher
343)     blocksize, in this case, the PKCS7 padding always is `b'\x10' * 16`.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

344) 
345)     Args:
346)         data:
347)             The encrypted master keys data.
348)         keys:
349)             The encryption and signing keys for the master keys data.
350)             These should have previously been derived via the
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

351)             [`_derive_master_keys_keys`][] function.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

352) 
353)     Returns:
354)         The master encryption, signing and hashing keys.
355) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

356)     Raises:
357)         cryptography.exceptions.InvalidSignature:
358)             The data does not contain a valid signature under the given
359)             key.
360)         ValueError:
361)             The format is invalid, in a non-cryptographic way.  (For
362)             example, it contains an unsupported version marker, or
363)             unexpected extra contents, or invalid padding.)
364) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

365)     Warning:
366)         Non-public function, provided for didactical and educational
367)         purposes only.  Subject to change without notice, including
368)         removal.
369) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

370)     """
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

371)     data = memoryview(data).toreadonly().cast('c')
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

372)     keys = keys.toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

373)     ciphertext, claimed_mac = struct.unpack(
374)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
375)     )
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

376)     actual_mac = hmac.HMAC(keys.signing_key, hashes.SHA256())
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

377)     actual_mac.update(ciphertext)
378)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

379)         _msg.TranslatedString(
380)             _msg.DebugMsgTemplate.MASTER_KEYS_DATA_MAC_INFO,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

381)             sign_key=_h(keys.signing_key),
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

382)             ciphertext=_h(ciphertext),
383)             claimed_mac=_h(claimed_mac),
384)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

385)         ),
386)     )
387)     actual_mac.verify(claimed_mac)
388) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

389)     try:
390)         iv, payload = struct.unpack(
391)             f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

392)         )
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

393)         decryptor = ciphers.Cipher(
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

394)             algorithms.AES256(keys.encryption_key), modes.CBC(iv)
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

395)         ).decryptor()
396)         padded_plaintext = bytearray()
397)         padded_plaintext.extend(decryptor.update(payload))
398)         padded_plaintext.extend(decryptor.finalize())
399)         unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
400)         plaintext = bytearray()
401)         plaintext.extend(unpadder.update(padded_plaintext))
402)         plaintext.extend(unpadder.finalize())
403)         hashing_key, encryption_key, signing_key = struct.unpack(
404)             f'{KEY_SIZE}s {KEY_SIZE}s {KEY_SIZE}s', plaintext
405)         )
406)     except (ValueError, struct.error) as exc:
407)         msg = 'Invalid encrypted master keys payload'
408)         raise ValueError(msg) from exc
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

409)     return _types.StoreroomMasterKeys(
410)         hashing_key=hashing_key,
411)         encryption_key=encryption_key,
412)         signing_key=signing_key,
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

413)     ).toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

414) 
415) 
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

416) def _decrypt_session_keys(
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

417)     data: Buffer,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

418)     master_keys: _types.StoreroomMasterKeys,
419) ) -> _types.StoreroomKeyPair:
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

420)     r"""Decrypt the bucket item's session keys.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

421) 
422)     The bucket item's session keys are single-use keys for encrypting
423)     and signing a single item in the storage bucket.  The encrypted
424)     session key data consists of:
425) 
426)     - a 16-byte IV,
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

427)     - a 64-byte AES256-CBC-encrypted payload, plus 16 further bytes of
428)       PKCS7 padding, and
429)     - a 32-byte MAC of the preceding 96 bytes.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

430) 
431)     The encrypted payload is encrypted with the master encryption key,
432)     and the MAC is created with the master signing key.  As per standard
433)     cryptographic procedure, the MAC can be verified before attempting
434)     to decrypt the payload.
435) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

436)     Because the payload size is both fixed and a multiple of the cipher
437)     blocksize, in this case, the PKCS7 padding always is `b'\x10' * 16`.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

438) 
439)     Args:
440)         data:
441)             The encrypted bucket item session key data.
442)         master_keys:
443)             The master keys.  Presumably these have previously been
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

444)             obtained via the [`_decrypt_master_keys_data`][] function.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

445) 
446)     Returns:
447)         The bucket item's encryption and signing keys.
448) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

449)     Raises:
450)         cryptography.exceptions.InvalidSignature:
451)             The data does not contain a valid signature under the given
452)             key.
453)         ValueError:
454)             The format is invalid, in a non-cryptographic way.  (For
455)             example, it contains an unsupported version marker, or
456)             unexpected extra contents, or invalid padding.)
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

457) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

458)     Warning:
459)         Non-public function, provided for didactical and educational
460)         purposes only.  Subject to change without notice, including
461)         removal.
462) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

463)     """
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

464)     data = memoryview(data).toreadonly().cast('c')
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

465)     master_keys = master_keys.toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

466)     ciphertext, claimed_mac = struct.unpack(
467)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
468)     )
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

469)     actual_mac = hmac.HMAC(master_keys.signing_key, hashes.SHA256())
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

470)     actual_mac.update(ciphertext)
471)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

472)         _msg.TranslatedString(
473)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

474)             sign_key=_h(master_keys.signing_key),
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

475)             ciphertext=_h(ciphertext),
476)             claimed_mac=_h(claimed_mac),
477)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

478)         ),
479)     )
480)     actual_mac.verify(claimed_mac)
481) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

482)     try:
483)         iv, payload = struct.unpack(
484)             f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
485)         )
486)         decryptor = ciphers.Cipher(
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

487)             algorithms.AES256(master_keys.encryption_key), modes.CBC(iv)
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

488)         ).decryptor()
489)         padded_plaintext = bytearray()
490)         padded_plaintext.extend(decryptor.update(payload))
491)         padded_plaintext.extend(decryptor.finalize())
492)         unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
493)         plaintext = bytearray()
494)         plaintext.extend(unpadder.update(padded_plaintext))
495)         plaintext.extend(unpadder.finalize())
496)         session_encryption_key, session_signing_key = struct.unpack(
497)             f'{KEY_SIZE}s {KEY_SIZE}s', plaintext
498)         )
499)     except (ValueError, struct.error) as exc:
500)         msg = 'Invalid encrypted session keys payload'
501)         raise ValueError(msg) from exc
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

502) 
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

503)     session_keys = _types.StoreroomKeyPair(
504)         encryption_key=session_encryption_key,
505)         signing_key=session_signing_key,
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

506)     ).toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

507) 
508)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

509)         _msg.TranslatedString(
510)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

511)             enc_key=_h(master_keys.encryption_key),
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

512)             iv=_h(iv),
513)             ciphertext=_h(payload),
514)             plaintext=_h(plaintext),
515)             code=_msg.TranslatedString(
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

516)                 'StoreroomKeyPair(encryption_key=bytes.fromhex({enc_key!r}), '
517)                 'signing_key=bytes.fromhex({sign_key!r}))',
518)                 enc_key=session_keys.encryption_key.hex(' '),
519)                 sign_key=session_keys.signing_key.hex(' '),
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

520)             ),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

521)         ),
522)     )
523) 
524)     return session_keys
525) 
526) 
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

527) def _decrypt_contents(
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

528)     data: Buffer,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

529)     session_keys: _types.StoreroomKeyPair,
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

530) ) -> Buffer:
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

531)     """Decrypt the bucket item's contents.
532) 
533)     The data consists of:
534) 
535)     - a 16-byte IV,
536)     - a variable-sized AES256-CBC-encrypted payload (using PKCS7 padding
537)       on the inside), and
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

538)     - a 32-byte MAC of the preceding bytes.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

539) 
540)     The encrypted payload is encrypted with the bucket item's session
541)     encryption key, and the MAC is created with the bucket item's
542)     session signing key.  As per standard cryptographic procedure, the
543)     MAC can be verified before attempting to decrypt the payload.
544) 
545)     Args:
546)         data:
547)             The encrypted bucket item payload data.
548)         session_keys:
549)             The bucket item's session keys.  Presumably these have
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

550)             previously been obtained via the [`_decrypt_session_keys`][]
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

551)             function.
552) 
553)     Returns:
554)         The bucket item's payload.
555) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

556)     Raises:
557)         cryptography.exceptions.InvalidSignature:
558)             The data does not contain a valid signature under the given
559)             key.
560)         ValueError:
561)             The format is invalid, in a non-cryptographic way.  (For
562)             example, it contains an unsupported version marker, or
563)             unexpected extra contents, or invalid padding.)
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

564) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

565)     Warning:
566)         Non-public function, provided for didactical and educational
567)         purposes only.  Subject to change without notice, including
568)         removal.
569) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

570)     """
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

571)     data = memoryview(data).toreadonly().cast('c')
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

572)     session_keys = session_keys.toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

573)     ciphertext, claimed_mac = struct.unpack(
574)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
575)     )
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

576)     actual_mac = hmac.HMAC(session_keys.signing_key, hashes.SHA256())
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

577)     actual_mac.update(ciphertext)
578)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

579)         _msg.TranslatedString(
580)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_MAC_INFO,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

581)             sign_key=_h(session_keys.signing_key),
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

582)             ciphertext=_h(ciphertext),
583)             claimed_mac=_h(claimed_mac),
584)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

585)         ),
586)     )
587)     actual_mac.verify(claimed_mac)
588) 
589)     iv, payload = struct.unpack(
590)         f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
591)     )
592)     decryptor = ciphers.Cipher(
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

593)         algorithms.AES256(session_keys.encryption_key), modes.CBC(iv)
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

594)     ).decryptor()
595)     padded_plaintext = bytearray()
596)     padded_plaintext.extend(decryptor.update(payload))
597)     padded_plaintext.extend(decryptor.finalize())
598)     unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
599)     plaintext = bytearray()
600)     plaintext.extend(unpadder.update(padded_plaintext))
601)     plaintext.extend(unpadder.finalize())
602) 
603)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

604)         _msg.TranslatedString(
605)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_INFO,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

606)             enc_key=_h(session_keys.encryption_key),
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

607)             iv=_h(iv),
608)             ciphertext=_h(payload),
609)             plaintext=_h(plaintext),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

610)         ),
611)     )
612) 
613)     return plaintext
614) 
615) 
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

616) def _decrypt_bucket_item(
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

617)     bucket_item: Buffer,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

618)     master_keys: _types.StoreroomMasterKeys,
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

619) ) -> Buffer:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

620)     """Decrypt a bucket item.
621) 
622)     Args:
623)         bucket_item:
624)             The encrypted bucket item.
625)         master_keys:
626)             The master keys.  Presumably these have previously been
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

627)             obtained via the [`_decrypt_master_keys_data`][] function.
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

628) 
629)     Returns:
630)         The decrypted bucket item.
631) 
632)     Raises:
633)         cryptography.exceptions.InvalidSignature:
634)             The data does not contain a valid signature under the given
635)             key.
636)         ValueError:
637)             The format is invalid, in a non-cryptographic way.  (For
638)             example, it contains an unsupported version marker, or
639)             unexpected extra contents, or invalid padding.)
640) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

641)     Warning:
642)         Non-public function, provided for didactical and educational
643)         purposes only.  Subject to change without notice, including
644)         removal.
645) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

646)     """
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

647)     bucket_item = memoryview(bucket_item).toreadonly().cast('c')
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

648)     master_keys = master_keys.toreadonly()
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

649)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

650)         _msg.TranslatedString(
651)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_KEY_INFO,
652)             plaintext=_h(bucket_item),
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

653)             enc_key=_h(master_keys.encryption_key),
654)             sign_key=_h(master_keys.signing_key),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

655)         ),
656)     )
657)     data_version, encrypted_session_keys, data_contents = struct.unpack(
658)         (
659)             f'B {ENCRYPTED_KEYPAIR_SIZE}s '
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

660)             f'{len(bucket_item) - 1 - ENCRYPTED_KEYPAIR_SIZE}s'
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

661)         ),
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

662)         bucket_item,
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

663)     )
664)     if data_version != 1:
665)         msg = f'Cannot handle version {data_version} encrypted data'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

666)         raise ValueError(msg)
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

667)     session_keys = _decrypt_session_keys(encrypted_session_keys, master_keys)
668)     return _decrypt_contents(data_contents, session_keys)
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

669) 
670) 
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

671) def _decrypt_bucket_file(
Marco Ricci Support exports from outsid...

Marco Ricci authored 7 months ago

672)     filename: str,
Marco Ricci Move storeroom helper types...

Marco Ricci authored 2 months ago

673)     master_keys: _types.StoreroomMasterKeys,
Marco Ricci Support exports from outsid...

Marco Ricci authored 7 months ago

674)     *,
675)     root_dir: str | bytes | os.PathLike = '.',
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

676) ) -> Iterator[Buffer]:
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

677)     """Decrypt a complete bucket.
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

678) 
679)     Args:
680)         filename:
681)             The bucket file's filename.
682)         master_keys:
683)             The master keys.  Presumably these have previously been
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

684)             obtained via the [`_decrypt_master_keys_data`][] function.
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

685)         root_dir:
686)             The root directory of the data store.  The filename is
687)             interpreted relatively to this directory.
688) 
689)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 5 months ago

690)         A decrypted bucket item.
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

691) 
692)     Raises:
693)         cryptography.exceptions.InvalidSignature:
694)             The data does not contain a valid signature under the given
695)             key.
696)         ValueError:
697)             The format is invalid, in a non-cryptographic way.  (For
698)             example, it contains an unsupported version marker, or
699)             unexpected extra contents, or invalid padding.)
700) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

701)     Warning:
702)         Non-public function, provided for didactical and educational
703)         purposes only.  Subject to change without notice, including
704)         removal.
705) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

706)     """
Marco Ricci Make key pairs, key sets an...

Marco Ricci authored 2 months ago

707)     master_keys = master_keys.toreadonly()
Marco Ricci Support exports from outsid...

Marco Ricci authored 7 months ago

708)     with open(
709)         os.path.join(os.fsdecode(root_dir), filename), 'rb'
710)     ) as bucket_file:
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

711)         header_line = bucket_file.readline()
712)         try:
713)             header = json.loads(header_line)
714)         except ValueError as exc:
715)             msg = f'Invalid bucket file: {filename}'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

716)             raise ValueError(msg) from exc
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

717)         if header != {'version': 1}:
718)             msg = f'Invalid bucket file: {filename}'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

719)             raise ValueError(msg) from None
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

720)         for line in bucket_file:
Marco Ricci Make the "storeroom" module...

Marco Ricci authored 1 month ago

721)             yield _decrypt_bucket_item(
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

722)                 base64.standard_b64decode(line), master_keys
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

723)             )
724) 
725) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

726) def _store(config: dict[str, Any], path: str, json_contents: bytes) -> None:
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

727)     """Store the JSON contents at path in the config structure.
728) 
729)     Traverse the config structure according to path, and set the value
730)     of the leaf to the decoded JSON contents.
731) 
732)     A path `/foo/bar/xyz` translates to the JSON structure
733)     `{"foo": {"bar": {"xyz": ...}}}`.
734) 
735)     Args:
736)         config:
737)             The (top-level) configuration structure to update.
738)         path:
739)             The path within the configuration structure to traverse.
740)         json_contents:
741)             The contents to set the item to, after JSON-decoding.
742) 
743)     Raises:
744)         json.JSONDecodeError:
745)             There was an error parsing the JSON contents.
746) 
747)     """
748)     contents = json.loads(json_contents)
749)     path_parts = [part for part in path.split('/') if part]
750)     for part in path_parts[:-1]:
751)         config = config.setdefault(part, {})
752)     if path_parts:
753)         config[path_parts[-1]] = contents
754) 
755) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

756) if __name__ == '__main__':
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

757)     logging.basicConfig(level=('DEBUG' if os.getenv('DEBUG') else 'WARNING'))
Marco Ricci Harmonize the interface for...

Marco Ricci authored 2 months ago

758)     config_structure = export_storeroom_data(format='storeroom')