Skip to content

Commit 05dbc47

Browse files
committed
feat: support get image key!!
1 parent 67c1b1a commit 05dbc47

5 files changed

Lines changed: 251 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies = [
2121
"pypinyin>=0.53.0",
2222
"wx_key",
2323
"packaging",
24+
"httpx",
2425
]
2526

2627
[project.optional-dependencies]

src/wechat_decrypt_tool/key_service.py

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# import sys
2+
# import requests
23

34
try:
45
import wx_key
@@ -10,15 +11,25 @@
1011
import time
1112
import psutil
1213
import subprocess
14+
import hashlib
15+
import os
16+
import json
17+
import random
1318
import logging
14-
from typing import Optional, List
19+
import httpx
20+
from pathlib import Path
21+
from typing import Optional, List, Dict, Any
1522
from dataclasses import dataclass
1623
from packaging import version as pkg_version # 建议使用 packaging 库处理版本比较
1724
from .wechat_detection import detect_wechat_installation
25+
from .key_store import upsert_account_keys_in_store
26+
from .media_helpers import _resolve_account_dir, _resolve_account_wxid_dir
1827

1928
logger = logging.getLogger(__name__)
2029

2130

31+
# ====================== 以下是hook逻辑 ======================================
32+
2233
@dataclass
2334
class HookConfig:
2435
min_version: str
@@ -185,3 +196,169 @@ def fetch_key(self) -> str:
185196
def get_db_key_workflow():
186197
fetcher = WeChatKeyFetcher()
187198
return fetcher.fetch_key()
199+
200+
201+
# ============================== 以下是图片密钥逻辑 =====================================
202+
203+
204+
# 远程 API 配置
205+
REMOTE_URL = "https://view.free.c3o.re/dashboard"
206+
NEXT_ACTION_ID = "7c8f99280c70626ccf5960cc4a68f368197e15f8e9"
207+
208+
209+
def get_wechat_internal_global_config(wx_dir: Path, file_name1) -> bytes:
210+
"""
211+
获取 Blob 1: 微信内部的 global_config 文件
212+
路径逻辑: account_dir (wxid_xxx) -> parent (xwechat_files) -> all_users -> config -> global_config
213+
"""
214+
xwechat_files_root = wx_dir.parent
215+
216+
target_path = os.path.join(xwechat_files_root, "all_users", "config", file_name1)
217+
218+
if not os.path.exists(target_path):
219+
logger.error(f"未找到微信内部 global_config: {target_path}")
220+
raise FileNotFoundError(f"找不到文件: {target_path},请确认微信数据目录结构是否完整")
221+
222+
return Path(target_path).read_bytes()
223+
224+
225+
# def get_local_config_sha3_224() -> bytes:
226+
# """
227+
# 不要在意,抽象的实现 哈哈哈
228+
# """
229+
# content = json.dumps({
230+
# "wxfile_dir": "C:\\Users\\17078\\xwechat_files",
231+
# "weixin_id_folder": "wxid_lnyf4hdo9csb12_f1c4",
232+
# "cache_dir": "C:\\Users\\17078\\Desktop\\wxDBHook\\test\\wx-dat\\wx-dat\\.cache",
233+
# "db_key": "",
234+
# "port": 8001
235+
# }, indent=4).encode("utf-8")
236+
#
237+
# # 计算 SHA3-224
238+
# digest = hashlib.sha3_224(content).digest()
239+
# return digest
240+
241+
# async def log_request(request):
242+
# print(f"--- Request Raw ---")
243+
# print(f"{request.method} {request.url} {request.extensions.get('http_version', b'HTTP/1.1').decode()}")
244+
# for name, value in request.headers.items():
245+
# print(f"{name}: {value}")
246+
#
247+
# print()
248+
#
249+
# body = request.read()
250+
# if body:
251+
# print(body.decode(errors='replace'))
252+
# print(f"-------------------\n")
253+
254+
255+
async def fetch_and_save_remote_keys(account: Optional[str] = None) -> Dict[str, Any]:
256+
# 1. 确定账号目录和 WXID
257+
account_dir = _resolve_account_dir(account)
258+
wx_id_dir = _resolve_account_wxid_dir(account_dir)
259+
wxid = wx_id_dir.name
260+
261+
logger.info(f"正在为账号 {wxid} 获取密钥...")
262+
263+
try:
264+
blob1_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1= "global_config") # 估计这是唯一有效的数据!!
265+
logger.info(f"获取微信内部配置成功,大小: {len(blob1_bytes)} bytes")
266+
except Exception as e:
267+
raise RuntimeError(f"读取微信内部文件失败: {e}")
268+
269+
try:
270+
blob2_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1= "global_config.crc")
271+
logger.info(f"获取微信内部配置成功,大小: {len(blob2_bytes)} bytes")
272+
except Exception as e:
273+
raise RuntimeError(f"读取微信内部文件失败: {e}")
274+
275+
# Blob 3: 空 (联系人)
276+
blob3_bytes = b""
277+
278+
279+
# 3. 构造请求
280+
headers = {
281+
"Accept": "text/x-component",
282+
"Next-Action": NEXT_ACTION_ID,
283+
"Next-Router-State-Tree": "%5B%22%22%2C%7B%22children%22%3A%5B%22dashboard%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D",
284+
"Origin": "https://view.free.c3o.re",
285+
"Referer": "https://view.free.c3o.re/dashboard",
286+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
287+
}
288+
289+
files = {
290+
'1': ('blob', blob1_bytes, 'application/octet-stream'),
291+
'2': ('blob', blob2_bytes, 'application/octet-stream'),
292+
'3': ('blob', blob3_bytes, 'application/octet-stream'),
293+
'0': (None, json.dumps([wxid, "$A1", "$A2", "$A3", 0],separators=(",",":")).encode('utf-8')),
294+
}
295+
296+
297+
async with httpx.AsyncClient(timeout=30) as client:
298+
logger.info("向远程服务器发送请求...")
299+
response = await client.post(REMOTE_URL, headers=headers, files=files)
300+
301+
if response.status_code != 200:
302+
raise RuntimeError(f"远程服务器错误: {response.status_code} - {response.text[:100]}")
303+
304+
305+
result_data = {}
306+
lines = response.text.split('\n')
307+
308+
found_config = False
309+
for line in lines:
310+
line = line.strip()
311+
if not line:
312+
continue
313+
314+
if line.startswith('1:'):
315+
try:
316+
json_part = line[2:] # 去掉 "1:"
317+
data_obj = json.loads(json_part)
318+
319+
if "config" in data_obj:
320+
config = data_obj["config"]
321+
result_data = {
322+
"xor_key": config.get("xor_key", ""),
323+
"aes_key": config.get("aes_key", ""),
324+
"nick_name": config.get("nick_name", ""),
325+
"avatar_url": config.get("avatar_url", "")
326+
}
327+
found_config = True
328+
break
329+
except Exception as e:
330+
logger.warning(f"解析响应行失败: {e}")
331+
continue
332+
333+
if not found_config or not result_data.get("aes_key"):
334+
logger.error(f"响应中未找到密钥信息。Full Response: {response.text[:500]}")
335+
raise RuntimeError("解析失败: 服务器未返回 config 数据")
336+
337+
# 6. 处理并保存密钥
338+
xor_raw = str(result_data["xor_key"])
339+
aes_val = str(result_data["aes_key"])
340+
341+
# 转换 XOR Key (服务器返回的是十进制字符串 "178")
342+
try:
343+
if xor_raw.startswith("0x"):
344+
xor_int = int(xor_raw, 16)
345+
else:
346+
xor_int = int(xor_raw)
347+
xor_hex_str = f"0x{xor_int:02X}" # 格式化为 0xB2
348+
except:
349+
xor_hex_str = xor_raw # 转换失败则原样保留
350+
351+
# 保存到数据库/Store
352+
upsert_account_keys_in_store(
353+
account=wxid,
354+
image_xor_key=xor_hex_str,
355+
image_aes_key=aes_val
356+
)
357+
358+
return {
359+
"wxid": wxid,
360+
"xor_key": xor_hex_str,
361+
"aes_key": aes_val,
362+
"nick_name": result_data["nick_name"]
363+
}
364+

