Marco Ricci commited on 2025-01-19 21:10:38
Zeige 2 geänderte Dateien mit 251 Einfügungen und 0 Löschungen.
Add missing docstrings, and make the (private) methods visible.
| ... | ... |
@@ -6,3 +6,15 @@ |
| 6 | 6 |
|
| 7 | 7 |
::: derivepassphrase.exporter.vault_native |
| 8 | 8 |
heading_level: 1 |
| 9 |
+ filters: |
|
| 10 |
+ - "^[A-Za-z0-9]" |
|
| 11 |
+ - "^__[a-zA-Z0-9_-]+__" |
|
| 12 |
+ - "^_pbkdf2$" |
|
| 13 |
+ - "^_parse_contents$" |
|
| 14 |
+ - "^_derive_keys$" |
|
| 15 |
+ - "^_generate_keys$" |
|
| 16 |
+ - "^_check_signature$" |
|
| 17 |
+ - "^_hmac_input$" |
|
| 18 |
+ - "^_decrypt_payload$" |
|
| 19 |
+ - "^_make_decryptor$" |
|
| 20 |
+ - "^_evp_bytestokey_md5_one_iteration_no_salt$" |
| ... | ... |
@@ -170,6 +170,34 @@ class VaultNativeConfigParser(abc.ABC): |
| 170 | 170 |
def _pbkdf2( |
| 171 | 171 |
password: str | Buffer, key_size: int, iterations: int |
| 172 | 172 |
) -> bytes: |
| 173 |
+ """Generate a key from a password. |
|
| 174 |
+ |
|
| 175 |
+ Uses PBKDF2 with HMAC-SHA1, with the vault UUID as a fixed salt |
|
| 176 |
+ value. |
|
| 177 |
+ |
|
| 178 |
+ Args: |
|
| 179 |
+ password: |
|
| 180 |
+ The password from which to derive the key. |
|
| 181 |
+ key_size: |
|
| 182 |
+ The size of the output string. The effective key size |
|
| 183 |
+ (in bytes) is thus half of this output string size. |
|
| 184 |
+ iterations: |
|
| 185 |
+ The PBKDF2 iteration count. |
|
| 186 |
+ |
|
| 187 |
+ Returns: |
|
| 188 |
+ The PBKDF2-derived key, encoded as a lowercase ASCII |
|
| 189 |
+ hexadecimal string. |
|
| 190 |
+ |
|
| 191 |
+ Danger: Insecure use of cryptography |
|
| 192 |
+ This function is insecure because it uses a fixed salt |
|
| 193 |
+ value, which is not secure against rainbow tables. It is |
|
| 194 |
+ further difficult to use because the effective key size is |
|
| 195 |
+ only half as large as the "size" parameter (output string |
|
| 196 |
+ size). Finally, though the use of SHA-1 in HMAC per se is |
|
| 197 |
+ not known to be insecure, SHA-1 is known not to be |
|
| 198 |
+ collision-resistant. |
|
| 199 |
+ |
|
| 200 |
+ """ |
|
| 173 | 201 |
if isinstance(password, str): |
| 174 | 202 |
password = password.encode('utf-8')
|
| 175 | 203 |
raw_key = pbkdf2.PBKDF2HMAC( |
| ... | ... |
@@ -194,6 +222,16 @@ class VaultNativeConfigParser(abc.ABC): |
| 194 | 222 |
return result_key |
| 195 | 223 |
|
| 196 | 224 |
def _parse_contents(self) -> None: |
| 225 |
+ """Parse the contents into IV, payload and MAC. |
|
| 226 |
+ |
|
| 227 |
+ This operates on, and sets, multiple internal attributes of the |
|
| 228 |
+ parser. |
|
| 229 |
+ |
|
| 230 |
+ Raises: |
|
| 231 |
+ ValueError: |
|
| 232 |
+ The configuration file contents are clearly truncated. |
|
| 233 |
+ |
|
| 234 |
+ """ |
|
| 197 | 235 |
logger.info( |
| 198 | 236 |
_msg.TranslatedString( |
| 199 | 237 |
_msg.InfoMsgTemplate.VAULT_NATIVE_PARSING_IV_PAYLOAD_MAC, |
| ... | ... |
@@ -224,6 +262,12 @@ class VaultNativeConfigParser(abc.ABC): |
| 224 | 262 |
) |
| 225 | 263 |
|
| 226 | 264 |
def _derive_keys(self) -> None: |
| 265 |
+ """Derive the signing and encryption keys. |
|
| 266 |
+ |
|
| 267 |
+ This is a bookkeeping method. The actual work is done in |
|
| 268 |
+ [`_generate_keys`][]. |
|
| 269 |
+ |
|
| 270 |
+ """ |
|
| 227 | 271 |
logger.info( |
| 228 | 272 |
_msg.TranslatedString( |
| 229 | 273 |
_msg.InfoMsgTemplate.VAULT_NATIVE_DERIVING_KEYS, |
| ... | ... |
@@ -239,9 +283,29 @@ class VaultNativeConfigParser(abc.ABC): |
| 239 | 283 |
|
| 240 | 284 |
@abc.abstractmethod |
| 241 | 285 |
def _generate_keys(self) -> None: |
| 286 |
+ """Derive the signing and encryption keys, and set the key sizes. |
|
| 287 |
+ |
|
| 288 |
+ Subclasses must override this, as the derivation system is |
|
| 289 |
+ version-specific. The default implementation raises an error. |
|
| 290 |
+ |
|
| 291 |
+ Raises: |
|
| 292 |
+ AssertionError: |
|
| 293 |
+ There is no default implementation. |
|
| 294 |
+ |
|
| 295 |
+ """ |
|
| 242 | 296 |
raise AssertionError |
| 243 | 297 |
|
| 244 | 298 |
def _check_signature(self) -> None: |
| 299 |
+ """Check for a valid MAC on the encrypted vault configuration. |
|
| 300 |
+ |
|
| 301 |
+ The MAC uses HMAC-SHA1, and thus is 32 bytes long, before |
|
| 302 |
+ encoding. |
|
| 303 |
+ |
|
| 304 |
+ Raises: |
|
| 305 |
+ ValueError: |
|
| 306 |
+ The MAC is invalid. |
|
| 307 |
+ |
|
| 308 |
+ """ |
|
| 245 | 309 |
logger.info( |
| 246 | 310 |
_msg.TranslatedString( |
| 247 | 311 |
_msg.InfoMsgTemplate.VAULT_NATIVE_CHECKING_MAC, |
| ... | ... |
@@ -265,9 +329,26 @@ class VaultNativeConfigParser(abc.ABC): |
| 265 | 329 |
|
| 266 | 330 |
@abc.abstractmethod |
| 267 | 331 |
def _hmac_input(self) -> bytes: |
| 332 |
+ """Return the input the MAC is supposed to verify. |
|
| 333 |
+ |
|
| 334 |
+ Subclasses must override this, as the MAC-attested data is |
|
| 335 |
+ version-specific. The default implementation raises an error. |
|
| 336 |
+ |
|
| 337 |
+ Raises: |
|
| 338 |
+ AssertionError: |
|
| 339 |
+ There is no default implementation. |
|
| 340 |
+ |
|
| 341 |
+ """ |
|
| 268 | 342 |
raise AssertionError |
| 269 | 343 |
|
| 270 | 344 |
def _decrypt_payload(self) -> Any: # noqa: ANN401 |
| 345 |
+ """Return the decrypted vault configuration. |
|
| 346 |
+ |
|
| 347 |
+ Requires [`_parse_contents`][] and [`_derive_keys`][] to have |
|
| 348 |
+ run, and relies on [`_check_signature`][] for tampering |
|
| 349 |
+ detection. |
|
| 350 |
+ |
|
| 351 |
+ """ |
|
| 271 | 352 |
logger.info( |
| 272 | 353 |
_msg.TranslatedString( |
| 273 | 354 |
_msg.InfoMsgTemplate.VAULT_NATIVE_DECRYPTING_CONTENTS, |
| ... | ... |
@@ -297,6 +378,16 @@ class VaultNativeConfigParser(abc.ABC): |
| 297 | 378 |
|
| 298 | 379 |
@abc.abstractmethod |
| 299 | 380 |
def _make_decryptor(self) -> ciphers.CipherContext: |
| 381 |
+ """Return the cipher context object used for decryption. |
|
| 382 |
+ |
|
| 383 |
+ Subclasses must override this, as the cipher setup is |
|
| 384 |
+ version-specific. The default implementation raises an error. |
|
| 385 |
+ |
|
| 386 |
+ Raises: |
|
| 387 |
+ AssertionError: |
|
| 388 |
+ There is no default implementation. |
|
| 389 |
+ |
|
| 390 |
+ """ |
|
| 300 | 391 |
raise AssertionError |
| 301 | 392 |
|
| 302 | 393 |
|
| ... | ... |
@@ -313,6 +404,11 @@ class VaultNativeV03ConfigParser(VaultNativeConfigParser): |
| 313 | 404 |
""" |
| 314 | 405 |
|
| 315 | 406 |
KEY_SIZE = 32 |
| 407 |
+ """ |
|
| 408 |
+ Key size for both the encryption and the signing key, including the |
|
| 409 |
+ encoding as a hexadecimal string. (The effective cryptographic |
|
| 410 |
+ strength is half of this value.) |
|
| 411 |
+ """ |
|
| 316 | 412 |
|
| 317 | 413 |
def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 |
| 318 | 414 |
super().__init__(*args, **kwargs) |
| ... | ... |
@@ -320,14 +416,45 @@ class VaultNativeV03ConfigParser(VaultNativeConfigParser): |
| 320 | 416 |
self._mac_size = 32 |
| 321 | 417 |
|
| 322 | 418 |
def _generate_keys(self) -> None: |
| 419 |
+ """Derive the signing and encryption keys, and set the key sizes. |
|
| 420 |
+ |
|
| 421 |
+ Version 0.3 vault configurations use a constant key size; see |
|
| 422 |
+ [`KEY_SIZE`][]. The encryption and signing keys differ in how |
|
| 423 |
+ many rounds of PBKDF2 they use (100 and 200, respectively). |
|
| 424 |
+ |
|
| 425 |
+ Danger: Insecure use of cryptography |
|
| 426 |
+ This function makes use of the insecure function |
|
| 427 |
+ [`VaultNativeConfigParser._pbkdf2`][], without any attempts |
|
| 428 |
+ at mitigating its insecurity. It further uses `_pbkdf2` |
|
| 429 |
+ with the low iteration count of 100 and 200 rounds, which is |
|
| 430 |
+ *drastically* insufficient to defend against password |
|
| 431 |
+ guessing attacks using GPUs or ASICs. We provide this |
|
| 432 |
+ function for the purpose of interoperability with existing |
|
| 433 |
+ vault installations. Do not rely on this system to keep |
|
| 434 |
+ your vault configuration secure against access by even |
|
| 435 |
+ moderately determined attackers! |
|
| 436 |
+ |
|
| 437 |
+ """ |
|
| 323 | 438 |
self._encryption_key = self._pbkdf2(self._password, self.KEY_SIZE, 100) |
| 324 | 439 |
self._signing_key = self._pbkdf2(self._password, self.KEY_SIZE, 200) |
| 325 | 440 |
self._encryption_key_size = self._signing_key_size = self.KEY_SIZE |
| 326 | 441 |
|
| 327 | 442 |
def _hmac_input(self) -> bytes: |
| 443 |
+ """Return the input the MAC is supposed to verify. |
|
| 444 |
+ |
|
| 445 |
+ This includes hexadecimal encoding of the message payload. |
|
| 446 |
+ |
|
| 447 |
+ """ |
|
| 328 | 448 |
return self._message.hex().lower().encode('ASCII')
|
| 329 | 449 |
|
| 330 | 450 |
def _make_decryptor(self) -> ciphers.CipherContext: |
| 451 |
+ """Return the cipher context object used for decryption. |
|
| 452 |
+ |
|
| 453 |
+ This is a standard AES256-CBC cipher context using the |
|
| 454 |
+ previously derived encryption key and the IV declared in the |
|
| 455 |
+ (MAC-verified) message payload. |
|
| 456 |
+ |
|
| 457 |
+ """ |
|
| 331 | 458 |
return ciphers.Cipher( |
| 332 | 459 |
algorithms.AES256(self._encryption_key), modes.CBC(self._iv) |
| 333 | 460 |
).decryptor() |
| ... | ... |
@@ -357,6 +484,22 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser): |
| 357 | 484 |
self._mac_size = 64 |
| 358 | 485 |
|
| 359 | 486 |
def _parse_contents(self) -> None: |
| 487 |
+ """Parse the contents into IV, payload and MAC. |
|
| 488 |
+ |
|
| 489 |
+ Like the base class implementation, this operates on, and sets, |
|
| 490 |
+ multiple internal attributes of the parser. In version 0.2 |
|
| 491 |
+ vault configurations, the payload is encoded in base64 and the |
|
| 492 |
+ message tag (MAC) is encoded in hexadecimal, so unlike the base |
|
| 493 |
+ class implementation, we additionally decode the payload and the |
|
| 494 |
+ MAC. |
|
| 495 |
+ |
|
| 496 |
+ Raises: |
|
| 497 |
+ ValueError: |
|
| 498 |
+ The configuration file contents are clearly truncated, |
|
| 499 |
+ or the payload or the message tag cannot be decoded |
|
| 500 |
+ properly. |
|
| 501 |
+ |
|
| 502 |
+ """ |
|
| 360 | 503 |
super()._parse_contents() |
| 361 | 504 |
self._payload = base64.standard_b64decode(self._payload) |
| 362 | 505 |
self._message_tag = bytes.fromhex(self._message_tag.decode('ASCII'))
|
| ... | ... |
@@ -369,18 +512,114 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser): |
| 369 | 512 |
) |
| 370 | 513 |
|
| 371 | 514 |
def _generate_keys(self) -> None: |
| 515 |
+ """Derive the signing and encryption keys, and set the key sizes. |
|
| 516 |
+ |
|
| 517 |
+ Version 0.2 vault configurations use 8-byte encryption keys and |
|
| 518 |
+ 16-byte signing keys, including the hexadecimal encoding. They |
|
| 519 |
+ both use 16 rounds of PBKDF2. This is due to an oversight in |
|
| 520 |
+ vault, where the author mistakenly supplied the intended |
|
| 521 |
+ iteration count as the key size, and the key size as the |
|
| 522 |
+ iteration count. |
|
| 523 |
+ |
|
| 524 |
+ Danger: Insecure use of cryptography |
|
| 525 |
+ This function makes use of the insecure function |
|
| 526 |
+ [`VaultNativeConfigParser._pbkdf2`][], without any attempts |
|
| 527 |
+ at mitigating its insecurity. It further uses `_pbkdf2` |
|
| 528 |
+ with the low iteration count of 16 rounds, which is |
|
| 529 |
+ *drastically* insufficient to defend against password |
|
| 530 |
+ guessing attacks using GPUs or ASICs, and generates the |
|
| 531 |
+ encryption key as a truncation of the signing key. We |
|
| 532 |
+ provide this function for the purpose of interoperability |
|
| 533 |
+ with existing vault installations. Do not rely on this |
|
| 534 |
+ system to keep your vault configuration secure against |
|
| 535 |
+ access by even moderately determined attackers! |
|
| 536 |
+ |
|
| 537 |
+ """ |
|
| 372 | 538 |
self._encryption_key = self._pbkdf2(self._password, 8, 16) |
| 373 | 539 |
self._signing_key = self._pbkdf2(self._password, 16, 16) |
| 374 | 540 |
self._encryption_key_size = 8 |
| 375 | 541 |
self._signing_key_size = 16 |
| 376 | 542 |
|
| 377 | 543 |
def _hmac_input(self) -> bytes: |
| 544 |
+ """Return the input the MAC is supposed to verify. |
|
| 545 |
+ |
|
| 546 |
+ This includes hexadecimal encoding of the message payload. |
|
| 547 |
+ |
|
| 548 |
+ """ |
|
| 378 | 549 |
return base64.standard_b64encode(self._message) |
| 379 | 550 |
|
| 380 | 551 |
def _make_decryptor(self) -> ciphers.CipherContext: |
| 552 |
+ """Return the cipher context object used for decryption. |
|
| 553 |
+ |
|
| 554 |
+ This is a standard AES256-CBC cipher context. The encryption key |
|
| 555 |
+ and the IV are derived via the OpenSSL `EVP_BytesToKey` function |
|
| 556 |
+ (using MD5, no salt, and one iteration). This is what the |
|
| 557 |
+ Node.js `crypto` library (v21 series and older) used in its |
|
| 558 |
+ implementation of `crypto.createCipher("aes256", password)`.
|
|
| 559 |
+ |
|
| 560 |
+ Danger: Insecure use of cryptography |
|
| 561 |
+ This function makes use of (an implementation of) the |
|
| 562 |
+ OpenSSL function `EVP_BytesToKey`, which generates |
|
| 563 |
+ cryptographically weak keys, without any attempts at |
|
| 564 |
+ mitigating its insecurity. We provide this function for the |
|
| 565 |
+ purpose of interoperability with existing vault |
|
| 566 |
+ installations. Do not rely on this system to keep your |
|
| 567 |
+ vault configuration secure against access by even moderately |
|
| 568 |
+ determined attackers! |
|
| 569 |
+ |
|
| 570 |
+ """ |
|
| 571 |
+ |
|
| 381 | 572 |
def evp_bytestokey_md5_one_iteration_no_salt( |
| 382 | 573 |
data: bytes, key_size: int, iv_size: int |
| 383 | 574 |
) -> tuple[bytes, bytes]: |
| 575 |
+ """Reimplement OpenSSL's `EVP_BytesToKey` with fixed parameters. |
|
| 576 |
+ |
|
| 577 |
+ `EVP_BytesToKey` in general is a key derivation function, |
|
| 578 |
+ i.e., a function that derives key material from an input |
|
| 579 |
+ byte string. `EVP_BytesToKey` conceptually splits the |
|
| 580 |
+ derived key material into an encryption key and an |
|
| 581 |
+ initialization vector (IV). |
|
| 582 |
+ |
|
| 583 |
+ Note: Algorithm description |
|
| 584 |
+ `EVP_BytesToKey` takes an input byte string, two output |
|
| 585 |
+ size (encryption key size and IV size), a message digest |
|
| 586 |
+ function, a salt value and an iteration count. The |
|
| 587 |
+ derived key material is calculated in blocks, each of |
|
| 588 |
+ which is the output of (iterated application of) the |
|
| 589 |
+ message digest function. The input to the message |
|
| 590 |
+ digest function is the concatenation of the previous |
|
| 591 |
+ block (if any) with the input byte string and the salt |
|
| 592 |
+ value (if any): |
|
| 593 |
+ |
|
| 594 |
+ ~~~~ python |
|
| 595 |
+ |
|
| 596 |
+ data = block_input = b''.join([ |
|
| 597 |
+ previous_block, input_string, salt |
|
| 598 |
+ ]) |
|
| 599 |
+ for i in range(iteration_count): |
|
| 600 |
+ data = message_digest(data) |
|
| 601 |
+ block = data |
|
| 602 |
+ |
|
| 603 |
+ ~~~~ |
|
| 604 |
+ |
|
| 605 |
+ We use as many blocks as are necessary to cover the |
|
| 606 |
+ total output byte string size. The first few bytes |
|
| 607 |
+ (dictated by the encryption key size) form the |
|
| 608 |
+ encryption key, the other bytes (dictated by the IV |
|
| 609 |
+ size) form the IV. |
|
| 610 |
+ |
|
| 611 |
+ We implement exactly the subset of `EVP_BytesToKey` that the |
|
| 612 |
+ Node.js `crypto` library (v21 series and older) uses in its |
|
| 613 |
+ implementation of `crypto.createCipher("aes256", password)`.
|
|
| 614 |
+ Specifically, the message digest function is fixed to MD5, |
|
| 615 |
+ the salt is always empty, and the iteration count is fixed |
|
| 616 |
+ at one. |
|
| 617 |
+ |
|
| 618 |
+ Returns: |
|
| 619 |
+ A 2-tuple containing the derived encryption key and the |
|
| 620 |
+ derived initialization vector. |
|
| 621 |
+ |
|
| 622 |
+ """ |
|
| 384 | 623 |
total_size = key_size + iv_size |
| 385 | 624 |
buffer = bytearray() |
| 386 | 625 |
last_block = b'' |
| 387 | 626 |