diff --git a/bitwarden_client.py b/bitwarden_client.py index dc19461..d26321a 100644 --- a/bitwarden_client.py +++ b/bitwarden_client.py @@ -252,10 +252,20 @@ class BitwardenClient: # Get encrypted user key enc_user_key = result.get("Key") + _debug(f"[LOGIN] enc_user_key present: {enc_user_key is not None}") if enc_user_key: - self.enc_key, self.mac_key = self.crypto.decrypt_user_key( - enc_user_key, master_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( + 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 { "success": True, "master_key": master_key, diff --git a/crypto.py b/crypto.py index 158bd58..eff4ba3 100644 --- a/crypto.py +++ b/crypto.py @@ -4,11 +4,24 @@ Handles KDF (PBKDF2, Argon2), AES-CBC-256, HMAC, HKDF, and vault decryption. """ import hashlib import hmac +import logging import os import struct from base64 import b64decode, b64encode 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 import hashes, padding from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC @@ -17,8 +30,8 @@ from cryptography.hazmat.backends import default_backend # Bitwarden-specific constants ENCRYPTION_KEY_LENGTH = 32 # 256 bits MAC_KEY_LENGTH = 32 -HKDF_INFO_EMAIL = b"enc" -HKDF_INFO_MASTER_PASSWORD = b"enc" +HKDF_INFO_ENC = b"enc" +HKDF_INFO_MAC = b"mac" # Cipher type markers (ST arrays in Bitwarden) CIPHER_TYPE_AES_CBC_256_B64 = "2." @@ -73,19 +86,16 @@ class BitwardenCrypto: ) def stretch_master_key(self, master_key: bytes) -> Tuple[bytes, bytes]: - """Stretch master key into encryption key + MAC key using HKDF.""" - # Bitwarden uses HKDF with empty salt and specific info strings - prk = hmac.new(b"", master_key, hashlib.sha256).digest() + """Stretch master key into encryption key + MAC key using HKDF-expand.""" + _debug(f"[CRYPTO] stretch_master_key: master_key (first 8 b64)={b64encode(master_key[:8]).decode()}") + _debug(f"[CRYPTO] stretch_master_key: master_key length={len(master_key)}") - # Encryption key - enc_key = hmac.new( - prk, b"\x01" + HKDF_INFO_EMAIL + b"\x00", hashlib.sha256 - ).digest() + # Bitwarden SDK: HKDF-expand(key, info) = HMAC-SHA256(key, info || 0x01) + enc_key = hmac.new(master_key, b"enc\x01", hashlib.sha256).digest() + mac_key = hmac.new(master_key, b"mac\x01", hashlib.sha256).digest() - # MAC key - mac_key = hmac.new( - prk, b"\x02" + HKDF_INFO_EMAIL + b"\x00", hashlib.sha256 - ).digest() + _debug(f"[CRYPTO] stretch: enc_key (first 8 b64)={b64encode(enc_key[:8]).decode()}") + _debug(f"[CRYPTO] stretch: mac_key (first 8 b64)={b64encode(mac_key[:8]).decode()}") return enc_key, mac_key @@ -121,9 +131,11 @@ class BitwardenCrypto: Format: 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 if enc_string.startswith(CIPHER_TYPE_AES_CBC_256_B64): parts = enc_string[2:].split("|") + _debug(f"[CRYPTO] decrypt_cipher_string: type 2, parts count={len(parts)}") if len(parts) == 2: # Format: TYPE.BASE64(IV)|BASE64(CT) iv_b64, ct_b64 = parts @@ -136,6 +148,13 @@ class BitwardenCrypto: iv = b64decode(iv_b64) ct = b64decode(ct_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): raise ValueError("MAC verification failed") 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. """ + _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) + _debug(f"[CRYPTO] decrypt_user_key: calling decrypt_cipher_string...") user_key = self.decrypt_cipher_string( 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: # Has separate MAC key