2d292af3e81527750e46a2167d30efe840ac58ca
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
5) """Exporter for the vault native configuration format (v0.2 or v0.3)."""
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

6) 
7) from __future__ import annotations
8) 
9) import abc
10) import base64
11) import json
12) import logging
13) import warnings
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

14) from typing import TYPE_CHECKING
15) 
16) from derivepassphrase import exporter, vault
17) 
18) if TYPE_CHECKING:
19)     from typing import Any
20) 
21)     from typing_extensions import Buffer
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 3 weeks ago

22) 
23) if TYPE_CHECKING:
24)     from cryptography import exceptions as crypt_exceptions
25)     from cryptography import utils as crypt_utils
26)     from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding
27)     from cryptography.hazmat.primitives.ciphers import algorithms, modes
28)     from cryptography.hazmat.primitives.kdf import pbkdf2
29) else:
30)     try:
31)         from cryptography import exceptions as crypt_exceptions
32)         from cryptography import utils as crypt_utils
33)         from cryptography.hazmat.primitives import (
34)             ciphers,
35)             hashes,
36)             hmac,
37)             padding,
38)         )
39)         from cryptography.hazmat.primitives.ciphers import algorithms, modes
40)         from cryptography.hazmat.primitives.kdf import pbkdf2
41)     except ModuleNotFoundError as exc:
42) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 3 weeks ago

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

Marco Ricci authored 2 weeks ago

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

Marco Ricci authored 3 weeks ago

49)                     raise self.exc
50) 
51)                 return func
52) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

53)         crypt_exceptions = crypt_utils = _DummyModule(exc)
54)         ciphers = hashes = hmac = padding = _DummyModule(exc)
55)         algorithms = modes = pbkdf2 = _DummyModule(exc)
Marco Ricci Add preliminary tests for t...

Marco Ricci authored 3 weeks ago

56)         STUBBED = True
57)     else:
58)         STUBBED = False
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

59) 
60) logger = logging.getLogger(__name__)
61) 
62) 
63) def _h(bs: bytes | bytearray) -> str:
64)     return 'bytes.fromhex({!r})'.format(bs.hex(' '))
65) 
66) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

67) class VaultNativeConfigParser(abc.ABC):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

68)     """A base parser for vault's native configuration format.
69) 
70)     Certain details are specific to the respective vault versions, and
71)     are abstracted out.  This class by itself is not instantiable
72)     because of this.
73) 
74)     """
75) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

76)     def __init__(self, contents: Buffer, password: str | Buffer) -> None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

77)         """Initialize the parser.
78) 
79)         Args:
80)             contents:
81)                 The binary contents of the encrypted configuration file.
82) 
83)                 Note: On disk, these are usually stored in
84)                 base64-encoded form, not in the "raw" form as needed
85)                 here.
86) 
87)             password:
88)                 The vault master key/master passphrase the file is
89)                 encrypted with.  Must be non-empty.  See
90)                 [`derivepassphrase.exporter.get_vault_key`][] for
91)                 details.
92) 
93)                 If this is a text string, then the UTF-8 encoding of the
94)                 string is used as the binary password.
95) 
96)         """
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

97)         if not password:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

98)             msg = 'Password must not be empty'
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

99)             raise ValueError(msg)  # noqa: DOC501
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

100)         self._contents = bytes(contents)
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

101)         self._iv_size = 0
102)         self._mac_size = 0
103)         self._encryption_key = b''
104)         self._encryption_key_size = 0
105)         self._signing_key = b''
106)         self._signing_key_size = 0
107)         self._message = b''
108)         self._message_tag = b''
109)         self._iv = b''
110)         self._payload = b''
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

111)         self._password = password
112)         self._sentinel: object = object()
113)         self._data: Any = self._sentinel
114) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

115)     def __call__(self) -> Any:  # noqa: ANN401
116)         """Return the decrypted and parsed vault configuration.
117) 
118)         Raises:
119)             cryptography.exceptions.InvalidSignature:
120)                 The encrypted configuration does not contain a valid
121)                 signature.
122)             ValueError:
123)                 The format is invalid, in a non-cryptographic way.  (For
124)                 example, it contains an unsupported version marker, or
125)                 unexpected extra contents, or invalid padding.)
126) 
127)         """
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

128)         if self._data is self._sentinel:
129)             self._parse_contents()
130)             self._derive_keys()
131)             self._check_signature()
132)             self._data = self._decrypt_payload()
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

133)         return self._data
134) 
135)     @staticmethod
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

136)     def _pbkdf2(
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

137)         password: str | Buffer, key_size: int, iterations: int
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

138)     ) -> bytes:
139)         if isinstance(password, str):
140)             password = password.encode('utf-8')
141)         raw_key = pbkdf2.PBKDF2HMAC(
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

142)             algorithm=hashes.SHA1(),
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

