fe7b6349a5c5781a4d1b1ecf16d976e43d0e7f95
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 Add docstrings and better v...

Marco Ricci authored 7 months ago

23) from __future__ import annotations
24) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

25) import base64
Marco Ricci Add support for Python 3.9

Marco Ricci authored 5 months ago

26) import fnmatch
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

27) import json
28) import logging
29) import os
30) import os.path
31) import struct
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 2 months ago

34) from derivepassphrase import _cli_msg as _msg
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

35) from derivepassphrase import exporter
36) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

37) if TYPE_CHECKING:
38)     from collections.abc import Iterator
39) 
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 6 months ago

40)     from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding
41)     from cryptography.hazmat.primitives.ciphers import algorithms, modes
42)     from cryptography.hazmat.primitives.kdf import pbkdf2
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

44) else:
45)     try:
46)         from cryptography.hazmat.primitives import (
47)             ciphers,
48)             hashes,
49)             hmac,
50)             padding,
51)         )
52)         from cryptography.hazmat.primitives.ciphers import algorithms, modes
53)         from cryptography.hazmat.primitives.kdf import pbkdf2
54)     except ModuleNotFoundError as exc:
55) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

62)                     raise self.exc
63) 
64)                 return func
65) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

68)         STUBBED = True
69)     else:
70)         STUBBED = False
71) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

79) __all__ = ('export_storeroom_data',)
80) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

82) 
83) 
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

84) def _h(bs: Buffer) -> str:
85)     return '<{}>'.format(memoryview(bs).hex(' '))
86) 
87) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

88) class KeyPair(TypedDict):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

89)     """A pair of AES256 keys, one for encryption and one for signing.
90) 
91)     Attributes:
92)         encryption_key:
93)             AES256 key, used for encryption with AES256-CBC (with PKCS#7
94)             padding).
95)         signing_key:
96)             AES256 key, used for signing with HMAC-SHA256.
97) 
98)     """
99) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

100)     encryption_key: bytes
Marco Ricci Enable cross-references on...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

102)     signing_key: bytes
Marco Ricci Enable cross-references on...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

104) 
105) 
106) class MasterKeys(TypedDict):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

107)     """A triple of AES256 keys, for encryption, signing and hashing.
108) 
109)     Attributes:
110)         hashing_key:
111)             AES256 key, used for hashing with HMAC-SHA256 to derive
112)             a hash table slot for an item.
113)         encryption_key:
114)             AES256 key, used for encryption with AES256-CBC (with PKCS#7
115)             padding).
116)         signing_key:
117)             AES256 key, used for signing with HMAC-SHA256.
118) 
119)     """
120) 
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

121)     hashing_key: bytes
Marco Ricci Enable cross-references on...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

123)     encryption_key: bytes
Marco Ricci Enable cross-references on...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

125)     signing_key: bytes
Marco Ricci Enable cross-references on...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

127) 
128) 
129) def derive_master_keys_keys(password: str | bytes, iterations: int) -> KeyPair:
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

