Skip to content

Commit bfa468e

Browse files
committed
fix(decrypt): 修复 SQLCipher 密钥兼容性回归
- 解密时同时兼容 raw enc_key 与 SQLCipher 派生密钥两种输入形态 - 通过首页 HMAC 自动识别可用密钥模式,避免真实账号密钥被误判为不匹配 - 后续页面解密统一使用识别出的有效密钥,恢复数据库解密流程 - 补充 SQLCipher passphrase 场景回归测试,覆盖此次回归问题
1 parent 5b751ca commit bfa468e

2 files changed

Lines changed: 78 additions & 11 deletions

File tree

src/wechat_decrypt_tool/wechat_decrypt.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ def _derive_mac_key(raw_key: bytes, salt: bytes) -> bytes:
4343
return hashlib.pbkdf2_hmac("sha512", raw_key, mac_salt, 2, dklen=KEY_SIZE)
4444

4545

46+
def _derive_sqlcipher_enc_key(key_material: bytes, salt: bytes) -> bytes:
47+
return hashlib.pbkdf2_hmac("sha512", key_material, salt, 256000, dklen=KEY_SIZE)
48+
49+
50+
def _resolve_page1_key_material(key_material: bytes, page1: bytes) -> tuple[bytes, bytes, str] | None:
51+
salt = page1[:SALT_SIZE]
52+
stored_page1_hmac = page1[PAGE_SIZE - HMAC_SIZE : PAGE_SIZE]
53+
54+
candidates = [
55+
("raw_enc_key", key_material, _derive_mac_key(key_material, salt)),
56+
]
57+
58+
derived_key = _derive_sqlcipher_enc_key(key_material, salt)
59+
candidates.append(("sqlcipher_passphrase", derived_key, _derive_mac_key(derived_key, salt)))
60+
61+
for mode, enc_key, mac_key in candidates:
62+
expected_page1_hmac = _compute_page_hmac(mac_key, page1, 1)
63+
if stored_page1_hmac == expected_page1_hmac:
64+
return enc_key, mac_key, mode
65+
66+
return None
67+
68+
4669
def _compute_page_hmac(mac_key: bytes, page: bytes, page_num: int) -> bytes:
4770
offset = SALT_SIZE if page_num == 1 else 0
4871
data_end = PAGE_SIZE - RESERVE_SIZE + IV_SIZE
@@ -323,8 +346,9 @@ def _clear_last_error(self) -> None:
323346
def decrypt_database(self, db_path: str, output_path: str) -> bool:
324347
"""解密微信4.x版本数据库
325348
326-
这里传入的 key 已经是从微信进程内存提取出的 raw enc_key,
327-
不是 SQLCipher 的口令,因此不能再做一轮 PBKDF2。
349+
兼容两种输入形态:
350+
- raw enc_key(部分内存扫描/工具直接返回)
351+
- SQLCipher 口令/基础 key(需先用数据库 salt 做一轮 PBKDF2)
328352
"""
329353
from .logging_config import get_logger
330354
logger = get_logger(__name__)
@@ -370,15 +394,14 @@ def decrypt_database(self, db_path: str, output_path: str) -> bool:
370394
tmp_output_path = ""
371395
return True
372396

373-
salt = page1[:SALT_SIZE]
374-
mac_key = _derive_mac_key(self.key_bytes, salt)
375-
expected_page1_hmac = _compute_page_hmac(mac_key, page1, 1)
376-
stored_page1_hmac = page1[PAGE_SIZE - HMAC_SIZE : PAGE_SIZE]
377-
if stored_page1_hmac != expected_page1_hmac:
397+
resolved_key_material = _resolve_page1_key_material(self.key_bytes, page1)
398+
if resolved_key_material is None:
378399
message = f"当前数据库密钥不正确,或该密钥不属于当前账号/当前设备: {db_path}"
379400
self._set_last_error("key_mismatch", message)
380401
logger.error(f"页面 1 HMAC验证失败,密钥与数据库不匹配: {db_path}")
381402
return False
403+
enc_key, mac_key, key_mode = resolved_key_material
404+
logger.info(f"页面 1 HMAC验证通过: mode={key_mode} path={db_path}")
382405

383406
total_pages = (file_size + PAGE_SIZE - 1) // PAGE_SIZE
384407
successful_pages = 0
@@ -406,7 +429,7 @@ def decrypt_database(self, db_path: str, output_path: str) -> bool:
406429
logger.error(f"页面 {page_num} HMAC验证失败,终止解密: {db_path}")
407430
return False
408431

