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

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

7 The vault native formats are the configuration formats used by vault

8 v0.2 and v0.3. The configuration is stored as a single encrypted file,

9 which is encrypted and authenticated. v0.2 and v0.3 differ in some

10 details concerning key derivation and expected format of internal

11 structures, so they are *not* compatible. v0.2 additionally contains

12 cryptographic weaknesses (API misuse of a key derivation function, and

13 a low-entropy method of generating initialization vectors for CBC block

14 encryption mode) and should thus be avoided if possible.

15

16 The public interface is the [`export_vault_native_data`][] function.

17 Multiple *non-public* classes are additionally documented here for

18 didactical and educational reasons, but they are not part of the module

19 API, are subject to change without notice (including removal), and

20 should *not* be used or relied on.

21

22"""

23

24# ruff: noqa: S303

25

26 from __future__ import annotations (empty)

27

28 import abc (empty)

29 import base64 (empty)

30 import importlib (empty)

31 import json (empty)

32 import logging (empty)

33 import os (empty)

34 import pathlib (empty)

35 import threading (empty)

36 import warnings (empty)

37 from typing import TYPE_CHECKING (empty)

38

39 from derivepassphrase import exporter, vault (empty)

40 from derivepassphrase._internals import cli_messages as _msg (empty)

41

42 if TYPE_CHECKING: (empty)

43 from typing import Any

44

45 from typing_extensions import Buffer

46

47 if TYPE_CHECKING: (empty)

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

55 else:

56 try: (empty)

57 importlib.import_module('cryptography') (empty)

58 except ModuleNotFoundError as exc: (empty)

59 # For type checking only, will not actually be called, so no

60 # coverage.

61 class _DummyModule: # pragma: no cover (empty)

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

63 self.exc = exc (empty)

64

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

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

67 raise self.exc

68

69 return func (empty)

70

71 crypt_exceptions = crypt_utils = _DummyModule(exc) (empty)

72 ciphers = hashes = hmac = padding = _DummyModule(exc) (empty)

73 algorithms = modes = pbkdf2 = _DummyModule(exc) (empty)

74 STUBBED = True (empty)

75 else:

76 from cryptography import exceptions as crypt_exceptions (empty)

77 from cryptography import utils as crypt_utils (empty)

78 from cryptography.hazmat.primitives import ( (empty)

79 ciphers,

80 hashes,

81 hmac,

82 padding,

83 )

84 from cryptography.hazmat.primitives.ciphers import algorithms, modes (empty)

85 from cryptography.hazmat.primitives.kdf import pbkdf2 (empty)

86

87 STUBBED = False (empty)

88

89 __all__ = ('export_vault_native_data',) (empty)

90

91 logger = logging.getLogger(__name__) (empty)

92

93

94@exporter.register_export_vault_config_data_handler('v0.2', 'v0.3') (empty)

95 def export_vault_native_data( # noqa: D417 (empty)

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') 11 ctx 1efgbljmhado

113 if path is None: 10 ctx 1efgbljmhad

114 path = exporter.get_vault_path() 5 ctx 1efbad

115 else:

116 path = pathlib.Path(os.fsdecode(path)) 6 ctx 1gljmhd

117 with path.open('rb') as infile: 10 ctx 1efgbljmhad

118 contents = base64.standard_b64decode(infile.read()) 8 ctx 1efgbjhad

119 if key is None: 8 ctx 1efgbjhad

120 key = exporter.get_vault_key() 6 ctx 1efjhad

121 parser_class: type[VaultNativeConfigParser] | None = { 8 ctx 1efgbjhad

122 'v0.2': VaultNativeV02ConfigParser,

123 'v0.3': VaultNativeV03ConfigParser,

124 }.get(format)

125 if parser_class is None: # pragma: no cover 8 ctx 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: 8 ctx 1efgbjhad

133 return parser_class(contents, key)() 8 ctx 1efgbjhad

134 except ValueError as exc: 3 ctx 1jha

135 raise exporter.NotAVaultConfigError(path, format=format) from exc 3 ctx 1jha

136

137

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

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

140

141

142 class VaultNativeConfigParser(abc.ABC): (empty)

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

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: 10 ctx 1efgbjhadcn

181 msg = 'Password must not be empty' 1 ctx 1n

182 raise ValueError(msg) 1 ctx 1n

183 self._consistency_lock = threading.RLock() 9 ctx 1efgbjhadc

184 self._contents = bytes(contents) 9 ctx 1efgbjhadc

185 self._iv_size = 0 9 ctx 1efgbjhadc

186 self._mac_size = 0 9 ctx 1efgbjhadc

187 self._encryption_key = b'' 9 ctx 1efgbjhadc

188 self._encryption_key_size = 0 9 ctx 1efgbjhadc

189 self._signing_key = b'' 9 ctx 1efgbjhadc

190 self._signing_key_size = 0 9 ctx 1efgbjhadc

191 self._message = b'' 9 ctx 1efgbjhadc

192 self._message_tag = b'' 9 ctx 1efgbjhadc

193 self._iv = b'' 9 ctx 1efgbjhadc

194 self._payload = b'' 9 ctx 1efgbjhadc

195 self._password = password 9 ctx 1efgbjhadc

196 self._sentinel: object = object() 9 ctx 1efgbjhadc

197 self._data: Any = self._sentinel 9 ctx 1efgbjhadc

198

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

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: 9 ctx 1efgbjhadc

213 if self._data is self._sentinel: 9 ctx 1efgbjhadc

214 self._parse_contents() 9 ctx 1efgbjhadc

215 self._derive_keys() 8 ctx 1efgbhadc

216 self._check_signature() 8 ctx 1efgbhadc

217 self._data = self._decrypt_payload() 7 ctx 1efgbadc

218 return self._data 9 ctx 1efgbjhadc

219

220 @staticmethod (empty)

221 def _pbkdf2( (empty)

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): 9 ctx 1efgbhkadc

253 password = password.encode('utf-8') 2 ctx 1dc

254 raw_key = pbkdf2.PBKDF2HMAC( 9 ctx 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') 9 ctx 1efgbhkadc

261 logger.debug( 9 ctx 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 9 ctx 1efgbhkadc

274

275 def _parse_contents(self) -> None: (empty)

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( 9 ctx 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]: 9 ctx 1efgbjhadc

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

294

295 with self._consistency_lock: 9 ctx 1efgbjhadc

296 contents = self._contents 9 ctx 1efgbjhadc

297 iv_size = self._iv_size 9 ctx 1efgbjhadc

298 mac_size = self._mac_size 9 ctx 1efgbjhadc

299

300 if len(contents) < iv_size + 16 + mac_size: 9 ctx 1efgbjhadc

301 msg = 'Invalid vault configuration file: file is truncated' 1 ctx 1j

302 raise ValueError(msg) 1 ctx 1j

303

304 cutpos1 = len(contents) - mac_size 8 ctx 1efgbhadc

305 cutpos2 = iv_size 8 ctx 1efgbhadc

306 message, message_tag = cut(contents, cutpos1) 8 ctx 1efgbhadc

307 iv, payload = cut(message, cutpos2) 8 ctx 1efgbhadc

308

309 self._message = message 8 ctx 1efgbhadc

310 self._message_tag = message_tag 8 ctx 1efgbhadc

311 self._iv = iv 8 ctx 1efgbhadc

312 self._payload = payload 9 ctx 1efgbjhadc

313

314 logger.debug( 8 ctx 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: (empty)

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( 8 ctx 1efgbhadc

332 _msg.TranslatedString(

333 _msg.InfoMsgTemplate.VAULT_NATIVE_DERIVING_KEYS,

334 ),

335 )

336 with self._consistency_lock: 8 ctx 1efgbhadc

337 self._generate_keys() 8 ctx 1efgbhadc

338 assert len(self._encryption_key) == self._encryption_key_size, ( 8 ctx 1efgbhadc

339 'Derived encryption key is invalid'

340 )

341 assert len(self._signing_key) == self._signing_key_size, ( 8 ctx 1efgbhadc

342 'Derived signing key is invalid'

343 )

344

345 @abc.abstractmethod (empty)

346 def _generate_keys(self) -> None: (empty)

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

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( 8 ctx 1efgbhadc

371 _msg.TranslatedString(

372 _msg.InfoMsgTemplate.VAULT_NATIVE_CHECKING_MAC,

373 ),

374 )

375 with self._consistency_lock: 8 ctx 1efgbhadc

376 mac = hmac.HMAC(self._signing_key, hashes.SHA256()) 8 ctx 1efgbhadc

377 mac_input = self._hmac_input() 8 ctx 1efgbhadc

378 mac_expected = self._message_tag 8 ctx 1efgbhadc

379 logger.debug( 8 ctx 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) 8 ctx 1efgbhadc

387 try: 8 ctx 1efgbhadc

388 mac.verify(mac_expected) 8 ctx 1efgbhadc

389 except crypt_exceptions.InvalidSignature: 2 ctx 1ha

390 msg = 'File does not contain a valid signature' 2 ctx 1ha

391 raise ValueError(msg) from None 2 ctx 1ha

392

393 @abc.abstractmethod (empty)

394 def _hmac_input(self) -> bytes: (empty)

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

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( 7 ctx 1efgbadc

416 _msg.TranslatedString(

417 _msg.InfoMsgTemplate.VAULT_NATIVE_DECRYPTING_CONTENTS,

418 ),

419 )

420 with self._consistency_lock: 7 ctx 1efgbadc

421 payload = self._payload 7 ctx 1efgbadc

422 iv_size = self._iv_size 7 ctx 1efgbadc

423 decryptor = self._make_decryptor() 7 ctx 1efgbadc

424 padded_plaintext = bytearray() 7 ctx 1efgbadc

425 padded_plaintext.extend(decryptor.update(payload)) 7 ctx 1efgbadc

426 padded_plaintext.extend(decryptor.finalize()) 7 ctx 1efgbadc

427 logger.debug( 7 ctx 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() 7 ctx 1efgbadc

434 plaintext = bytearray() 7 ctx 1efgbadc

435 plaintext.extend(unpadder.update(padded_plaintext)) 7 ctx 1efgbadc

436 plaintext.extend(unpadder.finalize()) 7 ctx 1efgbadc

437 logger.debug( 7 ctx 1efgbadc

438 _msg.TranslatedString(

439 _msg.DebugMsgTemplate.VAULT_NATIVE_PLAINTEXT,

440 contents=_h(plaintext),

441 ),

442 )

443 return json.loads(plaintext) 7 ctx 1efgbadc

444

445 @abc.abstractmethod (empty)

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

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

460 class VaultNativeV03ConfigParser(VaultNativeConfigParser): (empty)

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

473 """ (empty)

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