130)     """Derive encryption and signing keys for the master keys data.
131) 
132)     The master password is run through a key derivation function to
133)     obtain a 64-byte string, which is then split to yield two 32-byte
134)     keys.  The key derivation function is PBKDF2, using HMAC-SHA1 and
135)     salted with the storeroom master keys UUID.
136) 
137)     Args:
138)         password:
139)             A master password for the storeroom instance.  Usually read
140)             from the `VAULT_KEY` environment variable, otherwise
141)             defaults to the username.
142)         iterations:
143)             A count of rounds for the underlying key derivation
144)             function.  Usually stored as a setting next to the encrypted
145)             master keys data.
146) 
147)     Returns:
148)         A 2-tuple of keys, the encryption key and the signing key, to
149)         decrypt and verify the master keys data with.
150) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

151)     Warning:
152)         Non-public function, provided for didactical and educational
153)         purposes only.  Subject to change without notice, including
154)         removal.
155) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

162)         salt=STOREROOM_MASTER_KEYS_UUID,
163)         iterations=iterations,
164)     ).derive(password)
165)     encryption_key, signing_key = struct.unpack(
166)         f'{KEY_SIZE}s {KEY_SIZE}s', master_keys_keys_blob
167)     )
168)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

169)         _msg.TranslatedString(
170)             _msg.DebugMsgTemplate.DERIVED_MASTER_KEYS_KEYS,
171)             enc_key=_h(encryption_key),
172)             sign_key=_h(signing_key),
173)             pw_bytes=_h(password),
174)             algorithm='SHA256',
175)             length=64,
176)             salt=STOREROOM_MASTER_KEYS_UUID,
177)             iterations=iterations,
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

178)         ),
179)     )
180)     return {
181)         'encryption_key': encryption_key,
182)         'signing_key': signing_key,
183)     }
184) 
185) 
186) def decrypt_master_keys_data(data: bytes, keys: KeyPair) -> MasterKeys:
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

188) 
189)     The master keys data contains:
190) 
191)     - a 16-byte IV,
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

195) 
196)     The decrypted payload itself consists of three 32-byte keys: the
197)     hashing, encryption and signing keys, in that order.
198) 
199)     The encrypted payload is encrypted with the encryption key, and the
200)     MAC is created based on the signing key.  As per standard
201)     cryptographic procedure, the MAC can be verified before attempting
202)     to decrypt the payload.
203) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

204)     Because the payload size is both fixed and a multiple of the cipher
205)     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

206) 
207)     Args:
208)         data:
209)             The encrypted master keys data.
210)         keys:
211)             The encryption and signing keys for the master keys data.
212)             These should have previously been derived via the
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

213)             [`derive_master_keys_keys`][] function.
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

214) 
215)     Returns:
216)         The master encryption, signing and hashing keys.
217) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

218)     Raises:
219)         cryptography.exceptions.InvalidSignature:
220)             The data does not contain a valid signature under the given
221)             key.
222)         ValueError:
223)             The format is invalid, in a non-cryptographic way.  (For
224)             example, it contains an unsupported version marker, or
225)             unexpected extra contents, or invalid padding.)
226) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

227)     Warning:
228)         Non-public function, provided for didactical and educational
229)         purposes only.  Subject to change without notice, including
230)         removal.
231) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

233)     ciphertext, claimed_mac = struct.unpack(
234)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
235)     )
236)     actual_mac = hmac.HMAC(keys['signing_key'], hashes.SHA256())
237)     actual_mac.update(ciphertext)
238)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

239)         _msg.TranslatedString(
240)             _msg.DebugMsgTemplate.MASTER_KEYS_DATA_MAC_INFO,
241)             sign_key=_h(keys['signing_key']),
242)             ciphertext=_h(ciphertext),
243)             claimed_mac=_h(claimed_mac),
244)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

245)         ),
246)     )
247)     actual_mac.verify(claimed_mac)
248) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

253)         decryptor = ciphers.Cipher(
254)             algorithms.AES256(keys['encryption_key']), modes.CBC(iv)
255)         ).decryptor()
256)         padded_plaintext = bytearray()
257)         padded_plaintext.extend(decryptor.update(payload))
258)         padded_plaintext.extend(decryptor.finalize())
259)         unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
260)         plaintext = bytearray()
261)         plaintext.extend(unpadder.update(padded_plaintext))
262)         plaintext.extend(unpadder.finalize())
263)         hashing_key, encryption_key, signing_key = struct.unpack(
264)             f'{KEY_SIZE}s {KEY_SIZE}s {KEY_SIZE}s', plaintext
265)         )
266)     except (ValueError, struct.error) as exc:
267)         msg = 'Invalid encrypted master keys payload'
268)         raise ValueError(msg) from exc
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

269)     return {
270)         'hashing_key': hashing_key,
271)         'encryption_key': encryption_key,
272)         'signing_key': signing_key,
273)     }
274) 
275) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

276) def decrypt_session_keys(data: bytes, master_keys: MasterKeys) -> KeyPair:
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

278) 
279)     The bucket item's session keys are single-use keys for encrypting
280)     and signing a single item in the storage bucket.  The encrypted
281)     session key data consists of:
282) 
283)     - a 16-byte IV,
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

287) 
288)     The encrypted payload is encrypted with the master encryption key,
289)     and the MAC is created with the master signing key.  As per standard
290)     cryptographic procedure, the MAC can be verified before attempting
291)     to decrypt the payload.
292) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

293)     Because the payload size is both fixed and a multiple of the cipher
294)     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

295) 
296)     Args:
297)         data:
298)             The encrypted bucket item session key data.
299)         master_keys:
300)             The master keys.  Presumably these have previously been
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

302) 
303)     Returns:
304)         The bucket item's encryption and signing keys.
305) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

306)     Raises:
307)         cryptography.exceptions.InvalidSignature:
308)             The data does not contain a valid signature under the given
309)             key.
310)         ValueError:
311)             The format is invalid, in a non-cryptographic way.  (For
312)             example, it contains an unsupported version marker, or
313)             unexpected extra contents, or invalid padding.)
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

314) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

315)     Warning:
316)         Non-public function, provided for didactical and educational
317)         purposes only.  Subject to change without notice, including
318)         removal.
319) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

321)     ciphertext, claimed_mac = struct.unpack(
322)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
323)     )
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

324)     actual_mac = hmac.HMAC(master_keys['signing_key'], hashes.SHA256())
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

325)     actual_mac.update(ciphertext)
326)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

327)         _msg.TranslatedString(
328)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO,
329)             sign_key=_h(master_keys['signing_key']),
330)             ciphertext=_h(ciphertext),
331)             claimed_mac=_h(claimed_mac),
332)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

333)         ),
334)     )
335)     actual_mac.verify(claimed_mac)
336) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

337)     try:
338)         iv, payload = struct.unpack(
339)             f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
340)         )
341)         decryptor = ciphers.Cipher(
342)             algorithms.AES256(master_keys['encryption_key']), modes.CBC(iv)
343)         ).decryptor()
344)         padded_plaintext = bytearray()
345)         padded_plaintext.extend(decryptor.update(payload))
346)         padded_plaintext.extend(decryptor.finalize())
347)         unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
348)         plaintext = bytearray()
349)         plaintext.extend(unpadder.update(padded_plaintext))
350)         plaintext.extend(unpadder.finalize())
351)         session_encryption_key, session_signing_key = struct.unpack(
352)             f'{KEY_SIZE}s {KEY_SIZE}s', plaintext
353)         )
354)     except (ValueError, struct.error) as exc:
355)         msg = 'Invalid encrypted session keys payload'
356)         raise ValueError(msg) from exc
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

357) 
358)     session_keys: KeyPair = {
359)         'encryption_key': session_encryption_key,
360)         'signing_key': session_signing_key,
361)     }
362) 
363)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

364)         _msg.TranslatedString(
365)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO,
366)             enc_key=_h(master_keys['encryption_key']),
367)             iv=_h(iv),
368)             ciphertext=_h(payload),
369)             plaintext=_h(plaintext),
370)             code=_msg.TranslatedString(
371)                 '{{"encryption_key": bytes.fromhex({enc_key!r}), '
372)                 '"signing_key": bytes.fromhex({sign_key!r})}}',
373)                 enc_key=session_keys['encryption_key'].hex(' '),
374)                 sign_key=session_keys['signing_key'].hex(' '),
375)             ),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

376)         ),
377)     )
378) 
379)     return session_keys
380) 
381) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

382) def decrypt_contents(data: bytes, session_keys: KeyPair) -> bytes:
383)     """Decrypt the bucket item's contents.
384) 
385)     The data consists of:
386) 
387)     - a 16-byte IV,
388)     - a variable-sized AES256-CBC-encrypted payload (using PKCS7 padding
389)       on the inside), and
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

