fix(crypto): correct HKDF key stretching to use HKDF-expand with proper info strings

This commit is contained in:
2026-08-26 14:40:28 +02:00
parent 6f590a219b
commit ea7f126e73
2 changed files with 48 additions and 16 deletions
+10
View File
@@ -252,10 +252,20 @@ class BitwardenClient:
# Get encrypted user key # Get encrypted user key
enc_user_key = result.get("Key") enc_user_key = result.get("Key")
_debug(f"[LOGIN] enc_user_key present: {enc_user_key is not None}")
if enc_user_key: if enc_user_key:
_debug(f"[LOGIN] enc_user_key (first 40): {enc_user_key[:40]}...")
_debug(f"[LOGIN] attempting to decrypt user key with master_key...")
try:
self.enc_key, self.mac_key = self.crypto.decrypt_user_key( self.enc_key, self.mac_key = self.crypto.decrypt_user_key(
enc_user_key, master_key enc_user_key, master_key
) )
_debug(f"[LOGIN] user key decrypted successfully!")
_debug(f"[LOGIN] enc_key (first 8 b64): {b64encode(self.enc_key[:8]).decode()}")
_debug(f"[LOGIN] mac_key (first 8 b64): {b64encode(self.mac_key[:8]).decode()}")
except Exception as e:
_debug(f"[LOGIN] user key decryption FAILED: {e}")
raise
return { return {
"success": True, "success": True,
"master_key": master_key, "master_key": master_key,
+35 -13
View File
@@ -4,11 +4,24 @@ Handles KDF (PBKDF2, Argon2), AES-CBC-256, HMAC, HKDF, and vault decryption.
""" """
import hashlib import hashlib
import hmac import hmac
import logging
import os import os
import struct import struct
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from typing import Optional, Tuple from typing import Optional, Tuple
_debug_log = "/tmp/decky-vaultwarden-debug.log"
def _debug(msg: str):
import datetime
ts = datetime.datetime.now().isoformat()
try:
with open(_debug_log, "a") as f:
f.write(f"[{ts}] {msg}\n")
f.flush()
except Exception:
pass
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes, padding from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
@@ -17,8 +30,8 @@ from cryptography.hazmat.backends import default_backend
# Bitwarden-specific constants # Bitwarden-specific constants
ENCRYPTION_KEY_LENGTH = 32 # 256 bits ENCRYPTION_KEY_LENGTH = 32 # 256 bits
MAC_KEY_LENGTH = 32 MAC_KEY_LENGTH = 32
HKDF_INFO_EMAIL = b"enc" HKDF_INFO_ENC = b"enc"
HKDF_INFO_MASTER_PASSWORD = b"enc" HKDF_INFO_MAC = b"mac"
# Cipher type markers (ST arrays in Bitwarden) # Cipher type markers (ST arrays in Bitwarden)
CIPHER_TYPE_AES_CBC_256_B64 = "2." CIPHER_TYPE_AES_CBC_256_B64 = "2."
@@ -73,19 +86,16 @@ class BitwardenCrypto:
) )
def stretch_master_key(self, master_key: bytes) -> Tuple[bytes, bytes]: def stretch_master_key(self, master_key: bytes) -> Tuple[bytes, bytes]:
"""Stretch master key into encryption key + MAC key using HKDF.""" """Stretch master key into encryption key + MAC key using HKDF-expand."""
# Bitwarden uses HKDF with empty salt and specific info strings _debug(f"[CRYPTO] stretch_master_key: master_key (first 8 b64)={b64encode(master_key[:8]).decode()}")
prk = hmac.new(b"", master_key, hashlib.sha256).digest() _debug(f"[CRYPTO] stretch_master_key: master_key length={len(master_key)}")
# Encryption key # Bitwarden SDK: HKDF-expand(key, info) = HMAC-SHA256(key, info || 0x01)
enc_key = hmac.new( enc_key = hmac.new(master_key, b"enc\x01", hashlib.sha256).digest()
prk, b"\x01" + HKDF_INFO_EMAIL + b"\x00", hashlib.sha256 mac_key = hmac.new(master_key, b"mac\x01", hashlib.sha256).digest()
).digest()
# MAC key _debug(f"[CRYPTO] stretch: enc_key (first 8 b64)={b64encode(enc_key[:8]).decode()}")
mac_key = hmac.new( _debug(f"[CRYPTO] stretch: mac_key (first 8 b64)={b64encode(mac_key[:8]).decode()}")
prk, b"\x02" + HKDF_INFO_EMAIL + b"\x00", hashlib.sha256
).digest()
return enc_key, mac_key return enc_key, mac_key
@@ -121,9 +131,11 @@ class BitwardenCrypto:
Format: TYPE.BASE64(IV) | BASE64(CT) | BASE64(MAC) Format: TYPE.BASE64(IV) | BASE64(CT) | BASE64(MAC)
or: TYPE.BASE64(IV).BASE64(CT).BASE64(MAC) or: TYPE.BASE64(IV).BASE64(CT).BASE64(MAC)
""" """
_debug(f"[CRYPTO] decrypt_cipher_string: type={enc_string[:2]}, full={enc_string[:50]}...")
# Determine cipher type # Determine cipher type
if enc_string.startswith(CIPHER_TYPE_AES_CBC_256_B64): if enc_string.startswith(CIPHER_TYPE_AES_CBC_256_B64):
parts = enc_string[2:].split("|") parts = enc_string[2:].split("|")
_debug(f"[CRYPTO] decrypt_cipher_string: type 2, parts count={len(parts)}")
if len(parts) == 2: if len(parts) == 2:
# Format: TYPE.BASE64(IV)|BASE64(CT) # Format: TYPE.BASE64(IV)|BASE64(CT)
iv_b64, ct_b64 = parts iv_b64, ct_b64 = parts
@@ -136,6 +148,13 @@ class BitwardenCrypto:
iv = b64decode(iv_b64) iv = b64decode(iv_b64)
ct = b64decode(ct_b64) ct = b64decode(ct_b64)
mac = b64decode(mac_b64) mac = b64decode(mac_b64)
_debug(f"[CRYPTO] decrypt_cipher_string: verifying MAC...")
computed_mac = self.hmac_sha256(mac_key, iv + ct)
_debug(f"[CRYPTO] decrypt_cipher_string: computed_mac (b64)={b64encode(computed_mac).decode()}")
_debug(f"[CRYPTO] decrypt_cipher_string: expected_mac (b64)={mac_b64}")
_debug(f"[CRYPTO] decrypt_cipher_string: mac_keys_equal={computed_mac == mac}")
_debug(f"[CRYPTO] decrypt_cipher_string: enc_key (first 8 b64)={b64encode(enc_key[:8]).decode()}")
_debug(f"[CRYPTO] decrypt_cipher_string: mac_key (first 8 b64)={b64encode(mac_key[:8]).decode()}")
if not self.verify_mac(mac_key, iv + ct, mac): if not self.verify_mac(mac_key, iv + ct, mac):
raise ValueError("MAC verification failed") raise ValueError("MAC verification failed")
return self.decrypt_aes_cbc_256(enc_key, iv, ct) return self.decrypt_aes_cbc_256(enc_key, iv, ct)
@@ -166,10 +185,13 @@ class BitwardenCrypto:
The user key is encrypted with the stretched master key. The user key is encrypted with the stretched master key.
""" """
_debug(f"[CRYPTO] decrypt_user_key: encrypted_user_key (first 30)={encrypted_user_key[:30]}...")
stretched_enc, stretched_mac = self.stretch_master_key(master_key) stretched_enc, stretched_mac = self.stretch_master_key(master_key)
_debug(f"[CRYPTO] decrypt_user_key: calling decrypt_cipher_string...")
user_key = self.decrypt_cipher_string( user_key = self.decrypt_cipher_string(
encrypted_user_key, stretched_enc, stretched_mac encrypted_user_key, stretched_enc, stretched_mac
) )
_debug(f"[CRYPTO] decrypt_user_key: decrypted user_key length={len(user_key)}")
if len(user_key) == 64: if len(user_key) == 64:
# Has separate MAC key # Has separate MAC key