480 super().__init__(*args, **kwargs) 10 ctx 1efgbjhadcn

481 self._iv_size = 16 9 ctx 1efgbjhadc

482 self._mac_size = 32 9 ctx 1efgbjhadc

483

484 def _generate_keys(self) -> None: (empty)

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: 8 ctx 1efgbhadc

505 self._encryption_key = self._pbkdf2( 8 ctx 1efgbhadc

506 self._password, self.KEY_SIZE, 100

507 )

508 self._signing_key = self._pbkdf2( 8 ctx 1efgbhadc

509 self._password, self.KEY_SIZE, 200

510 )

511 self._encryption_key_size = self._signing_key_size = self.KEY_SIZE 8 ctx 1efgbhadc

512

513 def _hmac_input(self) -> bytes: (empty)

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') 8 ctx 1efgbhadc

520

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

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: 7 ctx 1efgbadc

530 encryption_key = self._encryption_key 7 ctx 1efgbadc

531 iv = self._iv 7 ctx 1efgbadc

532 return ciphers.Cipher( 7 ctx 1efgbadc

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

534 ).decryptor()

535

536

537 class VaultNativeV02ConfigParser(VaultNativeConfigParser): (empty)

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

556 super().__init__(*args, **kwargs) 4 ctx 1bjac