391) 
392)     The encrypted payload is encrypted with the bucket item's session
393)     encryption key, and the MAC is created with the bucket item's
394)     session signing key.  As per standard cryptographic procedure, the
395)     MAC can be verified before attempting to decrypt the payload.
396) 
397)     Args:
398)         data:
399)             The encrypted bucket item payload data.
400)         session_keys:
401)             The bucket item's session keys.  Presumably these have
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

403)             function.
404) 
405)     Returns:
406)         The bucket item's payload.
407) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

408)     Raises:
409)         cryptography.exceptions.InvalidSignature:
410)             The data does not contain a valid signature under the given
411)             key.
412)         ValueError:
413)             The format is invalid, in a non-cryptographic way.  (For
414)             example, it contains an unsupported version marker, or
415)             unexpected extra contents, or invalid padding.)
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

416) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

417)     Warning:
418)         Non-public function, provided for didactical and educational
419)         purposes only.  Subject to change without notice, including
420)         removal.
421) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

423)     ciphertext, claimed_mac = struct.unpack(
424)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
425)     )
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

426)     actual_mac = hmac.HMAC(session_keys['signing_key'], hashes.SHA256())
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

427)     actual_mac.update(ciphertext)
428)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

429)         _msg.TranslatedString(
430)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_MAC_INFO,
431)             sign_key=_h(session_keys['signing_key']),
432)             ciphertext=_h(ciphertext),
433)             claimed_mac=_h(claimed_mac),
434)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

