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

1# SPDX-FileCopyrightText: 2025 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 # 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 

64 

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

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

67 raise self.exc 

68 

69 return func 

70 

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 

86 

87 STUBBED = False 

88 

89__all__ = ('export_vault_native_data',) 

90 

91logger = logging.getLogger(__name__) 

92 

93 

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. 

102 

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

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

105 

106 Other Args: 

107 format: 

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

109 

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

136 

137 

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

139 return '<{}>'.format(memoryview(bs).hex(' ')) 1efgbhadc

140 

141 

142class VaultNativeConfigParser(abc.ABC): 

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

144 

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. 

148 

149 """ 

150 

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

152 """Initialize the parser. 

153 

154 Args: 

155 contents: 

156 The binary contents of the encrypted configuration file. 

157 

158 Note: On disk, these are usually stored in 

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

160 here. 

161 

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. 

166 

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

168 string is used as the binary password. 

169 

170 Raises: 

171 ValueError: 

172 The password must not be empty. 

173 

174 Warning: 

175 Non-public class, provided for didactical and educational 

176 purposes only. Subject to change without notice, including 

177 removal. 

178 

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

198 

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

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

201 

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

210 

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

219 

220 @staticmethod 

221 def _pbkdf2( 

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

223 ) -> bytes: 

224 """Generate a key from a password. 

225 

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

227 salt value. 

228 

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. 

237 

238 Returns: 

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

240 hexadecimal string. 

241 

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. 

250 

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

274 

275 def _parse_contents(self) -> None: 

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

277 

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

279 parser. 

280 

281 Raises: 

282 ValueError: 

283 The configuration file contents are clearly truncated. 

284 

285 """ 

286 logger.info( 1efgbjhadc

287 _msg.TranslatedString( 

288 _msg.InfoMsgTemplate.VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC, 

289 ), 

290 ) 

291 

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

293 return buffer[:cutpoint], buffer[cutpoint:] 1efgbhadc

294 

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

299 

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

303 

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

308 

309 self._message = message 1efgbhadc

310 self._message_tag = message_tag 1efgbhadc

311 self._iv = iv 1efgbhadc

312 self._payload = payload 1efgbjhadc

313 

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 ) 

323 

324 def _derive_keys(self) -> None: 

325 """Derive the signing and encryption keys. 

326 

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

328 [`_generate_keys`][]. 

329 

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 ) 

344 

345 @abc.abstractmethod 

346 def _generate_keys(self) -> None: 

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

348 

349 Subclasses must override this, as the derivation system is 

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

351 

352 Raises: 

353 AssertionError: 

354 There is no default implementation. 

355 

356 """ 

357 raise AssertionError 

358 

359 def _check_signature(self) -> None: 

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

361 

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

363 encoding. 

364 

365 Raises: 

366 ValueError: 

367 The MAC is invalid. 

368 

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

392 

393 @abc.abstractmethod 

394 def _hmac_input(self) -> bytes: 

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

396 

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

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

399 

400 Raises: 

401 AssertionError: 

402 There is no default implementation. 

403 

404 """ 

405 raise AssertionError 

406 

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

408 """Return the decrypted vault configuration. 

409 

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

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

412 detection. 

413 

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

444 

445 @abc.abstractmethod 

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

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

448 

449 Subclasses must override this, as the cipher setup is 

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

451 

452 Raises: 

453 AssertionError: 

454 There is no default implementation. 

455 

456 """ 

457 raise AssertionError 

458 

459 

460class VaultNativeV03ConfigParser(VaultNativeConfigParser): 

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

462 

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

464 

465 Warning: 

466 Non-public class, provided for didactical and educational 

467 purposes only. Subject to change without notice, including 

468 removal. 

469 

470 """ 

471 

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 """ 

478 

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

483 

484 def _generate_keys(self) -> None: 

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

486 

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

490 

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! 

502 

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

512 

513 def _hmac_input(self) -> bytes: 

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

515 

516 This includes hexadecimal encoding of the message payload. 

517 

518 """ 

519 return self._message.hex().lower().encode('ASCII') 1efgbhadc

520 

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

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

523 

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. 

527 

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

535 

536 

537class VaultNativeV02ConfigParser(VaultNativeConfigParser): 

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

539 

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. 

547 

548 Warning: 

549 Non-public class, provided for didactical and educational 

550 purposes only. Subject to change without notice, including 

551 removal. 

552 

553 """ 

554 

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

559 

560 def _parse_contents(self) -> None: 

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

562 

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. 

569 

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. 

575 

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 ) 

590 

591 def _generate_keys(self) -> None: 

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

593 

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. 

600 

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! 

613 

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

620 

621 def _hmac_input(self) -> bytes: 

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

623 

624 This includes hexadecimal encoding of the message payload. 

625 

626 """ 

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

628 

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. 

634 

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

640 

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

651 

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 ~~~~ 

658 

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. 

664 

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. 

671 

672 

673 Returns: 

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

675 derived initialization vector. 

676 

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! 

685 

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

728 

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

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

731 

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)`. 

737 

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! 

747 

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

757 

758 

759if __name__ == '__main__': 

760 import os 

761 

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