fix(crypto): correct HKDF key stretching to use HKDF-expand with proper info strings
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user