435)         ),
436)     )
437)     actual_mac.verify(claimed_mac)
438) 
439)     iv, payload = struct.unpack(
440)         f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
441)     )
442)     decryptor = ciphers.Cipher(
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

443)         algorithms.AES256(session_keys['encryption_key']), modes.CBC(iv)
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

444)     ).decryptor()
445)     padded_plaintext = bytearray()
446)     padded_plaintext.extend(decryptor.update(payload))
447)     padded_plaintext.extend(decryptor.finalize())
448)     unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
449)     plaintext = bytearray()
450)     plaintext.extend(unpadder.update(padded_plaintext))
451)     plaintext.extend(unpadder.finalize())
452) 
453)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

454)         _msg.TranslatedString(
455)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_INFO,
456)             enc_key=_h(session_keys['encryption_key']),
457)             iv=_h(iv),
458)             ciphertext=_h(payload),
459)             plaintext=_h(plaintext),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

460)         ),
461)     )
462) 
463)     return plaintext
464) 
465) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

466) def decrypt_bucket_item(bucket_item: bytes, master_keys: MasterKeys) -> bytes:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

467)     """Decrypt a bucket item.
468) 
469)     Args:
470)         bucket_item:
471)             The encrypted bucket item.
472)         master_keys:
473)             The master keys.  Presumably these have previously been
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

475) 
476)     Returns:
477)         The decrypted bucket item.
478) 
479)     Raises:
480)         cryptography.exceptions.InvalidSignature:
481)             The data does not contain a valid signature under the given
482)             key.
483)         ValueError:
484)             The format is invalid, in a non-cryptographic way.  (For
485)             example, it contains an unsupported version marker, or
486)             unexpected extra contents, or invalid padding.)
487) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

488)     Warning:
489)         Non-public function, provided for didactical and educational
490)         purposes only.  Subject to change without notice, including
491)         removal.
492) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 2 months ago

495)         _msg.TranslatedString(
496)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_KEY_INFO,
497)             plaintext=_h(bucket_item),
498)             enc_key=_h(master_keys['encryption_key']),
499)             sign_key=_h(master_keys['signing_key']),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

500)         ),
501)     )
502)     data_version, encrypted_session_keys, data_contents = struct.unpack(
503)         (
504)             f'B {ENCRYPTED_KEYPAIR_SIZE}s '
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

511)         raise ValueError(msg)
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

512)     session_keys = decrypt_session_keys(encrypted_session_keys, master_keys)
513)     return decrypt_contents(data_contents, session_keys)
514) 
515) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

516) def decrypt_bucket_file(
Marco Ricci Support exports from outsid...

Marco Ricci authored 7 months ago

517)     filename: str,
518)     master_keys: MasterKeys,
519)     *,
520)     root_dir: str | bytes | os.PathLike = '.',
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

521) ) -> Iterator[bytes]:
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

523) 
524)     Args:
525)         filename:
526)             The bucket file's filename.
527)         master_keys:
528)             The master keys.  Presumably these have previously been
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

530)         root_dir:
531)             The root directory of the data store.  The filename is
532)             interpreted relatively to this directory.
533) 
534)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

536) 
537)     Raises:
538)         cryptography.exceptions.InvalidSignature:
539)             The data does not contain a valid signature under the given
540)             key.
541)         ValueError:
542)             The format is invalid, in a non-cryptographic way.  (For
543)             example, it contains an unsupported version marker, or
544)             unexpected extra contents, or invalid padding.)
545) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

546)     Warning:
547)         Non-public function, provided for didactical and educational
548)         purposes only.  Subject to change without notice, including
549)         removal.
550) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

551)     """
Marco Ricci Support exports from outsid...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

555)         header_line = bucket_file.readline()
556)         try:
557)             header = json.loads(header_line)
558)         except ValueError as exc:
559)             msg = f'Invalid bucket file: {filename}'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

564)         for line in bucket_file:
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