src/wechat_decrypt_tool/routers/keys.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from fastapi import APIRouter
44

55
from ..key_store import get_account_keys_from_store
6-
from ..key_service import get_db_key_workflow
6+
from ..key_service import get_db_key_workflow, fetch_and_save_remote_keys
77
from ..media_helpers import _load_media_keys, _resolve_account_dir
88
from ..path_fix import PathFixRoute
99

@@ -86,3 +86,42 @@ async def get_wechat_db_key():
8686
"errmsg": f"获取失败: {str(e)}",
8787
"data": {}
8888
}
89+
90+
91+
@router.get("/api/get_image_key", summary="获取并保存微信图片密钥")
92+
async def get_image_key(account: Optional[str] = None):
93+
"""
94+
通过模拟 Next.js Server Action 协议,利用本地微信配置文件换取 AES/XOR 密钥。
95+
96+
1. 读取 [wx_dir]/all_users/config/global_config (Blob 1)
97+
2. 计算 本地 global_config (JSON) 的 SHA3-224 (Blob 2)
98+
3. 构造 Multipart 包发送至远程服务器
99+
4. 解析返回流,自动存入本地数据库
100+
"""
101+
try:
102+
result = await fetch_and_save_remote_keys(account)
103+
104+
return {
105+
"status": 0,
106+
"errmsg": "ok",
107+
"data": {
108+
"xor_key": result["xor_key"],
109+
"aes_key": result["aes_key"],
110+
"nick_name": result.get("nick_name"),
111+
"account": result["wxid"]
112+
}
113+
}
114+
except FileNotFoundError as e:
115+
return {
116+
"status": -1,
117+
"errmsg": f"文件缺失: {str(e)}",
118+
"data": {}
119+
}
120+
except Exception as e:
121+
import traceback
122+
traceback.print_exc()
123+
return {
124+
"status": -1,
125+
"errmsg": f"获取失败: {str(e)}",
126+
"data": {}
127+
}

tools/key_wheels/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
> 这里放wx_key模块的python预编译wheel:https://github.com/H3CoF6/py_wx_key/releases/
2+
> 解压放入即可

uv.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)