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 |