a9bd473172968db23d6b79c7a12ad7bdb9203d3c
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) 
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

129) def derive_master_keys_keys(
130)     password: str | Buffer,
131)     iterations: int,
132) ) -> KeyPair:
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

165)         salt=STOREROOM_MASTER_KEYS_UUID,
166)         iterations=iterations,
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 7 months ago

168)     encryption_key, signing_key = struct.unpack(
169)         f'{KEY_SIZE}s {KEY_SIZE}s', master_keys_keys_blob
170)     )
171)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 7 months ago

181)         ),
182)     )
183)     return {
184)         'encryption_key': encryption_key,
185)         'signing_key': signing_key,
186)     }
187) 
188) 
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

189) def decrypt_master_keys_data(
190)     data: Buffer,
191)     keys: KeyPair,
192) ) -> MasterKeys:
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

194) 
195)     The master keys data contains:
196) 
197)     - a 16-byte IV,
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

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

212) 
213)     Args:
214)         data:
215)             The encrypted master keys data.
216)         keys:
217)             The encryption and signing keys for the master keys data.
218)             These should have previously been derived via the
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

220) 
221)     Returns:
222)         The master encryption, signing and hashing keys.
223) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

233)     Warning:
234)         Non-public function, provided for didactical and educational
235)         purposes only.  Subject to change without notice, including
236)         removal.
237) 
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 2 months ago

239)     data = memoryview(data).toreadonly().cast('c')
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

240)     ciphertext, claimed_mac = struct.unpack(
241)         f'{len(data) - MAC_SIZE}s {MAC_SIZE}s', data
242)     )
243)     actual_mac = hmac.HMAC(keys['signing_key'], hashes.SHA256())
244)     actual_mac.update(ciphertext)
245)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

246)         _msg.TranslatedString(
247)             _msg.DebugMsgTemplate.MASTER_KEYS_DATA_MAC_INFO,
248)             sign_key=_h(keys['signing_key']),
249)             ciphertext=_h(ciphertext),
250)             claimed_mac=_h(claimed_mac),
251)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

252)         ),
253)     )
254)     actual_mac.verify(claimed_mac)
255) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

276)     return {
277)         'hashing_key': hashing_key,
278)         'encryption_key': encryption_key,
279)         'signing_key': signing_key,
280)     }
281) 
282) 
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

283) def decrypt_session_keys(
284)     data: Buffer,
285)     master_keys: MasterKeys,
286) ) -> KeyPair:
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

288) 
289)     The bucket item's session keys are single-use keys for encrypting
290)     and signing a single item in the storage bucket.  The encrypted
291)     session key data consists of:
292) 
293)     - a 16-byte IV,
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

297) 
298)     The encrypted payload is encrypted with the master encryption key,
299)     and the MAC is created with the master signing key.  As per standard
300)     cryptographic procedure, the MAC can be verified before attempting
301)     to decrypt the payload.
302) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

305) 
306)     Args:
307)         data:
308)             The encrypted bucket item session key data.
309)         master_keys:
310)             The master keys.  Presumably these have previously been
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

312) 
313)     Returns:
314)         The bucket item's encryption and signing keys.
315) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

316)     Raises:
317)         cryptography.exceptions.InvalidSignature:
318)             The data does not contain a valid signature under the given
319)             key.
320)         ValueError:
321)             The format is invalid, in a non-cryptographic way.  (For
322)             example, it contains an unsupported version marker, or
323)             unexpected extra contents, or invalid padding.)
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

324) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

325)     Warning:
326)         Non-public function, provided for didactical and educational
327)         purposes only.  Subject to change without notice, including
328)         removal.
329) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

331)     data = memoryview(data).toreadonly().cast('c')
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

336)     actual_mac.update(ciphertext)
337)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

338)         _msg.TranslatedString(
339)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_MAC_INFO,
340)             sign_key=_h(master_keys['signing_key']),
341)             ciphertext=_h(ciphertext),
342)             claimed_mac=_h(claimed_mac),
343)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

344)         ),
345)     )
346)     actual_mac.verify(claimed_mac)
347) 
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

348)     try:
349)         iv, payload = struct.unpack(
350)             f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
351)         )
352)         decryptor = ciphers.Cipher(
353)             algorithms.AES256(master_keys['encryption_key']), modes.CBC(iv)
354)         ).decryptor()
355)         padded_plaintext = bytearray()
356)         padded_plaintext.extend(decryptor.update(payload))
357)         padded_plaintext.extend(decryptor.finalize())
358)         unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
359)         plaintext = bytearray()
360)         plaintext.extend(unpadder.update(padded_plaintext))
361)         plaintext.extend(unpadder.finalize())
362)         session_encryption_key, session_signing_key = struct.unpack(
363)             f'{KEY_SIZE}s {KEY_SIZE}s', plaintext
364)         )
365)     except (ValueError, struct.error) as exc:
366)         msg = 'Invalid encrypted session keys payload'
367)         raise ValueError(msg) from exc
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