143)             length=key_size // 2,
144)             salt=vault.Vault._UUID,  # noqa: SLF001
145)             iterations=iterations,
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

146)         ).derive(bytes(password))
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

147)         logger.debug(
148)             'binary = pbkdf2(%s, %s, %s, %s, %s) = %s -> %s',
149)             repr(password),
150)             repr(vault.Vault._UUID),  # noqa: SLF001
151)             iterations,
152)             key_size // 2,
153)             repr('sha1'),
154)             _h(raw_key),
155)             _h(raw_key.hex().lower().encode('ASCII')),
156)         )
157)         return raw_key.hex().lower().encode('ASCII')
158) 
159)     def _parse_contents(self) -> None:
160)         logger.info('Parsing IV, payload and signature from the file contents')
161) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

162)         if len(self._contents) < self._iv_size + 16 + self._mac_size:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

163)             msg = 'Invalid vault configuration file: file is truncated'
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

164)             raise ValueError(msg)
165) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

166)         def cut(buffer: bytes, cutpoint: int) -> tuple[bytes, bytes]:
167)             return buffer[:cutpoint], buffer[cutpoint:]
168) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

169)         cutpos1 = len(self._contents) - self._mac_size
170)         cutpos2 = self._iv_size
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

171) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

172)         self._message, self._message_tag = cut(self._contents, cutpos1)
173)         self._iv, self._payload = cut(self._message, cutpos2)
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

174) 
175)         logger.debug(
176)             'buffer %s = [[%s, %s], %s]',
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

177)             _h(self._contents),
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

178)             _h(self._iv),
179)             _h(self._payload),
180)             _h(self._message_tag),
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

181)         )
182) 
183)     def _derive_keys(self) -> None:
184)         logger.info('Deriving an encryption and signing key')
185)         self._generate_keys()
186)         assert (
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

187)             len(self._encryption_key) == self._encryption_key_size
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

188)         ), 'Derived encryption key is invalid'
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

189)         assert (
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

190)             len(self._signing_key) == self._signing_key_size
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

191)         ), 'Derived signing key is invalid'
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

192) 
193)     @abc.abstractmethod
194)     def _generate_keys(self) -> None:
195)         raise AssertionError
196) 
197)     def _check_signature(self) -> None:
198)         logger.info('Checking HMAC signature')
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

199)         mac = hmac.HMAC(self._signing_key, hashes.SHA256())
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

200)         mac_input = self._hmac_input()
201)         logger.debug(
202)             'mac_input = %s, expected_tag = %s',
203)             _h(mac_input),
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

204)             _h(self._message_tag),
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

205)         )
206)         mac.update(mac_input)
207)         try:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

208)             mac.verify(self._message_tag)
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

209)         except crypt_exceptions.InvalidSignature:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

210)             msg = 'File does not contain a valid signature'
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

211)             raise ValueError(msg) from None
212) 
213)     @abc.abstractmethod
214)     def _hmac_input(self) -> bytes:
215)         raise AssertionError
216) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

217)     def _decrypt_payload(self) -> Any:  # noqa: ANN401
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

218)         decryptor = self._make_decryptor()
219)         padded_plaintext = bytearray()
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

220)         padded_plaintext.extend(decryptor.update(self._payload))
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

221)         padded_plaintext.extend(decryptor.finalize())
222)         logger.debug('padded plaintext = %s', _h(padded_plaintext))
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

223)         unpadder = padding.PKCS7(self._iv_size * 8).unpadder()
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

224)         plaintext = bytearray()
225)         plaintext.extend(unpadder.update(padded_plaintext))
226)         plaintext.extend(unpadder.finalize())
227)         logger.debug('plaintext = %s', _h(plaintext))
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

228)         return json.loads(plaintext)
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

229) 
230)     @abc.abstractmethod
231)     def _make_decryptor(self) -> ciphers.CipherContext:
232)         raise AssertionError
233) 
234) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

235) class VaultNativeV03ConfigParser(VaultNativeConfigParser):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

236)     """A parser for vault's native configuration format (v0.3).
237) 
238)     This is the modern, pre-storeroom configuration format.
239) 
240)     """
241) 
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

242)     KEY_SIZE = 32
243) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

244)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401,D107
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

245)         super().__init__(*args, **kwargs)
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

246)         self._iv_size = 16
247)         self._mac_size = 32
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

248) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

249)     def __call__(self) -> Any:  # noqa: ANN401,D102
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

250)         if self._data is self._sentinel:
251)             logger.info('Attempting to parse as v0.3 configuration')
252)             return super().__call__()
253)         return self._data
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

254) 
255)     def _generate_keys(self) -> None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

256)         self._encryption_key = self._pbkdf2(self._password, self.KEY_SIZE, 100)
257)         self._signing_key = self._pbkdf2(self._password, self.KEY_SIZE, 200)
258)         self._encryption_key_size = self._signing_key_size = self.KEY_SIZE
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