557 self._iv_size = 16 4 ctx 1bjac

558 self._mac_size = 64 4 ctx 1bjac

559

560 def _parse_contents(self) -> None: (empty)

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: 4 ctx 1bjac

578 super()._parse_contents() 4 ctx 1bjac

579 payload = self._payload = base64.standard_b64decode(self._payload) 3 ctx 1bac

580 message_tag = self._message_tag = bytes.fromhex( 4 ctx 1bjac

581 self._message_tag.decode('ASCII')

582 )

583 logger.debug( 3 ctx 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: (empty)

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: 3 ctx 1bac

616 self._encryption_key = self._pbkdf2(self._password, 8, 16) 3 ctx 1bac

617 self._signing_key = self._pbkdf2(self._password, 16, 16) 3 ctx 1bac

618 self._encryption_key_size = 8 3 ctx 1bac

619 self._signing_key_size = 16 3 ctx 1bac

620

621 def _hmac_input(self) -> bytes: (empty)

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) 3 ctx 1bac

628

629 @staticmethod (empty)

630 def _evp_bytestokey_md5_one_iteration_no_salt( (empty)

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 3 ctx 1bac

688 buffer = bytearray() 3 ctx 1bac

689 last_block = b'' 3 ctx 1bac

690 salt = b'' 3 ctx 1bac

691 logger.debug( 3 ctx 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: 3 ctx 1bac

703 with warnings.catch_warnings(): 3 ctx 1bac

704 warnings.simplefilter( 3 ctx 1bac

705 'ignore', crypt_utils.CryptographyDeprecationWarning

706 )

707 block = hashes.Hash(hashes.MD5()) 3 ctx 1bac

708 block.update(last_block) 3 ctx 1bac

709 block.update(data) 3 ctx 1bac

710 block.update(salt) 3 ctx 1bac

711 last_block = block.finalize() 3 ctx 1bac

712 buffer.extend(last_block) 3 ctx 1bac

713 logger.debug( 3 ctx 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( 3 ctx 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]) 3 ctx 1bac

728

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

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: 3 ctx 1bac

750 data = base64.standard_b64encode(self._iv + self._encryption_key) 3 ctx 1bac

751 encryption_key, iv = self._evp_bytestokey_md5_one_iteration_no_salt( 3 ctx 1bac

752 data, key_size=32, iv_size=16

753 )

754 return ciphers.Cipher( 3 ctx 1bac

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

756 ).decryptor()

757

758

759 if __name__ == '__main__': (empty)

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