368) 
369)     session_keys: KeyPair = {
370)         'encryption_key': session_encryption_key,
371)         'signing_key': session_signing_key,
372)     }
373) 
374)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

375)         _msg.TranslatedString(
376)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_SESSION_KEYS_INFO,
377)             enc_key=_h(master_keys['encryption_key']),
378)             iv=_h(iv),
379)             ciphertext=_h(payload),
380)             plaintext=_h(plaintext),
381)             code=_msg.TranslatedString(
382)                 '{{"encryption_key": bytes.fromhex({enc_key!r}), '
383)                 '"signing_key": bytes.fromhex({sign_key!r})}}',
384)                 enc_key=session_keys['encryption_key'].hex(' '),
385)                 sign_key=session_keys['signing_key'].hex(' '),
386)             ),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

387)         ),
388)     )
389) 
390)     return session_keys
391) 
392) 
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

393) def decrypt_contents(
394)     data: Buffer,
395)     session_keys: KeyPair,
396) ) -> Buffer:
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

397)     """Decrypt the bucket item's contents.
398) 
399)     The data consists of:
400) 
401)     - a 16-byte IV,
402)     - a variable-sized AES256-CBC-encrypted payload (using PKCS7 padding
403)       on the inside), and
Marco Ricci Add remaining tests to the...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

405) 
406)     The encrypted payload is encrypted with the bucket item's session
407)     encryption key, and the MAC is created with the bucket item's
408)     session signing key.  As per standard cryptographic procedure, the
409)     MAC can be verified before attempting to decrypt the payload.
410) 
411)     Args:
412)         data:
413)             The encrypted bucket item payload data.
414)         session_keys:
415)             The bucket item's session keys.  Presumably these have
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

417)             function.
418) 
419)     Returns:
420)         The bucket item's payload.
421) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

422)     Raises:
423)         cryptography.exceptions.InvalidSignature:
424)             The data does not contain a valid signature under the given
425)             key.
426)         ValueError:
427)             The format is invalid, in a non-cryptographic way.  (For
428)             example, it contains an unsupported version marker, or
429)             unexpected extra contents, or invalid padding.)
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

430) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

431)     Warning:
432)         Non-public function, provided for didactical and educational
433)         purposes only.  Subject to change without notice, including
434)         removal.
435) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

437)     data = memoryview(data).toreadonly().cast('c')
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

442)     actual_mac.update(ciphertext)
443)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

444)         _msg.TranslatedString(
445)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_MAC_INFO,
446)             sign_key=_h(session_keys['signing_key']),
447)             ciphertext=_h(ciphertext),
448)             claimed_mac=_h(claimed_mac),
449)             actual_mac=_h(actual_mac.copy().finalize()),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

450)         ),
451)     )
452)     actual_mac.verify(claimed_mac)
453) 
454)     iv, payload = struct.unpack(
455)         f'{IV_SIZE}s {len(ciphertext) - IV_SIZE}s', ciphertext
456)     )
457)     decryptor = ciphers.Cipher(
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

459)     ).decryptor()
460)     padded_plaintext = bytearray()
461)     padded_plaintext.extend(decryptor.update(payload))
462)     padded_plaintext.extend(decryptor.finalize())
463)     unpadder = padding.PKCS7(IV_SIZE * 8).unpadder()
464)     plaintext = bytearray()
465)     plaintext.extend(unpadder.update(padded_plaintext))
466)     plaintext.extend(unpadder.finalize())
467) 
468)     logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

469)         _msg.TranslatedString(
470)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_INFO,
471)             enc_key=_h(session_keys['encryption_key']),
472)             iv=_h(iv),
473)             ciphertext=_h(payload),
474)             plaintext=_h(plaintext),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

475)         ),
476)     )
477) 
478)     return plaintext
479) 
480) 
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

481) def decrypt_bucket_item(
482)     bucket_item: Buffer,
483)     master_keys: MasterKeys,
484) ) -> Buffer:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

485)     """Decrypt a bucket item.
486) 
487)     Args:
488)         bucket_item:
489)             The encrypted bucket item.
490)         master_keys:
491)             The master keys.  Presumably these have previously been
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

493) 
494)     Returns:
495)         The decrypted bucket item.
496) 
497)     Raises:
498)         cryptography.exceptions.InvalidSignature:
499)             The data does not contain a valid signature under the given
500)             key.
501)         ValueError:
502)             The format is invalid, in a non-cryptographic way.  (For
503)             example, it contains an unsupported version marker, or
504)             unexpected extra contents, or invalid padding.)
505) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

