Coverage for src / derivepassphrase / exporter / vault_native.py: 100.000%

204 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 21:34 +0200

1# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> 

2# 

3# SPDX-License-Identifier: Zlib 

4 

5"""Exporter for the vault native configuration format (v0.2 or v0.3). 

6 

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. 

15 

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. 

21 

22""" 

23 

24# ruff: noqa: S303 

25 

26from __future__ import annotations 

27 

28import abc 

29import base64 

30import importlib 

31import json 

32import logging 

33import os 

34import pathlib 

35import threading 

36import warnings 

37from typing import TYPE_CHECKING 

38 

39from derivepassphrase import exporter, vault 

40from derivepassphrase._internals import cli_messages as _msg 

41 

42if TYPE_CHECKING: 

43 from typing import Any 

44 

45 from typing_extensions import Buffer 

46 

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 

53 

54 STUBBED = False 

55else: 

56 try: 

57 importlib.import_module("cryptography") 

58 except ModuleNotFoundError as exc: 

59 

60 class _DummyModule: 

61 def __init__(self, exc: type[Exception]) -> None: 

62 self.exc = exc 

63 

64 def __getattr__(self, name: str) -> Any: # noqa: ANN401 

65 def func(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401,ARG001 

66 raise self.exc 

67 

68 return func 

69 

70 crypt_exceptions = crypt_utils = _DummyModule(exc) 

71 ciphers = hashes = hmac = padding = _DummyModule(exc) 

72 algorithms = modes = pbkdf2 = _DummyModule(exc) 

73 STUBBED = True 

74 else: 

75 from cryptography import exceptions as crypt_exceptions 

76 from cryptography import utils as crypt_utils 

77 from cryptography.hazmat.primitives import ( 

78 ciphers, 

79 hashes, 

80 hmac, 

81 padding, 

82 ) 

83 from cryptography.hazmat.primitives.ciphers import algorithms, modes 

84 from cryptography.hazmat.primitives.kdf import pbkdf2 

85 

86 STUBBED = False 

87 

88__all__ = ("export_vault_native_data",) 

89 

90logger = logging.getLogger(__name__) 

91 

92 

93@exporter.register_export_vault_config_data_handler("v0.2", "v0.3") 

94def export_vault_native_data( # noqa: D417 

95 path: str | bytes | os.PathLike | None = None, 

96 key: str | Buffer | None = None, 

97 *, 

98 format: str, # noqa: A002 

99) -> Any: # noqa: ANN401 

100 """Export the full configuration stored in vault native format. 

101 

102 See [`exporter.ExportVaultConfigDataFunction`][] for an explanation 

103 of the call signature, and the exceptions to expect. 

104 

105 Other Args: 

106 format: 

107 The only supported formats are `v0.2` and `v0.3`. 

108 

109 """ # noqa: DOC201,DOC501 

110 # Trigger import errors if necessary. 

111 importlib.import_module("cryptography") 1ehljmgbfdao

112 if path is None: 1ehljmgbfda

113 path = exporter.get_vault_path() 1ebfda

114 else: 

115 path = pathlib.Path(os.fsdecode(path)) 1hljmgd

116 with path.open("rb") as infile: 1ehljmgbfda

117 contents = base64.standard_b64decode(infile.read()) 1ehjgbfda

118 if key is None: 1ehjgbfda

119 key = exporter.get_vault_key() 1ehjfda

120 parser_class: type[VaultNativeConfigParser] | None = { 1ehjgbfda

121 "v0.2": VaultNativeV02ConfigParser, 

122 "v0.3": VaultNativeV03ConfigParser, 

123 }.get(format) 

124 if parser_class is None: # pragma: no cover [failsafe] 1ehjgbfda

125 msg = exporter.INVALID_VAULT_NATIVE_CONFIGURATION_FORMAT.format( 

126 fmt=format 

127 ) 

128 raise ValueError(msg) 

129 try: 1ehjgbfda

130 return parser_class(contents, key)() 1ehjgbfda

131 except ValueError as exc: 1hja

132 raise exporter.NotAVaultConfigError(path, format=format) from exc 1hja

133 

134 

135def _h(bs: Buffer) -> str: 

136 return "<{}>".format(memoryview(bs).hex(" ")) 1ehgbfdac

137 

138 

139class VaultNativeConfigParser(abc.ABC): 

140 """A base parser for vault's native configuration format. 

141 

142 Certain details are specific to the respective vault versions, and 

143 are abstracted out. This class by itself is not instantiable 

144 because of this. 

145 

146 """ 

147 

148 def __init__(self, contents: Buffer, password: str | Buffer) -> None: 

149 """Initialize the parser. 

150 

151 Args: 

152 contents: 

153 The binary contents of the encrypted configuration file. 

154 

155 Note: On disk, these are usually stored in 

156 base64-encoded form, not in the "raw" form as needed 

157 here. 

158 

159 password: 

160 The vault master key/master passphrase the file is 

161 encrypted with. Must be non-empty. See 

162 [`exporter.get_vault_key`][] for details. 

163 

164 If this is a text string, then the UTF-8 encoding of the 

165 string is used as the binary password. 

166 

167 Raises: 

168 ValueError: 

169 The password must not be empty. 

170 

171 Warning: 

172 Non-public class, provided for didactical and educational 

173 purposes only. Subject to change without notice, including 

174 removal. 

175 

176 """ 

177 if not password: 1ehjgbfdanc

178 msg = "Password must not be empty" 1n

179 raise ValueError(msg) 1n

180 self._consistency_lock = threading.RLock() 1ehjgbfdac

181 self._contents = bytes(contents) 1ehjgbfdac

182 self._iv_size = 0 1ehjgbfdac

183 self._mac_size = 0 1ehjgbfdac

184 self._encryption_key = b"" 1ehjgbfdac

185 self._encryption_key_size = 0 1ehjgbfdac

186 self._signing_key = b"" 1ehjgbfdac

187 self._signing_key_size = 0 1ehjgbfdac

188 self._message = b"" 1ehjgbfdac

189 self._message_tag = b"" 1ehjgbfdac

190 self._iv = b"" 1ehjgbfdac

191 self._payload = b"" 1ehjgbfdac

192 self._password = password 1ehjgbfdac

193 self._sentinel: object = object() 1ehjgbfdac

194 self._data: Any = self._sentinel 1ehjgbfdac

195 

196 def __call__(self) -> Any: # noqa: ANN401 

197 """Return the decrypted and parsed vault configuration. 

198 

199 Raises: 

200 cryptography.exceptions.InvalidSignature: 

201 The encrypted configuration does not contain a valid 

202 signature. 

203 ValueError: 

204 The format is invalid, in a non-cryptographic way. (For 

205 example, it contains an unsupported version marker, or 

206 unexpected extra contents, or invalid padding.) 

207 

208 """ 

209 with self._consistency_lock: 1ehjgbfdac

210 if self._data is self._sentinel: 1ehjgbfdac

211 self._parse_contents() 1ehjgbfdac

212 self._derive_keys() 1ehgbfdac

213 self._check_signature() 1ehgbfdac

214 self._data = self._decrypt_payload() 1egbfdac

215 return self._data 1ehjgbfdac

216 

217 @staticmethod 

218 def _pbkdf2( 

219 password: str | Buffer, key_size: int, iterations: int 

220 ) -> bytes: 

221 """Generate a key from a password. 

222 

223 Uses PBKDF2 with HMAC-SHA1, with [vault.Vault.UUID][] as a fixed 

224 salt value. 

225 

226 Args: 

227 password: 

228 The password from which to derive the key. 

229 key_size: 

230 The size of the output string. The effective key size 

231 (in bytes) is thus half of this output string size. 

232 iterations: 

233 The PBKDF2 iteration count. 

234 

235 Returns: 

236 The PBKDF2-derived key, encoded as a lowercase ASCII 

237 hexadecimal string. 

238 

239 Danger: Insecure use of cryptography 

240 This function is insecure because it uses a fixed salt 

241 value, which is not secure against rainbow tables. It is 

242 further difficult to use because the effective key size is 

243 only half as large as the "size" parameter (output string 

244 size). Finally, though the use of SHA-1 in HMAC per se is 

245 not known to be insecure, SHA-1 is known not to be 

246 collision-resistant. 

247 

248 """ 

249 if isinstance(password, str): 1ehgbfdakc

250 password = password.encode("utf-8") 1dc

251 raw_key = pbkdf2.PBKDF2HMAC( 1ehgbfdakc

252 algorithm=hashes.SHA1(), 

253 length=key_size // 2, 

254 salt=vault.Vault.UUID, 

255 iterations=iterations, 

256 ).derive(bytes(password)) 

257 result_key = raw_key.hex().lower().encode("ASCII") 1ehgbfdakc

258 logger.debug( 1ehgbfdakc

259 _msg.TranslatedString( 

260 _msg.DebugMsgTemplate.VAULT_NATIVE_PBKDF2_CALL, 

261 password=password, 

262 salt=vault.Vault.UUID, 

263 iterations=iterations, 

264 key_size=key_size // 2, 

265 algorithm="sha1", 

266 raw_result=raw_key, 

267 result_key=result_key.decode("ASCII"), 

268 ), 

269 ) 

270 return result_key 1ehgbfdakc

271 

272 def _parse_contents(self) -> None: 

273 """Parse the contents into IV, payload and MAC. 

274 

275 This operates on, and sets, multiple internal attributes of the 

276 parser. 

277 

278 Raises: 

279 ValueError: 

280 The configuration file contents are clearly truncated. 

281 

282 """ 

283 logger.info( 1ehjgbfdac

284 _msg.TranslatedString( 

285 _msg.InfoMsgTemplate.VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC, 

286 ), 

287 ) 

288 

289 def cut(buffer: bytes, cutpoint: int) -> tuple[bytes, bytes]: 1ehjgbfdac

290 return buffer[:cutpoint], buffer[cutpoint:] 1ehgbfdac

291 

292 with self._consistency_lock: 1ehjgbfdac

293 contents = self._contents 1ehjgbfdac

294 iv_size = self._iv_size 1ehjgbfdac

295 mac_size = self._mac_size 1ehjgbfdac

296 

297 if len(contents) < iv_size + 16 + mac_size: 1ehjgbfdac

298 msg = "Invalid vault configuration file: file is truncated" 1j

299 raise ValueError(msg) 1j

300 

301 cutpos1 = len(contents) - mac_size 1ehgbfdac

302 cutpos2 = iv_size 1ehgbfdac

303 message, message_tag = cut(contents, cutpos1) 1ehgbfdac

304 iv, payload = cut(message, cutpos2) 1ehgbfdac

305 

306 self._message = message 1ehgbfdac

307 self._message_tag = message_tag 1ehgbfdac

308 self._iv = iv 1ehgbfdac

309 self._payload = payload 1ehjgbfdac

310 

311 logger.debug( 1ehgbfdac

312 _msg.TranslatedString( 

313 _msg.DebugMsgTemplate.VAULT_NATIVE_PARSE_BUFFER, 

314 contents=_h(contents), 

315 iv=_h(iv), 

316 payload=_h(payload), 

317 mac=_h(message_tag), 

318 ), 

319 ) 

320 

321 def _derive_keys(self) -> None: 

322 """Derive the signing and encryption keys. 

323 

324 This is a bookkeeping method. The actual work is done in 

325 [`_generate_keys`][]. 

326 

327 """ 

328 logger.info( 1ehgbfdac

329 _msg.TranslatedString( 

330 _msg.InfoMsgTemplate.VAULT_NATIVE_DERIVING_KEYS, 

331 ), 

332 ) 

333 with self._consistency_lock: 1ehgbfdac

334 self._generate_keys() 1ehgbfdac

335 assert len(self._encryption_key) == self._encryption_key_size, ( 1ehgbfdac

336 "Derived encryption key is invalid" 

337 ) 

338 assert len(self._signing_key) == self._signing_key_size, ( 1ehgbfdac

339 "Derived signing key is invalid" 

340 ) 

341 

342 @abc.abstractmethod 

343 def _generate_keys(self) -> None: 

344 """Derive the signing and encryption keys, and set the key sizes. 

345 

346 Subclasses must override this, as the derivation system is 

347 version-specific. The default implementation raises an error. 

348 

349 Raises: 

350 AssertionError: 

351 There is no default implementation. 

352 

353 """ 

354 raise AssertionError 

355 

356 def _check_signature(self) -> None: 

357 """Check for a valid MAC on the encrypted vault configuration. 

358 

359 The MAC uses HMAC-SHA1, and thus is 32 bytes long, before 

360 encoding. 

361 

362 Raises: 

363 ValueError: 

364 The MAC is invalid. 

365 

366 """ 

367 logger.info( 1ehgbfdac

368 _msg.TranslatedString( 

369 _msg.InfoMsgTemplate.VAULT_NATIVE_CHECKING_MAC, 

370 ), 

371 ) 

372 with self._consistency_lock: 1ehgbfdac

373 mac = hmac.HMAC(self._signing_key, hashes.SHA256()) 1ehgbfdac

374 mac_input = self._hmac_input() 1ehgbfdac

375 mac_expected = self._message_tag 1ehgbfdac

376 logger.debug( 1ehgbfdac

377 _msg.TranslatedString( 

378 _msg.DebugMsgTemplate.VAULT_NATIVE_CHECKING_MAC_DETAILS, 

379 mac_input=_h(mac_input), 

380 mac=_h(mac_expected), 

381 ), 

382 ) 

383 mac.update(mac_input) 1ehgbfdac

384 try: 1ehgbfdac

385 mac.verify(mac_expected) 1ehgbfdac

386 except crypt_exceptions.InvalidSignature: 1ha

387 msg = "File does not contain a valid signature" 1ha

388 raise ValueError(msg) from None 1ha

389 

390 @abc.abstractmethod 

391 def _hmac_input(self) -> bytes: 

392 """Return the input the MAC is supposed to verify. 

393 

394 Subclasses must override this, as the MAC-attested data is 

395 version-specific. The default implementation raises an error. 

396 

397 Raises: 

398 AssertionError: 

399 There is no default implementation. 

400 

401 """ 

402 raise AssertionError 

403 

404 def _decrypt_payload(self) -> Any: # noqa: ANN401 

405 """Return the decrypted vault configuration. 

406 

407 Requires [`_parse_contents`][] and [`_derive_keys`][] to have 

408 run, and relies on [`_check_signature`][] for tampering 

409 detection. 

410 

411 """ 

412 logger.info( 1egbfdac

413 _msg.TranslatedString( 

414 _msg.InfoMsgTemplate.VAULT_NATIVE_DECRYPTING_CONTENTS, 

415 ), 

416 ) 

417 with self._consistency_lock: 1egbfdac

418 payload = self._payload 1egbfdac

419 iv_size = self._iv_size 1egbfdac

420 decryptor = self._make_decryptor() 1egbfdac

421 padded_plaintext = bytearray() 1egbfdac

422 padded_plaintext.extend(decryptor.update(payload)) 1egbfdac

423 padded_plaintext.extend(decryptor.finalize()) 1egbfdac

424 logger.debug( 1egbfdac

425 _msg.TranslatedString( 

426 _msg.DebugMsgTemplate.VAULT_NATIVE_PADDED_PLAINTEXT, 

427 contents=_h(padded_plaintext), 

428 ), 

429 ) 

430 unpadder = padding.PKCS7(iv_size * 8).unpadder() 1egbfdac

431 plaintext = bytearray() 1egbfdac

432 plaintext.extend(unpadder.update(bytes(padded_plaintext))) 1egbfdac

433 plaintext.extend(unpadder.finalize()) 1egbfdac

434 logger.debug( 1egbfdac

435 _msg.TranslatedString( 

436 _msg.DebugMsgTemplate.VAULT_NATIVE_PLAINTEXT, 

437 contents=_h(plaintext), 

438 ), 

439 ) 

440 return json.loads(plaintext) 1egbfdac

441 

442 @abc.abstractmethod 

443 def _make_decryptor(self) -> ciphers.CipherContext: 

444 """Return the cipher context object used for decryption. 

445 

446 Subclasses must override this, as the cipher setup is 

447 version-specific. The default implementation raises an error. 

448 

449 Raises: 

450 AssertionError: 

451 There is no default implementation. 

452 

453 """ 

454 raise AssertionError 

455 

456 

457class VaultNativeV03ConfigParser(VaultNativeConfigParser): 

458 """A parser for vault's native configuration format (v0.3). 

459 

460 This is the modern, pre-storeroom configuration format. 

461 

462 Warning: 

463 Non-public class, provided for didactical and educational 

464 purposes only. Subject to change without notice, including 

465 removal. 

466 

467 """ 

468 

469 KEY_SIZE = 32 

470 """ 

471 Key size for both the encryption and the signing key, including the 

472 encoding as a hexadecimal string. (The effective cryptographic 

473 strength is half of this value.) 

474 """ 

475 

476 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 

477 super().__init__(*args, **kwargs) 1ehjgbfdanc

478 self._iv_size = 16 1ehjgbfdac

479 self._mac_size = 32 1ehjgbfdac

480 

481 def _generate_keys(self) -> None: 

482 """Derive the signing and encryption keys, and set the key sizes. 

483 

484 Version 0.3 vault configurations use a constant key size; see 

485 [`KEY_SIZE`][]. The encryption and signing keys differ in how 

486 many rounds of PBKDF2 they use (100 and 200, respectively). 

487 

488 Danger: Insecure use of cryptography 

489 This function makes use of the insecure function 

490 [`VaultNativeConfigParser._pbkdf2`][], without any attempts 

491 at mitigating its insecurity. It further uses `_pbkdf2` 

492 with the low iteration count of 100 and 200 rounds, which is 

493 *drastically* insufficient to defend against password 

494 guessing attacks using GPUs or ASICs. We provide this 

495 function for the purpose of interoperability with existing 

496 vault installations. Do not rely on this system to keep 

497 your vault configuration secure against access by even 

498 moderately determined attackers! 

499 

500 """ 

501 with self._consistency_lock: 1ehgbfdac

502 self._encryption_key = self._pbkdf2( 1ehgbfdac

503 self._password, self.KEY_SIZE, 100 

504 ) 

505 self._signing_key = self._pbkdf2( 1ehgbfdac

506 self._password, self.KEY_SIZE, 200 

507 ) 

508 self._encryption_key_size = self._signing_key_size = self.KEY_SIZE 1ehgbfdac

509 

510 def _hmac_input(self) -> bytes: 

511 """Return the input the MAC is supposed to verify. 

512 

513 This includes hexadecimal encoding of the message payload. 

514 

515 """ 

516 return self._message.hex().lower().encode("ASCII") 1ehgbfdac

517 

518 def _make_decryptor(self) -> ciphers.CipherContext: 

519 """Return the cipher context object used for decryption. 

520 

521 This is a standard AES256-CBC cipher context using the 

522 previously derived encryption key and the IV declared in the 

523 (MAC-verified) message payload. 

524 

525 """ 

526 with self._consistency_lock: 1egbfdac

527 encryption_key = self._encryption_key 1egbfdac

528 iv = self._iv 1egbfdac

529 return ciphers.Cipher( 1egbfdac

530 algorithms.AES256(encryption_key), modes.CBC(iv) 

531 ).decryptor() 

532 

533 

534class VaultNativeV02ConfigParser(VaultNativeConfigParser): 

535 """A parser for vault's native configuration format (v0.2). 

536 

537 This is the classic configuration format. Compared to v0.3, it 

538 contains an (accidental) API misuse for the generation of the master 

539 keys, a low-entropy method of generating initialization vectors for 

540 the AES-CBC encryption step, and extra layers of base64 encoding. 

541 Because of these significantly weakened confidentiality guarantees, 

542 v0.2 configurations should be upgraded to at least v0.3 as soon as 

543 possible. 

544 

545 Warning: 

546 Non-public class, provided for didactical and educational 

547 purposes only. Subject to change without notice, including 

548 removal. 

549 

550 """ 

551 

552 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 

553 super().__init__(*args, **kwargs) 1jbac

554 self._iv_size = 16 1jbac

555 self._mac_size = 64 1jbac

556 

557 def _parse_contents(self) -> None: 

558 """Parse the contents into IV, payload and MAC. 

559 

560 Like the base class implementation, this operates on, and sets, 

561 multiple internal attributes of the parser. In version 0.2 

562 vault configurations, the payload is encoded in base64 and the 

563 message tag (MAC) is encoded in hexadecimal, so unlike the base 

564 class implementation, we additionally decode the payload and the 

565 MAC. 

566 

567 Raises: 

568 ValueError: 

569 The configuration file contents are clearly truncated, 

570 or the payload or the message tag cannot be decoded 

571 properly. 

572 

573 """ 

574 with self._consistency_lock: 1jbac

575 super()._parse_contents() 1jbac

576 payload = self._payload = base64.standard_b64decode(self._payload) 1bac

577 message_tag = self._message_tag = bytes.fromhex( 1jbac

578 self._message_tag.decode("ASCII") 

579 ) 

580 logger.debug( 1bac

581 _msg.TranslatedString( 

582 _msg.DebugMsgTemplate.VAULT_NATIVE_V02_PAYLOAD_MAC_POSTPROCESSING, 

583 payload=_h(payload), 

584 mac=_h(message_tag), 

585 ), 

586 ) 

587 

588 def _generate_keys(self) -> None: 

589 """Derive the signing and encryption keys, and set the key sizes. 

590 

591 Version 0.2 vault configurations use 8-byte encryption keys and 

592 16-byte signing keys, including the hexadecimal encoding. They 

593 both use 16 rounds of PBKDF2. This is due to an oversight in 

594 vault, where the author mistakenly supplied the intended 

595 iteration count as the key size, and the key size as the 

596 iteration count. 

597 

598 Danger: Insecure use of cryptography 

599 This function makes use of the insecure function 

600 [`VaultNativeConfigParser._pbkdf2`][], without any attempts 

601 at mitigating its insecurity. It further uses `_pbkdf2` 

602 with the low iteration count of 16 rounds, which is 

603 *drastically* insufficient to defend against password 

604 guessing attacks using GPUs or ASICs, and generates the 

605 encryption key as a truncation of the signing key. We 

606 provide this function for the purpose of interoperability 

607 with existing vault installations. Do not rely on this 

608 system to keep your vault configuration secure against 

609 access by even moderately determined attackers! 

610 

611 """ 

612 with self._consistency_lock: 1bac

613 self._encryption_key = self._pbkdf2(self._password, 8, 16) 1bac

614 self._signing_key = self._pbkdf2(self._password, 16, 16) 1bac

615 self._encryption_key_size = 8 1bac

616 self._signing_key_size = 16 1bac

617 

618 def _hmac_input(self) -> bytes: 

619 """Return the input the MAC is supposed to verify. 

620 

621 This includes hexadecimal encoding of the message payload. 

622 

623 """ 

624 return base64.standard_b64encode(self._message) 1bac

625 

626 @staticmethod 

627 def _evp_bytestokey_md5_one_iteration_no_salt( 

628 data: bytes, key_size: int, iv_size: int 

629 ) -> tuple[bytes, bytes]: 

630 """Reimplement OpenSSL's `EVP_BytesToKey` with fixed parameters. 

631 

632 `EVP_BytesToKey` in general is a key derivation function, 

633 i.e., a function that derives key material from an input 

634 byte string. `EVP_BytesToKey` conceptually splits the 

635 derived key material into an encryption key and an 

636 initialization vector (IV). 

637 

638 Note: Algorithm description 

639 `EVP_BytesToKey` takes an input byte string, two output 

640 size (encryption key size and IV size), a message digest 

641 function, a salt value and an iteration count. The 

642 derived key material is calculated in blocks, each of 

643 which is the output of (iterated application of) the 

644 message digest function. The input to the message 

645 digest function is the concatenation of the previous 

646 block (if any) with the input byte string and the salt 

647 value (if any): 

648 

649 ~~~~ python 

650 data = block_input = b"".join([previous_block, input_string, salt]) 

651 for i in range(iteration_count): 

652 data = message_digest(data) 

653 block = data 

654 ~~~~ 

655 

656 We use as many blocks as are necessary to cover the 

657 total output byte string size. The first few bytes 

658 (dictated by the encryption key size) form the 

659 encryption key, the other bytes (dictated by the IV 

660 size) form the IV. 

661 

662 We implement exactly the subset of `EVP_BytesToKey` that the 

663 Node.js `crypto` library (v21 series and older) uses in its 

664 implementation of `crypto.createCipher("aes256", password)`. 

665 Specifically, the message digest function is fixed to MD5, 

666 the salt is always empty, and the iteration count is fixed 

667 at one. 

668 

669 

670 Returns: 

671 A 2-tuple containing the derived encryption key and the 

672 derived initialization vector. 

673 

674 Danger: Insecure use of cryptography 

675 This function reimplements the OpenSSL function 

676 `EVP_BytesToKey`, which generates cryptographically weak 

677 keys, without any attempts at mitigating its insecurity. We 

678 provide this function for the purpose of interoperability 

679 with existing vault installations. Do not rely on this 

680 system to keep your vault configuration secure against 

681 access by even moderately determined attackers! 

682 

683 """ 

684 total_size = key_size + iv_size 1bac

685 buffer = bytearray() 1bac

686 last_block = b"" 1bac

687 salt = b"" 1bac

688 logger.debug( 1bac

689 _msg.TranslatedString( 

690 _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_INIT, 

691 data=_h(data), 

692 salt=_h(salt), 

693 key_size=key_size, 

694 iv_size=iv_size, 

695 buffer_length=len(buffer), 

696 buffer=_h(buffer), 

697 ), 

698 ) 

699 while len(buffer) < total_size: 1bac

700 with warnings.catch_warnings(): 1bac

701 warnings.simplefilter( 1bac

702 "ignore", crypt_utils.CryptographyDeprecationWarning 

703 ) 

704 block = hashes.Hash(hashes.MD5()) 1bac

705 block.update(last_block) 1bac

706 block.update(data) 1bac

707 block.update(salt) 1bac

708 last_block = block.finalize() 1bac

709 buffer.extend(last_block) 1bac

710 logger.debug( 1bac

711 _msg.TranslatedString( 

712 _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_ROUND, 

713 buffer_length=len(buffer), 

714 buffer=_h(buffer), 

715 ), 

716 ) 

717 logger.debug( 1bac

718 _msg.TranslatedString( 

719 _msg.DebugMsgTemplate.VAULT_NATIVE_EVP_BYTESTOKEY_RESULT, 

720 enc_key=_h(buffer[:key_size]), 

721 iv=_h(buffer[key_size:total_size]), 

722 ), 

723 ) 

724 return bytes(buffer[:key_size]), bytes(buffer[key_size:total_size]) 1bac

725 

726 def _make_decryptor(self) -> ciphers.CipherContext: 

727 """Return the cipher context object used for decryption. 

728 

729 This is a standard AES256-CBC cipher context. The encryption key 

730 and the IV are derived via the OpenSSL `EVP_BytesToKey` function 

731 (using MD5, no salt, and one iteration). This is what the 

732 Node.js `crypto` library (v21 series and older) used in its 

733 implementation of `crypto.createCipher("aes256", password)`. 

734 

735 Danger: Insecure use of cryptography 

736 This function makes use of (an implementation of) the 

737 OpenSSL function `EVP_BytesToKey`, which generates 

738 cryptographically weak keys, without any attempts at 

739 mitigating its insecurity. We provide this function for the 

740 purpose of interoperability with existing vault 

741 installations. Do not rely on this system to keep your 

742 vault configuration secure against access by even moderately 

743 determined attackers! 

744 

745 """ 

746 with self._consistency_lock: 1bac

747 data = base64.standard_b64encode(self._iv + self._encryption_key) 1bac

748 encryption_key, iv = self._evp_bytestokey_md5_one_iteration_no_salt( 1bac

749 data, key_size=32, iv_size=16 

750 ) 

751 return ciphers.Cipher( 1bac

752 algorithms.AES256(encryption_key), modes.CBC(iv) 

753 ).decryptor() 

754 

755 

756if __name__ == "__main__": 

757 import os 

758 

759 logging.basicConfig(level=("DEBUG" if os.getenv("DEBUG") else "WARNING")) 

760 with exporter.get_vault_path().open("rb") as infile: 

761 contents = base64.standard_b64decode(infile.read()) 

762 password = exporter.get_vault_key() 

763 try: 

764 config = VaultNativeV03ConfigParser(contents, password)() 

765 except ValueError: 

766 config = VaultNativeV02ConfigParser(contents, password)() 

767 print(json.dumps(config, indent=2, sort_keys=True)) # noqa: T201