259) 
260)     def _hmac_input(self) -> bytes:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

261)         return self._message.hex().lower().encode('ASCII')
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

262) 
263)     def _make_decryptor(self) -> ciphers.CipherContext:
264)         return ciphers.Cipher(
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

265)             algorithms.AES256(self._encryption_key), modes.CBC(self._iv)
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

266)         ).decryptor()
267) 
268) 
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

269) class VaultNativeV02ConfigParser(VaultNativeConfigParser):
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

270)     """A parser for vault's native configuration format (v0.3).
271) 
272)     This is the modern, pre-storeroom configuration format.
273) 
274)     """
275) 
276)     def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401,D107
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

277)         super().__init__(*args, **kwargs)
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

278)         self._iv_size = 16
279)         self._mac_size = 64
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

280) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

281)     def __call__(self) -> Any:  # noqa: ANN401,D102
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

282)         if self._data is self._sentinel:
283)             logger.info('Attempting to parse as v0.2 configuration')
284)             return super().__call__()
285)         return self._data
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

286) 
287)     def _parse_contents(self) -> None:
288)         super()._parse_contents()
289)         logger.debug('Decoding payload (base64) and message tag (hex)')
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

290)         self._payload = base64.standard_b64decode(self._payload)
291)         self._message_tag = bytes.fromhex(self._message_tag.decode('ASCII'))
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

292) 
293)     def _generate_keys(self) -> None:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

294)         self._encryption_key = self._pbkdf2(self._password, 8, 16)
295)         self._signing_key = self._pbkdf2(self._password, 16, 16)
296)         self._encryption_key_size = 8
297)         self._signing_key_size = 16
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

298) 
299)     def _hmac_input(self) -> bytes:
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

300)         return base64.standard_b64encode(self._message)
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

301) 
302)     def _make_decryptor(self) -> ciphers.CipherContext:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

303)         def evp_bytestokey_md5_one_iteration_no_salt(
304)             data: bytes, key_size: int, iv_size: int
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

305)         ) -> tuple[bytes, bytes]:
306)             total_size = key_size + iv_size
307)             buffer = bytearray()
308)             last_block = b''
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

309)             salt = b''
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

310)             logging.debug(
311)                 (
312)                     'data = %s, salt = %s, key_size = %s, iv_size = %s, '
313)                     'buffer length = %s, buffer = %s'
314)                 ),
315)                 _h(data),
316)                 _h(salt),
317)                 key_size,
318)                 iv_size,
319)                 len(buffer),
320)                 _h(buffer),
321)             )
322)             while len(buffer) < total_size:
323)                 with warnings.catch_warnings():
324)                     warnings.simplefilter(
325)                         'ignore', crypt_utils.CryptographyDeprecationWarning
326)                     )
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

327)                     block = hashes.Hash(hashes.MD5())
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

328)                 block.update(last_block)
329)                 block.update(data)
330)                 block.update(salt)
331)                 last_block = block.finalize()
332)                 buffer.extend(last_block)
333)                 logging.debug(
334)                     'buffer length = %s, buffer = %s', len(buffer), _h(buffer)
335)                 )
336)             logging.debug(
337)                 'encryption_key = %s, iv = %s',
338)                 _h(buffer[:key_size]),
339)                 _h(buffer[key_size:total_size]),
340)             )
341)             return bytes(buffer[:key_size]), bytes(buffer[key_size:total_size])
342) 
Marco Ricci Apply new ruff ruleset to c...

Marco Ricci authored 2 weeks ago

343)         data = base64.standard_b64encode(self._iv + self._encryption_key)
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

344)         encryption_key, iv = evp_bytestokey_md5_one_iteration_no_salt(
345)             data, key_size=32, iv_size=16
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

346)         )
347)         return ciphers.Cipher(
348)             algorithms.AES256(encryption_key), modes.CBC(iv)
349)         ).decryptor()
350) 
351) 
352) if __name__ == '__main__':
353)     import os
354) 
355)     logging.basicConfig(level=('DEBUG' if os.getenv('DEBUG') else 'WARNING'))
Marco Ricci Move vault key and path det...

Marco Ricci authored 1 month ago

356)     with open(exporter.get_vault_path(), 'rb') as infile:
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

357)         contents = base64.standard_b64decode(infile.read())
Marco Ricci Move vault key and path det...

Marco Ricci authored 1 month ago

358)     password = exporter.get_vault_key()
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

359)     try:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

360)         config = VaultNativeV03ConfigParser(contents, password)()
Marco Ricci Add prototype for "vault v0...

Marco Ricci authored 1 month ago

361)     except ValueError:
Marco Ricci Rename vault v0.2/v0.3 clas...

Marco Ricci authored 2 weeks ago

362)         config = VaultNativeV02ConfigParser(contents, password)()