506)     Warning:
507)         Non-public function, provided for didactical and educational
508)         purposes only.  Subject to change without notice, including
509)         removal.
510) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

512)     bucket_item = memoryview(bucket_item).toreadonly().cast('c')
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 2 months ago

514)         _msg.TranslatedString(
515)             _msg.DebugMsgTemplate.DECRYPT_BUCKET_ITEM_KEY_INFO,
516)             plaintext=_h(bucket_item),
517)             enc_key=_h(master_keys['encryption_key']),
518)             sign_key=_h(master_keys['signing_key']),
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

519)         ),
520)     )
521)     data_version, encrypted_session_keys, data_contents = struct.unpack(
522)         (
523)             f'B {ENCRYPTED_KEYPAIR_SIZE}s '
Marco Ricci Add docstrings and better v...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

531)     session_keys = decrypt_session_keys(encrypted_session_keys, master_keys)
532)     return decrypt_contents(data_contents, session_keys)
533) 
534) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

536)     filename: str,
537)     master_keys: MasterKeys,
538)     *,
539)     root_dir: str | bytes | os.PathLike = '.',
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 6 months ago

542) 
543)     Args:
544)         filename:
545)             The bucket file's filename.
546)         master_keys:
547)             The master keys.  Presumably these have previously been
Marco Ricci Generate nicer documentatio...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

549)         root_dir:
550)             The root directory of the data store.  The filename is
551)             interpreted relatively to this directory.
552) 
553)     Yields:
Marco Ricci Convert old syntax for Yiel...

Marco Ricci authored 5 months ago

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

Marco Ricci authored 6 months ago

555) 
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.)
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 Support exports from outsid...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

574)         header_line = bucket_file.readline()
575)         try:
576)             header = json.loads(header_line)
577)         except ValueError as exc:
578)             msg = f'Invalid bucket file: {filename}'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

586)             )
587) 
588) 
Marco Ricci Add vault_native exporter f...

Marco Ricci authored 6 months ago

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

590)     """Store the JSON contents at path in the config structure.
591) 
592)     Traverse the config structure according to path, and set the value
593)     of the leaf to the decoded JSON contents.
594) 
595)     A path `/foo/bar/xyz` translates to the JSON structure
596)     `{"foo": {"bar": {"xyz": ...}}}`.
597) 
598)     Args:
599)         config:
600)             The (top-level) configuration structure to update.
601)         path:
602)             The path within the configuration structure to traverse.
603)         json_contents:
604)             The contents to set the item to, after JSON-decoding.
605) 
606)     Raises:
607)         json.JSONDecodeError:
608)             There was an error parsing the JSON contents.
609) 
610)     """
611)     contents = json.loads(json_contents)
612)     path_parts = [part for part in path.split('/') if part]
613)     for part in path_parts[:-1]:
614)         config = config.setdefault(part, {})
615)     if path_parts:
616)         config[path_parts[-1]] = contents
617) 
618) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

620)     storeroom_path: str | bytes | os.PathLike | None = None,
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

621)     master_keys_key: str | Buffer | None = None,
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

622) ) -> dict[str, Any]:
623)     """Export the full configuration stored in the storeroom.
624) 
625)     Args:
626)         storeroom_path:
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

634) 
635)     Returns:
636)         The full configuration, as stored in the storeroom.
637) 
638)         This may or may not be a valid configuration according to vault
639)         or derivepassphrase.
640) 
641)     Raises:
642)         RuntimeError:
643)             Something went wrong during data collection, e.g. we
644)             encountered unsupported or corrupted data in the storeroom.
645)         json.JSONDecodeError:
646)             An internal JSON data structure failed to parse from disk.
647)             The storeroom is probably corrupted.
648) 
649)     """
Marco Ricci Move vault key and path det...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

653)         master_keys_key = exporter.get_vault_key()
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

654)     elif not isinstance(master_keys_key, str):
655)         master_keys_key = memoryview(master_keys_key).toreadonly().cast('c')
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

659)         header = json.loads(master_keys_file.readline())
660)         if header != {'version': 1}:
661)             msg = 'bad or unsupported keys version header'
662)             raise RuntimeError(msg)
663)         raw_keys_data = base64.standard_b64decode(master_keys_file.readline())
664)         encrypted_keys_params, encrypted_keys = struct.unpack(
665)             f'B {len(raw_keys_data) - 1}s', raw_keys_data
666)         )
667)         if master_keys_file.read():
668)             msg = 'trailing data; cannot make sense of .keys file'
669)             raise RuntimeError(msg)
670)     encrypted_keys_version = encrypted_keys_params >> 4
671)     if encrypted_keys_version != 1:
672)         msg = f'cannot handle version {encrypted_keys_version} encrypted keys'
673)         raise RuntimeError(msg)
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