409-
target.write(_decrypt_page(self.key_bytes, page, page_num))
432+
target.write(_decrypt_page(enc_key, page, page_num))
410433
successful_pages += 1
411434

412435
logger.info(f"解密完成: 成功 {successful_pages} 页, 失败 0 页")

tests/test_wechat_decrypt_raw_key.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,22 @@
1919
SQLITE_HEADER,
2020
WeChatDatabaseDecryptor,
2121
_derive_mac_key,
22+
_derive_sqlcipher_enc_key,
2223
decrypt_wechat_databases,
2324
)
2425

2526

26-
def _encrypt_page(raw_key: bytes, plain_page: bytes, page_num: int, salt: bytes, iv: bytes) -> bytes:
27+
def _encrypt_page(
28+
raw_key: bytes,
29+
plain_page: bytes,
30+
page_num: int,
31+
salt: bytes,
32+
iv: bytes,
33+
*,
34+
sqlcipher_passphrase: bool = False,
35+
) -> bytes:
36+
enc_key = _derive_sqlcipher_enc_key(raw_key, salt) if sqlcipher_passphrase else raw_key
37+
2738
if page_num == 1:
2839
encrypted_input = plain_page[SALT_SIZE : PAGE_SIZE - RESERVE_SIZE]
2940
prefix = salt
@@ -32,15 +43,15 @@ def _encrypt_page(raw_key: bytes, plain_page: bytes, page_num: int, salt: bytes,
3243
prefix = b""
3344

3445
cipher = Cipher(
35-
algorithms.AES(raw_key),
46+
algorithms.AES(enc_key),
3647
modes.CBC(iv),
3748
backend=default_backend(),
3849
)
3950
encryptor = cipher.encryptor()
4051
encrypted = encryptor.update(encrypted_input) + encryptor.finalize()
4152

4253
page_without_hmac = prefix + encrypted + iv
43-
mac = hmac.new(_derive_mac_key(raw_key, salt), digestmod=hashlib.sha512)
54+
mac = hmac.new(_derive_mac_key(enc_key, salt), digestmod=hashlib.sha512)
4455
mac.update(page_without_hmac[SALT_SIZE if page_num == 1 else 0 :])
4556
mac.update(page_num.to_bytes(4, "little"))
4657
return page_without_hmac + mac.digest()
@@ -74,6 +85,39 @@ def test_decrypt_database_uses_raw_enc_key(self):
7485
self.assertTrue(decryptor.decrypt_database(str(src), str(dst)))
7586
self.assertEqual(dst.read_bytes(), page1 + page2)
7687

88+
def test_decrypt_database_falls_back_to_sqlcipher_passphrase_mode(self):
89+
passphrase_key = bytes.fromhex("9f5dd0d3b6d0477ea5045c9e380ee272e53927993eb548dd98a022e842d5f7bd")
90+
salt = bytes.fromhex("50f4090ef6897e146f94109f13743e34")
91+
iv1 = bytes.fromhex("0102030405060708090a0b0c0d0e0f10")
92+
iv2 = bytes.fromhex("1112131415161718191a1b1c1d1e1f20")
93+
94+
page1 = _build_plain_page(0x41, first_page=True)
95+
page2 = _build_plain_page(0x42, first_page=False)
96+
encrypted_db = _encrypt_page(
97+
passphrase_key,
98+
page1,
99+
1,
100+
salt,
101+
iv1,
102+
sqlcipher_passphrase=True,
103+
) + _encrypt_page(
104+
passphrase_key,
105+
page2,
106+
2,
107+
salt,
108+
iv2,
109+
sqlcipher_passphrase=True,
110+
)
111+
112+
with tempfile.TemporaryDirectory() as tmpdir:
113+
src = Path(tmpdir) / "source.db"
114+
dst = Path(tmpdir) / "out.db"
115+
src.write_bytes(encrypted_db)
116+
117+
decryptor = WeChatDatabaseDecryptor(passphrase_key.hex())
118+
self.assertTrue(decryptor.decrypt_database(str(src), str(dst)))
119+
self.assertEqual(dst.read_bytes(), page1 + page2)
120+
77121
def test_decrypt_database_keeps_existing_output_on_hmac_failure(self):
78122
good_key = bytes.fromhex("00112233445566778899aabbccddeefffedcba98765432100123456789abcdef")
79123
bad_key_hex = "ffeeddccbbaa998877665544332211000123456789abcdeffedcba9876543210"

0 commit comments

Comments
 (0)