Coverage for src\derivepassphrase\exporter\vault_native.py: 100.000%
204 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-23 12:17 +0200
1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info>
2#
3# SPDX-License-Identifier: Zlib
5"""Exporter for the vault native configuration format (v0.2 or v0.3).
7The vault native formats are the configuration formats used by vault
8v0.2 and v0.3. The configuration is stored as a single encrypted file,
9which is encrypted and authenticated. v0.2 and v0.3 differ in some
10details concerning key derivation and expected format of internal
11structures, so they are *not* compatible. v0.2 additionally contains
12cryptographic weaknesses (API misuse of a key derivation function, and
13a low-entropy method of generating initialization vectors for CBC block
14encryption mode) and should thus be avoided if possible.
16The public interface is the [`export_vault_native_data`][] function.
17Multiple *non-public* classes are additionally documented here for
18didactical and educational reasons, but they are not part of the module
19API, are subject to change without notice (including removal), and
20should *not* be used or relied on.
22"""
24# ruff: noqa: S303
26from __future__ import annotations
28import abc
29import base64
30import importlib
31import json
32import logging
33import os
34import pathlib
35import threading
36import warnings
37from typing import TYPE_CHECKING
39from derivepassphrase import exporter, vault
40from derivepassphrase._internals import cli_messages as _msg
42if TYPE_CHECKING:
43 from typing import Any
45 from typing_extensions import Buffer
47if TYPE_CHECKING:
48 from cryptography import exceptions as crypt_exceptions
49 from cryptography import utils as crypt_utils
50 from cryptography.hazmat.primitives import ciphers, hashes, hmac, padding
51 from cryptography.hazmat.primitives.ciphers import algorithms, modes
52 from cryptography.hazmat.primitives.kdf import pbkdf2
54 STUBBED = False
55else:
56 try:
57 importlib.import_module('cryptography')
58 except ModuleNotFoundError as exc:
59 # For type checking only, will not actually be called, so no
60 # coverage.
61 class _DummyModule: # pragma: no cover
62 def __init__(self, exc: type[Exception]) -> None:
63 self.exc = exc
65 def __getattr__(self, name: str) -> Any: # noqa: ANN401
66 def func(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401,ARG001
67 raise self.exc
69 return func
71 crypt_exceptions = crypt_utils = _DummyModule(exc)
72 ciphers = hashes = hmac = padding = _DummyModule(exc)
73 algorithms = modes = pbkdf2 = _DummyModule(exc)
74 STUBBED = True
75 else:
76 from cryptography import exceptions as crypt_exceptions
77 from cryptography import utils as crypt_utils
78 from cryptography.hazmat.primitives import (
79 ciphers,
80 hashes,
81 hmac,
82 padding,
83 )
84 from cryptography.hazmat.primitives.ciphers import algorithms, modes
85 from cryptography.hazmat.primitives.kdf import pbkdf2
87 STUBBED = False
89__all__ = ('export_vault_native_data',)
91logger = logging.getLogger(__name__)
94@exporter.register_export_vault_config_data_handler('v0.2', 'v0.3')
95def export_vault_native_data( # noqa: D417
96 path: str | bytes | os.PathLike | None = None,
97 key: str | Buffer | None = None,
98 *,
99 format: str, # noqa: A002
100) -> Any: # noqa: ANN401
101 """Export the full configuration stored in vault native format.
103 See [`exporter.ExportVaultConfigDataFunction`][] for an explanation
104 of the call signature, and the exceptions to expect.
106 Other Args:
107 format:
108 The only supported formats are `v0.2` and `v0.3`.
110 """ # noqa: DOC201,DOC501
111 # Trigger import errors if necessary.
112 importlib.import_module('cryptography') 1efgbljmhado
113 if path is None: 1efgbljmhad
114 path = exporter.get_vault_path() 1efbad
115 else:
116 path = pathlib.Path(os.fsdecode(path)) 1gljmhd
117 with path.open('rb') as infile: 1efgbljmhad
118 contents = base64.standard_b64decode(infile.read()) 1efgbjhad
119 if key is None: 1efgbjhad
120 key = exporter.get_vault_key() 1efjhad
121 parser_class: type[VaultNativeConfigParser] | None = { 1efgbjhad
122 'v0.2': VaultNativeV02ConfigParser,
123 'v0.3': VaultNativeV03ConfigParser,
124 }.get(format)
125 if parser_class is None: # pragma: no cover 1efgbjhad
126 # Defensive programming, will not actually be called like this
127 # by our code, so no coverage.
128 msg = exporter.INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format(
129 fmt=format
130 )
131 raise ValueError(msg)
132 try: 1efgbjhad
133 return parser_class(contents, key)() 1efgbjhad
134 except ValueError as exc: 1jha
135 raise exporter.NotAVaultConfigError(path, format=format) from exc 1jha
138def _h(bs: Buffer) -> str:
139 return '<{}>'.format(memoryview(bs).hex(' ')) 1efgbhadc
142class VaultNativeConfigParser(abc.ABC):
143 """A base parser for vault's native configuration format.
145 Certain details are specific to the respective vault versions, and
146 are abstracted out. This class by itself is not instantiable
147 because of this.
149 """
151 def __init__(self, contents: Buffer, password: str | Buffer) -> None:
152 """Initialize the parser.
154 Args:
155 contents:
156 The binary contents of the encrypted configuration file.
158 Note: On disk, these are usually stored in
159 base64-encoded form, not in the "raw" form as needed
160 here.
162 password:
163 The vault master key/master passphrase the file is
164 encrypted with. Must be non-empty. See
165 [`exporter.get_vault_key`][] for details.
167 If this is a text string, then the UTF-8 encoding of the
168 string is used as the binary password.
170 Raises:
171 ValueError:
172 The password must not be empty.
174 Warning:
175 Non-public class, provided for didactical and educational
176 purposes only. Subject to change without notice, including
177 removal.
179 """
180 if not password: 1efgbjhadcn
181 msg = 'Password must not be empty' 1n
182 raise ValueError(msg) 1n
183 self._consistency_lock = threading.RLock() 1efgbjhadc
184 self._contents = bytes(contents) 1efgbjhadc
185 self._iv_size = 0 1efgbjhadc
186 self._mac_size = 0 1efgbjhadc
187 self._encryption_key = b'' 1efgbjhadc
188 self._encryption_key_size = 0 1efgbjhadc
189 self._signing_key = b'' 1efgbjhadc
190 self._signing_key_size = 0 1efgbjhadc
191 self._message = b'' 1efgbjhadc
192 self._message_tag = b'' 1efgbjhadc
193 self._iv = b'' 1efgbjhadc
194 self._payload = b'' 1efgbjhadc
195 self._password = password 1efgbjhadc
196 self._sentinel: object = object() 1efgbjhadc
197 self._data: Any = self._sentinel 1efgbjhadc
199 def __call__(self) -> Any: # noqa: ANN401
200 """Return the decrypted and parsed vault configuration.
202 Raises:
203 cryptography.exceptions.InvalidSignature:
204 The encrypted configuration does not contain a valid
205 signature.
206 ValueError:
207 The format is invalid, in a non-cryptographic way. (For
208 example, it contains an unsupported version marker, or
209 unexpected extra contents, or invalid padding.)
211 """
212 with self._consistency_lock: 1efgbjhadc
213 if self._data is self._sentinel: 1efgbjhadc
214 self._parse_contents() 1efgbjhadc
215 self._derive_keys() 1efgbhadc
216 self._check_signature() 1efgbhadc
217 self._data = self._decrypt_payload() 1efgbadc
218 return self._data 1efgbjhadc
220 @staticmethod
221 def _pbkdf2(
222 password: str | Buffer, key_size: int, iterations: int
223 ) -> bytes:
224 """Generate a key from a password.
226 Uses PBKDF2 with HMAC-SHA1, with [vault.Vault.UUID][] as a fixed
227 salt value.
229 Args:
230 password:
231 The password from which to derive the key.
232 key_size:
233 The size of the output string. The effective key size
234 (in bytes) is thus half of this output string size.
235 iterations:
236 The PBKDF2 iteration count.
238 Returns:
239 The PBKDF2-derived key, encoded as a lowercase ASCII
240 hexadecimal string.
242 Danger: Insecure use of cryptography
243 This function is insecure because it uses a fixed salt
244 value, which is not secure against rainbow tables. It is
245 further difficult to use because the effective key size is
246 only half as large as the "size" parameter (output string
247 size). Finally, though the use of SHA-1 in HMAC per se is
248 not known to be insecure, SHA-1 is known not to be
249 collision-resistant.
251 """
252 if isinstance(password, str): 1efgbhkadc
253 password = password.encode('utf-8') 1dc
254 raw_key = pbkdf2.PBKDF2HMAC( 1efgbhkadc
255 algorithm=hashes.SHA1(),
256 length=key_size // 2,
257 salt=vault.Vault.UUID,
258 iterations=iterations,
259 ).derive(bytes(password))
260 result_key = raw_key.hex().lower().encode('ASCII') 1efgbhkadc
261 logger.debug( 1efgbhkadc
262 _msg.TranslatedString(
263 _msg.DebugMsgTemplate.VAULT_NATIVE_PBKDF2_CALL,
264 password=password,
265 salt=vault.Vault.UUID,
266 iterations=iterations,
267 key_size=key_size // 2,
268 algorithm='sha1',
269 raw_result=raw_key,
270 result_key=result_key.decode('ASCII'),
271 ),
272 )
273 return result_key 1efgbhkadc
275 def _parse_contents(self) -> None:
276 """Parse the contents into IV, payload and MAC.
278 This operates on, and sets, multiple internal attributes of the
279 parser.
281 Raises:
282 ValueError:
283 The configuration file contents are clearly truncated.
285 """
286 logger.info( 1efgbjhadc
287 _msg.TranslatedString(
288 _msg.InfoMsgTemplate.VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC,
289 ),
290 )
292 def cut(buffer: bytes, cutpoint: int) -> tuple[bytes, bytes]: 1efgbjhadc
293 return buffer[:cutpoint], buffer[cutpoint:] 1efgbhadc
295 with self._consistency_lock: 1efgbjhadc
296 contents = self._contents 1efgbjhadc
297 iv_size = self._iv_size 1efgbjhadc
298 mac_size = self._mac_size 1efgbjhadc
300 if len(contents) < iv_size + 16 + mac_size: 1efgbjhadc
301 msg = 'Invalid vault configuration file: file is truncated' 1j
302 raise ValueError(msg) 1j
304 cutpos1 = len(contents) - mac_size 1efgbhadc
305 cutpos2 = iv_size 1efgbhadc
306 message, message_tag = cut(contents, cutpos1) 1efgbhadc
307 iv, payload = cut(message, cutpos2) 1efgbhadc
309 self._message = message 1efgbhadc
310 self._message_tag = message_tag 1efgbhadc
311 self._iv = iv 1efgbhadc
312 self._payload = payload 1efgbjhadc
314 logger.debug( 1efgbhadc
315 _msg.TranslatedString(
316 _msg.DebugMsgTemplate.VAULT_NATIVE_PARSE_BUFFER,
317 contents=_h(contents),
318 iv=_h(iv),
319 payload=_h(payload),
320 mac=_h(message_tag),
321 ),
322 )
324 def _derive_keys(self) -> None:
325 """Derive the signing and encryption keys.
327 This is a bookkeeping method. The actual work is done in
328 [`_generate_keys`][].
330 """
331 logger.info( 1efgbhadc
332 _msg.TranslatedString(
333 _msg.InfoMsgTemplate.VAULT_NATIVE_DERIVING_KEYS,
334 ),
335 )
336 with self._consistency_lock: 1efgbhadc
337 self._generate_keys() 1efgbhadc
338 assert len(self._encryption_key) == self._encryption_key_size, ( 1efgbhadc
339 'Derived encryption key is invalid'
340 )
341 assert len(self._signing_key) == self._signing_key_size, ( 1efgbhadc
342 'Derived signing key is invalid'
343 )
345 @abc.abstractmethod
346 def _generate_keys(self) -> None:
347 """Derive the signing and encryption keys, and set the key sizes.
349 Subclasses must override this, as the derivation system is
350 version-specific. The default implementation raises an error.
352 Raises:
353 AssertionError:
354 There is no default implementation.
356 """
357 raise AssertionError
359 def _check_signature(self) -> None:
360 """Check for a valid MAC on the encrypted vault configuration.
362 The MAC uses HMAC-SHA1, and thus is 32 bytes long, before
363 encoding.
365 Raises:
366 ValueError:
367 The MAC is invalid.
369 """
370 logger.info( 1efgbhadc
371 _msg.TranslatedString(
372 _msg.InfoMsgTemplate.VAULT_NATIVE_CHECKING_MAC,
373 ),
374 )
375 with self._consistency_lock: 1efgbhadc
376 mac = hmac.HMAC(self._signing_key, hashes.SHA256()) 1efgbhadc
377 mac_input = self._hmac_input() 1efgbhadc
378 mac_expected = self._message_tag 1efgbhadc
379 logger.debug( 1efgbhadc
380 _msg.TranslatedString(
381 _msg.DebugMsgTemplate.VAULT_NATIVE_CHECKING_MAC_DETAILS,
382 mac_input=_h(mac_input),
383 mac=_h(mac_expected),
384 ),
385 )
386 mac.update(mac_input) 1efgbhadc
387 try: 1efgbhadc
388 mac.verify(mac_expected) 1efgbhadc
389 except crypt_exceptions.InvalidSignature: 1ha
390 msg = 'File does not contain a valid signature' 1ha
391 raise ValueError(msg) from None 1ha
393 @abc.abstractmethod
394 def _hmac_input(self) -> bytes:
395 """Return the input the MAC is supposed to verify.
397 Subclasses must override this, as the MAC-attested data is
398 version-specific. The default implementation raises an error.
400 Raises:
401 AssertionError:
402 There is no default implementation.
404 """
405 raise AssertionError
407 def _decrypt_payload(self) -> Any: # noqa: ANN401
408 """Return the decrypted vault configuration.
410 Requires [`_parse_contents`][] and [`_derive_keys`][] to have
411 run, and relies on [`_check_signature`][] for tampering
412 detection.
414 """
415 logger.info( 1efgbadc
416 _msg.TranslatedString(
417 _msg.InfoMsgTemplate.VAULT_NATIVE_DECRYPTING_CONTENTS,
418 ),
419 )
420 with self._consistency_lock: 1efgbadc
421 payload = self._payload 1efgbadc
422 iv_size = self._iv_size 1efgbadc
423 decryptor = self._make_decryptor() 1efgbadc
424 padded_plaintext = bytearray() 1efgbadc
425 padded_plaintext.extend(decryptor.update(payload)) 1efgbadc
426 padded_plaintext.extend(decryptor.finalize()) 1efgbadc
427 logger.debug( 1efgbadc
428 _msg.TranslatedString(
429 _msg.DebugMsgTemplate.VAULT_NATIVE_PADDED_PLAINTEXT,
430 contents=_h(padded_plaintext),
431 ),
432 )
433 unpadder = padding.PKCS7(iv_size * 8).unpadder() 1efgbadc
434 plaintext = bytearray() 1efgbadc
435 plaintext.extend(unpadder.update(padded_plaintext)) 1efgbadc
436 plaintext.extend(unpadder.finalize()) 1efgbadc
437 logger.debug( 1efgbadc
438 _msg.TranslatedString(
439 _msg.DebugMsgTemplate.VAULT_NATIVE_PLAINTEXT,
440 contents=_h(plaintext),
441 ),
442 )
443 return json.loads(plaintext) 1efgbadc
445 @abc.abstractmethod
446 def _make_decryptor(self) -> ciphers.CipherContext:
447 """Return the cipher context object used for decryption.
449 Subclasses must override this, as the cipher setup is
450 version-specific. The default implementation raises an error.
452 Raises:
453 AssertionError:
454 There is no default implementation.
456 """
457 raise AssertionError
460class VaultNativeV03ConfigParser(VaultNativeConfigParser):
461 """A parser for vault's native configuration format (v0.3).
463 This is the modern, pre-storeroom configuration format.
465 Warning:
466 Non-public class, provided for didactical and educational
467 purposes only. Subject to change without notice, including
468 removal.
470 """
472 KEY_SIZE = 32
473 """
474 Key size for both the encryption and the signing key, including the
475 encoding as a hexadecimal string. (The effective cryptographic
476 strength is half of this value.)
477 """
479 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
480 super().__init__(*args, **kwargs) 1efgbjhadcn
481 self._iv_size = 16 1efgbjhadc
482 self._mac_size = 32 1efgbjhadc
484 def _generate_keys(self) -> None:
485 """Derive the signing and encryption keys, and set the key sizes.
487 Version 0.3 vault configurations use a constant key size; see
488 [`KEY_SIZE`][]. The encryption and signing keys differ in how
489 many rounds of PBKDF2 they use (100 and 200, respectively).
491 Danger: Insecure use of cryptography
492 This function makes use of the insecure function
493 [`VaultNativeConfigParser._pbkdf2`][], without any attempts
494 at mitigating its insecurity. It further uses `_pbkdf2`
495 with the low iteration count of 100 and 200 rounds, which is
496 *drastically* insufficient to defend against password
497 guessing attacks using GPUs or ASICs. We provide this
498 function for the purpose of interoperability with existing
499 vault installations. Do not rely on this system to keep
500 your vault configuration secure against access by even
501 moderately determined attackers!
503 """
504 with self._consistency_lock: 1efgbhadc
505 self._encryption_key = self._pbkdf2( 1efgbhadc
506 self._password, self.KEY_SIZE, 100
507 )
508 self._signing_key = self._pbkdf2( 1efgbhadc
509 self._password, self.KEY_SIZE, 200
510 )
511 self._encryption_key_size = self._signing_key_size = self.KEY_SIZE 1efgbhadc
513 def _hmac_input(self) -> bytes:
514 """Return the input the MAC is supposed to verify.
516 This includes hexadecimal encoding of the message payload.
518 """
519 return self._message.hex().lower().encode('ASCII') 1efgbhadc
521 def _make_decryptor(self) -> ciphers.CipherContext:
522 """Return the cipher context object used for decryption.
524 This is a standard AES256-CBC cipher context using the
525 previously derived encryption key and the IV declared in the
526 (MAC-verified) message payload.
528 """
529 with self._consistency_lock: 1efgbadc
530 encryption_key = self._encryption_key 1efgbadc
531 iv = self._iv 1efgbadc
532 return ciphers.Cipher( 1efgbadc
533 algorithms.AES256(encryption_key), modes.CBC(iv)
534 ).decryptor()
537class VaultNativeV02ConfigParser(VaultNativeConfigParser):
538 """A parser for vault's native configuration format (v0.2).
540 This is the classic configuration format. Compared to v0.3, it
541 contains an (accidental) API misuse for the generation of the master
542 keys, a low-entropy method of generating initialization vectors for
543 the AES-CBC encryption step, and extra layers of base64 encoding.
544 Because of these significantly weakened confidentiality guarantees,
545 v0.2 configurations should be upgraded to at least v0.3 as soon as
546 possible.
548 Warning:
549 Non-public class, provided for didactical and educational
550 purposes only. Subject to change without notice, including
551 removal.
553 """
555 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401
556 super().__init__(*args, **kwargs) 1bjac
557 self._iv_size = 16 1bjac
558 self._mac_size = 64 1bjac
560 def _parse_contents(self) -> None:
561 """Parse the contents into IV, payload and MAC.
563 Like the base class implementation, this operates on, and sets,
564 multiple internal attributes of the parser. In version 0.2
565 vault configurations, the payload is encoded in base64 and the
566 message tag (MAC) is encoded in hexadecimal, so unlike the base
567 class implementation, we additionally decode the payload and the
568 MAC.
570 Raises:
571 ValueError:
572 The configuration file contents are clearly truncated,
573 or the payload or the message tag cannot be decoded
574 properly.
576 """
577 with self._consistency_lock: 1bjac
578 super()._parse_contents() 1bjac
579 payload = self._payload = base64.standard_b64decode(self._payload) 1bac
580 message_tag = self._message_tag = bytes.fromhex( 1bjac
581 self._message_tag.decode('ASCII')
582 )
583 logger.debug( 1bac
584 _msg.TranslatedString(
585 _msg.DebugMsgTemplate.VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING,
586 payload=_h(payload),
587 mac=_h(message_tag),
588 ),
589 )
591 def _generate_keys(self) -> None:
592 """Derive the signing and encryption keys, and set the key sizes.
594 Version 0.2 vault configurations use 8-byte encryption keys and
595 16-byte signing keys, including the hexadecimal encoding. They
596 both use 16 rounds of PBKDF2. This is due to an oversight in
597 vault, where the author mistakenly supplied the intended
598 iteration count as the key size, and the key size as the
599 iteration count.
601 Danger: Insecure use of cryptography
602 This function makes use of the insecure function
603 [`VaultNativeConfigParser._pbkdf2`][], without any attempts
604 at mitigating its insecurity. It further uses `_pbkdf2`
605 with the low iteration count of 16 rounds, which is
606 *drastically* insufficient to defend against password
607 guessing attacks using GPUs or ASICs, and generates the
608 encryption key as a truncation of the signing key. We
609 provide this function for the purpose of interoperability
610 with existing vault installations. Do not rely on this
611 system to keep your vault configuration secure against
612 access by even moderately determined attackers!
614 """
615 with self._consistency_lock: 1bac
616 self._encryption_key = self._pbkdf2(self._password, 8, 16) 1bac
617 self._signing_key = self._pbkdf2(self._password, 16, 16) 1bac
618 self._encryption_key_size = 8 1bac
619 self._signing_key_size = 16 1bac
621 def _hmac_input(self) -> bytes:
622 """Return the input the MAC is supposed to verify.
624 This includes hexadecimal encoding of the message payload.
626 """
627 return base64.standard_b64encode(self._message) 1bac
629 @staticmethod
630 def _evp_bytestokey_md5_one_iteration_no_salt(
631 data: bytes, key_size: int, iv_size: int
632 ) -> tuple[bytes, bytes]:
633 """Reimplement OpenSSL's `EVP_BytesToKey` with fixed parameters.
635 `EVP_BytesToKey` in general is a key derivation function,
636 i.e., a function that derives key material from an input
637 byte string. `EVP_BytesToKey` conceptually splits the
638 derived key material into an encryption key and an
639 initialization vector (IV).
641 Note: Algorithm description
642 `EVP_BytesToKey` takes an input byte string, two output
643 size (encryption key size and IV size), a message digest
644 function, a salt value and an iteration count. The
645 derived key material is calculated in blocks, each of
646 which is the output of (iterated application of) the
647 message digest function. The input to the message
648 digest function is the concatenation of the previous
649 block (if any) with the input byte string and the salt
650 value (if any):
652 ~~~~ python
653 data = block_input = b''.join([previous_block, input_string, salt])
654 for i in range(iteration_count):
655 data = message_digest(data)
656 block = data
657 ~~~~
659 We use as many blocks as are necessary to cover the
660 total output byte string size. The first few bytes
661 (dictated by the encryption key size) form the
662 encryption key, the other bytes (dictated by the IV
663 size) form the IV.
665 We implement exactly the subset of `EVP_BytesToKey` that the
666 Node.js `crypto` library (v21 series and older) uses in its
667 implementation of `crypto.createCipher("aes256", password)`.
668 Specifically, the message digest function is fixed to MD5,
669 the salt is always empty, and the iteration count is fixed
670 at one.
673 Returns:
674 A 2-tuple containing the derived encryption key and the
675 derived initialization vector.
677 Danger: Insecure use of cryptography
678 This function reimplements the OpenSSL function
679 `EVP_BytesToKey`, which generates cryptographically weak
680 keys, without any attempts at mitigating its insecurity. We
681 provide this function for the purpose of interoperability
682 with existing vault installations. Do not rely on this
683 system to keep your vault configuration secure against
684 access by even moderately determined attackers!
686 """
687 total_size = key_size + iv_size 1bac
688 buffer = bytearray() 1bac
689 last_block = b'' 1bac
690 salt = b'' 1bac
691 logger.debug( 1bac
692 _msg.TranslatedString(
693 _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_INIT,
694 data=_h(data),
695 salt=_h(salt),
696 key_size=key_size,
697 iv_size=iv_size,
698 buffer_length=len(buffer),
699 buffer=_h(buffer),
700 ),
701 )
702 while len(buffer) < total_size: 1bac
703 with warnings.catch_warnings(): 1bac
704 warnings.simplefilter( 1bac
705 'ignore', crypt_utils.CryptographyDeprecationWarning
706 )
707 block = hashes.Hash(hashes.MD5()) 1bac
708 block.update(last_block) 1bac
709 block.update(data) 1bac
710 block.update(salt) 1bac
711 last_block = block.finalize() 1bac
712 buffer.extend(last_block) 1bac
713 logger.debug( 1bac
714 _msg.TranslatedString(
715 _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_ROUND,
716 buffer_length=len(buffer),
717 buffer=_h(buffer),
718 ),
719 )
720 logger.debug( 1bac
721 _msg.TranslatedString(
722 _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_RESULT,
723 enc_key=_h(buffer[:key_size]),
724 iv=_h(buffer[key_size:total_size]),
725 ),
726 )
727 return bytes(buffer[:key_size]), bytes(buffer[key_size:total_size]) 1bac
729 def _make_decryptor(self) -> ciphers.CipherContext:
730 """Return the cipher context object used for decryption.
732 This is a standard AES256-CBC cipher context. The encryption key
733 and the IV are derived via the OpenSSL `EVP_BytesToKey` function
734 (using MD5, no salt, and one iteration). This is what the
735 Node.js `crypto` library (v21 series and older) used in its
736 implementation of `crypto.createCipher("aes256", password)`.
738 Danger: Insecure use of cryptography
739 This function makes use of (an implementation of) the
740 OpenSSL function `EVP_BytesToKey`, which generates
741 cryptographically weak keys, without any attempts at
742 mitigating its insecurity. We provide this function for the
743 purpose of interoperability with existing vault
744 installations. Do not rely on this system to keep your
745 vault configuration secure against access by even moderately
746 determined attackers!
748 """
749 with self._consistency_lock: 1bac
750 data = base64.standard_b64encode(self._iv + self._encryption_key) 1bac
751 encryption_key, iv = self._evp_bytestokey_md5_one_iteration_no_salt( 1bac
752 data, key_size=32, iv_size=16
753 )
754 return ciphers.Cipher( 1bac
755 algorithms.AES256(encryption_key), modes.CBC(iv)
756 ).decryptor()
759if __name__ == '__main__':
760 import os
762 logging.basicConfig(level=('DEBUG' if os.getenv('DEBUG') else 'WARNING'))
763 with exporter.get_vault_path().open('rb') as infile:
764 contents = base64.standard_b64decode(infile.read())
765 password = exporter.get_vault_key()
766 try:
767 config = VaultNativeV03ConfigParser(contents, password)()
768 except ValueError:
769 config = VaultNativeV02ConfigParser(contents, password)()
770 print(json.dumps(config, indent=2, sort_keys=True)) # noqa: T201