{"contents":"# -*- coding: utf-8 -*-#\n# WeChat 4.x (Linux) database decryption.\n#\n# SQLCipher 4: AES-256-CBC, HMAC-SHA512, page=4096, reserve=80 (16 IV + 64 HMAC).\n# Keys are extracted as raw derived keys from process memory (no PBKDF2 needed).\nimport hmac\nimport hashlib\nimport os\nfrom typing import Dict, Tuple\nfrom Cryptodome.Cipher import AES\n\nfrom .utils import wx_core_error, wx_core_loger\n\nSQLITE_FILE_HEADER = \"SQLite format 3\\x00\"\n\nKEY_SIZE = 32\nDEFAULT_PAGESIZE = 4096\nHMAC_SIZE = 64 # SHA-512 digest\nRESERVE = 80 # 16 IV + 64 HMAC-SHA512\n\n\n@wx_core_error\ndef decrypt(enc_key_hex: str, db_path: str, out_path: str):\n \"\"\"\n 使用从进程内存提取的 raw key 解密 SQLCipher 4 数据库。\n :param enc_key_hex: 64 位十六进制字符串 (32 bytes, 已派生的 AES 密钥)\n :param db_path: 待解密的数据库路径\n :param out_path: 解密后的数据库输出路径\n \"\"\"\n if not os.path.exists(db_path) or not os.path.isfile(db_path):\n return False, f\"[-] db_path:'{db_path}' File not found!\"\n if not os.path.exists(os.path.dirname(out_path)):\n return False, f\"[-] out_path:'{out_path}' File not found!\"\n if len(enc_key_hex) != 64:\n return False, f\"[-] enc_key_hex:'{enc_key_hex}' Len Error!\"\n\n enc_key = bytes.fromhex(enc_key_hex.strip())\n\n try:\n with open(db_path, \"rb\") as file:\n blist = file.read()\n except Exception as e:\n return False, f\"[-] db_path:'{db_path}' {e}!\"\n\n if len(blist) \u003c DEFAULT_PAGESIZE:\n return False, f\"[-] db_path:'{db_path}' File too small!\"\n\n salt = blist[:16]\n if len(salt) != 16:\n return False, f\"[-] db_path:'{db_path}' File Error!\"\n\n # Verify HMAC before decryption\n # Page layout: [encrypted_data][IV 16 bytes][HMAC-SHA512 64 bytes]\n # HMAC covers encrypted_data + IV (everything except the HMAC itself)\n mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])\n mac_key = hashlib.pbkdf2_hmac(\"sha512\", enc_key, mac_salt, 2, KEY_SIZE)\n\n first_page_data = blist[16:DEFAULT_PAGESIZE]\n h = hmac.new(mac_key, first_page_data[:-HMAC_SIZE], hashlib.sha512)\n h.update(b'\\x01\\x00\\x00\\x00')\n if h.digest() != first_page_data[-HMAC_SIZE:]:\n return False, f\"[-] Key Error! (key:'{enc_key_hex}'; db_path:'{db_path}')\"\n\n with open(out_path, \"wb\") as deFile:\n deFile.write(SQLITE_FILE_HEADER.encode())\n for i in range(0, len(blist), DEFAULT_PAGESIZE):\n if i == 0:\n page = blist[16:DEFAULT_PAGESIZE]\n else:\n page = blist[i:i + DEFAULT_PAGESIZE]\n if len(page) \u003c RESERVE:\n deFile.write(page)\n continue\n # [encrypted_data][IV 16][HMAC 64]\n iv = page[-(HMAC_SIZE + 16):-HMAC_SIZE]\n encrypted = page[:-(HMAC_SIZE + 16)]\n decrypted = AES.new(enc_key, AES.MODE_CBC, iv).decrypt(encrypted)\n deFile.write(decrypted)\n deFile.write(page[-(HMAC_SIZE + 16):])\n\n return True, [db_path, out_path, enc_key_hex]\n\n\n@wx_core_error\ndef batch_decrypt(db_keys: Dict[str, Tuple[str, str]], out_path: str, is_print: bool = False):\n \"\"\"\n 批量解密 SQLCipher 4 数据库。\n :param db_keys: {db_path: (enc_key_hex, salt_hex)} 从 memscan.extract_all_keys() 获取\n :param out_path: 输出目录\n :param is_print: 是否打印日志\n \"\"\"\n if not os.path.exists(out_path):\n os.makedirs(out_path)\n\n all_paths = list(db_keys.keys())\n rt_path = os.path.commonprefix(all_paths)\n if not os.path.isdir(rt_path):\n rt_path = os.path.dirname(rt_path)\n\n result = []\n for db_path, (enc_key_hex, _salt_hex) in db_keys.items():\n rel = os.path.relpath(os.path.dirname(db_path), rt_path)\n outpath = os.path.join(out_path, rel, 'de_' + os.path.basename(db_path))\n if not os.path.exists(os.path.dirname(outpath)):\n os.makedirs(os.path.dirname(outpath))\n result.append(decrypt(enc_key_hex, db_path, outpath))\n\n # Clean empty dirs\n for root, dirs, files in os.walk(out_path, topdown=False):\n for d in dirs:\n dirpath = os.path.join(root, d)\n if not os.listdir(dirpath):\n os.rmdir(dirpath)\n\n if is_print:\n print(\"=\" * 32)\n success_count = 0\n fail_count = 0\n for code, ret in result:\n if not code:\n print(ret)\n fail_count += 1\n else:\n print(f'[+] \"{ret[0]}\" -\u003e \"{ret[1]}\"')\n success_count += 1\n print(\"-\" * 32)\n print(f\"[+] 共 {len(result)} 个文件, 成功 {success_count} 个, 失败 {fail_count} 个\")\n print(\"=\" * 32)\n return True, result\n","is_binary":false,"path":"wxdump_linux/wx_core/decryption.py","ref":""}