674)     logger.info(
675)         _msg.TranslatedString(_msg.InfoMsgTemplate.PARSING_MASTER_KEYS_DATA)
676)     )
Marco Ricci Add prototype for "storeroo...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

680)     )
681)     master_keys = decrypt_master_keys_data(encrypted_keys, master_keys_keys)
682) 
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

685)     # Use glob.glob(..., root_dir=...) here once Python 3.9 becomes
686)     # unsupported.
687)     storeroom_path_str = os.fsdecode(storeroom_path)
688)     valid_hashdirs = [
689)         hashdir_name
690)         for hashdir_name in os.listdir(storeroom_path_str)
691)         if fnmatch.fnmatch(hashdir_name, '[01][0-9a-f]')
692)     ]
693)     for file in valid_hashdirs:
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

694)         logger.info(
695)             _msg.TranslatedString(
696)                 _msg.InfoMsgTemplate.DECRYPTING_BUCKET,
697)                 bucket_number=file,
698)             )
699)         )
Marco Ricci Accept all bytes-like objec...

Marco Ricci authored 2 months ago

700)         bucket_contents = [
701)             bytes(item)
702)             for item in decrypt_bucket_file(file, master_keys, root_dir=storeroom_path)
703)         ]
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

704)         bucket_index = json.loads(bucket_contents.pop(0))
705)         for pos, item in enumerate(bucket_index):
706)             json_contents[item] = bucket_contents[pos]
707)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

708)                 _msg.TranslatedString(
709)                     _msg.DebugMsgTemplate.BUCKET_ITEM_FOUND,
710)                     path=item,
711)                     value=bucket_contents[pos],
712)                 )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

713)             )
714)     dirs_to_check: dict[str, list[str]] = {}
715)     json_payload: Any
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

716)     logger.info(
717)         _msg.TranslatedString(_msg.InfoMsgTemplate.ASSEMBLING_CONFIG_STRUCTURE)
718)     )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 2 months ago

722)                 _msg.TranslatedString(
723)                     _msg.DebugMsgTemplate.POSTPONING_DIRECTORY_CONTENTS_CHECK,
724)                     path=path,
725)                     contents=json_content.decode('utf-8'),
726)                 )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

727)             )
728)             json_payload = json.loads(json_content)
729)             if not isinstance(json_payload, list) or any(
730)                 not isinstance(x, str) for x in json_payload
731)             ):
732)                 msg = (
733)                     f'Directory index is not actually an index: '
734)                     f'{json_content!r}'
735)                 )
736)                 raise RuntimeError(msg)
737)             dirs_to_check[path] = json_payload
738)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

739)                 _msg.TranslatedString(
740)                     _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS_EMPTY_DIRECTORY,
741)                     path=path,
742)                 ),
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 7 months ago

745)         else:
746)             logger.debug(
Marco Ricci Make debug and info message...

Marco Ricci authored 2 months ago

747)                 _msg.TranslatedString(
748)                     _msg.DebugMsgTemplate.SETTING_CONFIG_STRUCTURE_CONTENTS,
749)                     path=path,
750)                     value=json_content.decode('utf-8'),
751)                 ),
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 6 months ago

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

Marco Ricci authored 2 months ago

754)     logger.info(
755)         _msg.TranslatedString(
756)             _msg.InfoMsgTemplate.CHECKING_CONFIG_STRUCTURE_CONSISTENCY,
757)         )
758)     )
Marco Ricci Emit new info messages and...

Marco Ricci authored 3 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 5 months ago

762)         obj: dict[Any, Any] = config_structure
763)         for part in _dir.split('/'):
764)             if part:
765)                 # Because we iterate paths in sorted order, parent
766)                 # directories are encountered before child directories.
767)                 # So parent directories always exist (lest we would have
768)                 # aborted earlier).
769)                 #
770)                 # Of course, the type checker doesn't necessarily know
771)                 # this, so we need to use assertions anyway.
772)                 maybe_obj = obj.get(part)
Marco Ricci Update ruff to v0.8.x, refo...

Marco Ricci authored 2 months ago

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

Marco Ricci authored 5 months ago

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

Marco Ricci authored 7 months ago

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

Marco Ricci authored 2 months ago

780)         logger.debug(
781)             _msg.TranslatedString(
782)                 _msg.DebugMsgTemplate.DIRECTORY_CONTENTS_CHECK_OK,
783)                 path=_dir,
784)                 contents=json.dumps(namelist),
785)             )
786)         )
Marco Ricci Add an actual storeroom exp...

Marco Ricci authored 7 months ago

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

Marco Ricci authored 7 months ago

788) 
789) 
790) if __name__ == '__main__':