565)             yield decrypt_bucket_item(
566)                 base64.standard_b64decode(line), master_keys
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

567)             )
568) 
569) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

570) 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

571)     """Store the JSON contents at path in the config structure.
572) 
573)     Traverse the config structure according to path, and set the value
574)     of the leaf to the decoded JSON contents.
575) 
576)     A path `/foo/bar/xyz` translates to the JSON structure
577)     `{"foo": {"bar": {"xyz": ...}}}`.
578) 
579)     Args:
580)         config:
581)             The (top-level) configuration structure to update.
582)         path:
583)             The path within the configuration structure to traverse.
584)         json_contents:
585)             The contents to set the item to, after JSON-decoding.
586) 
587)     Raises:
588)         json.JSONDecodeError:
589)             There was an error parsing the JSON contents.
590) 
591)     """
592)     contents = json.loads(json_contents)
593)     path_parts = [part for part in path.split('/') if part]
594)     for part in path_parts[:-1]:
595)         config = config.setdefault(part, {})
596)     if path_parts:
597)         config[path_parts[-1]] = contents
598) 
599) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

600) def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

601)     storeroom_path: str | bytes | os.PathLike | None = None,
602)     master_keys_key: str | bytes | None = None,
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

603) ) -> dict[str, Any]:
604)     """Export the full configuration stored in the storeroom.
605) 
606)     Args:
607)         storeroom_path:
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

608)             Path to the storeroom; usually `~/.vault`.  If not given,
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

609)             then query [`exporter.get_vault_path`][] for the value.
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

610)         master_keys_key:
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

611)             Encryption key/password for the master keys, usually the
612)             username, or passed via the `VAULT_KEY` environment
613)             variable.  If not given, then query
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

614)             [`exporter.get_vault_key`][] for the value.
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

615) 
616)     Returns:
617)         The full configuration, as stored in the storeroom.
618) 
619)         This may or may not be a valid configuration according to vault
620)         or derivepassphrase.
621) 
622)     Raises:
623)         RuntimeError:
624)             Something went wrong during data collection, e.g. we
625)             encountered unsupported or corrupted data in the storeroom.
626)         json.JSONDecodeError:
627)             An internal JSON data structure failed to parse from disk.
628)             The storeroom is probably corrupted.
629) 
630)     """
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

631)     if storeroom_path is None:
632)         storeroom_path = exporter.get_vault_path()
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

633)     if master_keys_key is None:
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

634)         master_keys_key = exporter.get_vault_key()
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

635)     with open(
636)         os.path.join(os.fsdecode(storeroom_path), '.keys'), encoding='utf-8'
637)     ) as master_keys_file:
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

638)         header = json.loads(master_keys_file.readline())
639)         if header != {'version': 1}:
640)             msg = 'bad or unsupported keys version header'
641)             raise RuntimeError(msg)
642)         raw_keys_data = base64.standard_b64decode(master_keys_file.readline())
643)         encrypted_keys_params, encrypted_keys = struct.unpack(
644)             f'B {len(raw_keys_data) - 1}s', raw_keys_data
645)         )
646)         if master_keys_file.read():
647)             msg = 'trailing data; cannot make sense of .keys file'
648)             raise RuntimeError(msg)
649)     encrypted_keys_version = encrypted_keys_params >> 4
650)     if encrypted_keys_version != 1:
651)         msg = f'cannot handle version {encrypted_keys_version} encrypted keys'
652)         raise RuntimeError(msg)
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

653)     logger.info(
654)         _msg.TranslatedString(_msg.InfoMsgTemplate.PARSING_MASTER_KEYS_DATA)
655)     )
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

656)     encrypted_keys_iterations = 2 ** (10 + (encrypted_keys_params & 0x0F))
657)     master_keys_keys = derive_master_keys_keys(
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

658)         master_keys_key, encrypted_keys_iterations
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

659)     )
660)     master_keys = decrypt_master_keys_data(encrypted_keys, master_keys_keys)
661) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

662)     config_structure: dict[str, Any] = {}
663)     json_contents: dict[str, bytes] = {}
Marco Ricci Add support for Python 3.9

Marco Ricci authored 5 months ago

664)     # Use glob.glob(..., root_dir=...) here once Python 3.9 becomes
665)     # unsupported.
666)     storeroom_path_str = os.fsdecode(storeroom_path)
667)     valid_hashdirs = [
668)         hashdir_name
669)         for hashdir_name in os.listdir(storeroom_path_str)
670)         if fnmatch.fnmatch(hashdir_name, '[01][0-9a-f]')
671)     ]
672)     for file in valid_hashdirs:
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

673)         logger.info(
674)             _msg.TranslatedString(
675)                 _msg.InfoMsgTemplate.DECRYPTING_BUCKET,
676)                 bucket_number=file,
677)             )
678)         )
Marco Ricci Support exports from outsid...

Marco Ricci authored 7 months ago

679)         bucket_contents = list(
680)             decrypt_bucket_file(file, master_keys, root_dir=storeroom_path)
681)         )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

682)         bucket_index = json.loads(bucket_contents.pop(0))
683)         for pos, item in enumerate(bucket_index):
684)             json_contents[item] = bucket_contents[pos]
685)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

686)                 _msg.TranslatedString(
687)                     _msg.DebugMsgTemplate.BUCKET_ITEM_FOUND,
688)                     path=item,
689)                     value=bucket_contents[pos],
690)                 )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

691)             )
692)     dirs_to_check: dict[str, list[str]] = {}
693)     json_payload: Any
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

694)     logger.info(
695)         _msg.TranslatedString(_msg.InfoMsgTemplate.ASSEMBLING_CONFIG_STRUCTURE)
696)     )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

697)     for path, json_content in sorted(json_contents.items()):
698)         if path.endswith('/'):
699)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

700)                 _msg.TranslatedString(
701)                     _msg.DebugMsgTemplate.POSTPONING_DIRECTORY_CONTENTS_CHECK,
702)                     path=path,
703)                     contents=json_content.decode('utf-8'),
704)                 )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

705)             )
706)             json_payload = json.loads(json_content)
707)             if not isinstance(json_payload, list) or any(
708)                 not isinstance(x, str) for x in json_payload
709)             ):
710)                 msg = (
711)                     f'Directory index is not actually an index: '
712)                     f'{json_content!r}'
713)                 )
714)                 raise RuntimeError(msg)
715)             dirs_to_check[path] = json_payload
716)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

717)                 _msg.TranslatedString(
718)                     _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY,
719)                     path=path,
720)                 ),
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

721)             )
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

722)             _store(config_structure, path, b'{}')
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

723)         else:
724)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

725)                 _msg.TranslatedString(
726)                     _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS,
727)                     path=path,
728)                     value=json_content.decode('utf-8'),
729)                 ),
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

730)             )
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

731)             _store(config_structure, path, json_content)
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

732)     logger.info(
733)         _msg.TranslatedString(
734)             _msg.InfoMsgTemplate.CHECKING_CONFIG_STRUCTURE_CONSISTENCY,
735)         )
736)     )
Marco Ricci Emit new info messages and...

Marco Ricci authored 3 months ago

737)     # Sorted order is important; see `maybe_obj` below.
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

738)     for _dir, namelist in sorted(dirs_to_check.items()):
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

739)         namelist = [x.rstrip('/') for x in namelist]  # noqa: PLW2901
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

740)         obj: dict[Any, Any] = config_structure
741)         for part in _dir.split('/'):
742)             if part:
743)                 # Because we iterate paths in sorted order, parent
744)                 # directories are encountered before child directories.
745)                 # So parent directories always exist (lest we would have
746)                 # aborted earlier).
747)                 #
748)                 # Of course, the type checker doesn't necessarily know
749)                 # this, so we need to use assertions anyway.
750)                 maybe_obj = obj.get(part)
Marco Ricci Update ruff to v0.8.x, refo...

Marco Ricci authored 2 months ago

751)                 assert isinstance(maybe_obj, dict), (
752)                     f'Cannot traverse storage path {_dir!r}'
753)                 )
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

754)                 obj = maybe_obj
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

755)         if set(obj.keys()) != set(namelist):
756)             msg = f'Object key mismatch for path {_dir!r}'
757)             raise RuntimeError(msg)
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

758)         logger.debug(
759)             _msg.TranslatedString(
760)                 _msg.DebugMsgTemplate.DIRECTORY_CONTENTS_CHECK_OK,
761)                 path=_dir,
762)                 contents=json.dumps(namelist),
763)             )
764)         )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

765)     return config_structure
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

766) 
767) 
768) if __name__ == '__main__':