feat: support get image key!!

This commit is contained in:
H3CoF6
2026-02-09 03:30:24 +08:00
parent 67c1b1af1f
commit 05dbc47ffa
5 changed files with 251 additions and 2 deletions

View File

@@ -21,6 +21,7 @@ dependencies = [
"pypinyin>=0.53.0", "pypinyin>=0.53.0",
"wx_key", "wx_key",
"packaging", "packaging",
"httpx",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -1,4 +1,5 @@
# import sys # import sys
# import requests
try: try:
import wx_key import wx_key
@@ -10,15 +11,25 @@ except ImportError:
import time import time
import psutil import psutil
import subprocess import subprocess
import hashlib
import os
import json
import random
import logging import logging
from typing import Optional, List import httpx
from pathlib import Path
from typing import Optional, List, Dict, Any
from dataclasses import dataclass from dataclasses import dataclass
from packaging import version as pkg_version # 建议使用 packaging 库处理版本比较 from packaging import version as pkg_version # 建议使用 packaging 库处理版本比较
from .wechat_detection import detect_wechat_installation from .wechat_detection import detect_wechat_installation
from .key_store import upsert_account_keys_in_store
from .media_helpers import _resolve_account_dir, _resolve_account_wxid_dir
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ====================== 以下是hook逻辑 ======================================
@dataclass @dataclass
class HookConfig: class HookConfig:
min_version: str min_version: str
@@ -185,3 +196,169 @@ class WeChatKeyFetcher:
def get_db_key_workflow(): def get_db_key_workflow():
fetcher = WeChatKeyFetcher() fetcher = WeChatKeyFetcher()
return fetcher.fetch_key() return fetcher.fetch_key()
# ============================== 以下是图片密钥逻辑 =====================================
# 远程 API 配置
REMOTE_URL = "https://view.free.c3o.re/dashboard"
NEXT_ACTION_ID = "7c8f99280c70626ccf5960cc4a68f368197e15f8e9"
def get_wechat_internal_global_config(wx_dir: Path, file_name1) -> bytes:
"""
获取 Blob 1: 微信内部的 global_config 文件
路径逻辑: account_dir (wxid_xxx) -> parent (xwechat_files) -> all_users -> config -> global_config
"""
xwechat_files_root = wx_dir.parent
target_path = os.path.join(xwechat_files_root, "all_users", "config", file_name1)
if not os.path.exists(target_path):
logger.error(f"未找到微信内部 global_config: {target_path}")
raise FileNotFoundError(f"找不到文件: {target_path},请确认微信数据目录结构是否完整")
return Path(target_path).read_bytes()
# def get_local_config_sha3_224() -> bytes:
# """
# 不要在意,抽象的实现 哈哈哈
# """
# content = json.dumps({
# "wxfile_dir": "C:\\Users\\17078\\xwechat_files",
# "weixin_id_folder": "wxid_lnyf4hdo9csb12_f1c4",
# "cache_dir": "C:\\Users\\17078\\Desktop\\wxDBHook\\test\\wx-dat\\wx-dat\\.cache",
# "db_key": "",
# "port": 8001
# }, indent=4).encode("utf-8")
#
# # 计算 SHA3-224
# digest = hashlib.sha3_224(content).digest()
# return digest
# async def log_request(request):
# print(f"--- Request Raw ---")
# print(f"{request.method} {request.url} {request.extensions.get('http_version', b'HTTP/1.1').decode()}")
# for name, value in request.headers.items():
# print(f"{name}: {value}")
#
# print()
#
# body = request.read()
# if body:
# print(body.decode(errors='replace'))
# print(f"-------------------\n")
async def fetch_and_save_remote_keys(account: Optional[str] = None) -> Dict[str, Any]:
# 1. 确定账号目录和 WXID
account_dir = _resolve_account_dir(account)
wx_id_dir = _resolve_account_wxid_dir(account_dir)
wxid = wx_id_dir.name
logger.info(f"正在为账号 {wxid} 获取密钥...")
try:
blob1_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1= "global_config") # 估计这是唯一有效的数据!!
logger.info(f"获取微信内部配置成功,大小: {len(blob1_bytes)} bytes")
except Exception as e:
raise RuntimeError(f"读取微信内部文件失败: {e}")
try:
blob2_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1= "global_config.crc")
logger.info(f"获取微信内部配置成功,大小: {len(blob2_bytes)} bytes")
except Exception as e:
raise RuntimeError(f"读取微信内部文件失败: {e}")
# Blob 3: 空 (联系人)
blob3_bytes = b""
# 3. 构造请求
headers = {
"Accept": "text/x-component",
"Next-Action": NEXT_ACTION_ID,
"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",
"Origin": "https://view.free.c3o.re",
"Referer": "https://view.free.c3o.re/dashboard",
"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"
}
files = {
'1': ('blob', blob1_bytes, 'application/octet-stream'),
'2': ('blob', blob2_bytes, 'application/octet-stream'),
'3': ('blob', blob3_bytes, 'application/octet-stream'),
'0': (None, json.dumps([wxid, "$A1", "$A2", "$A3", 0],separators=(",",":")).encode('utf-8')),
}
async with httpx.AsyncClient(timeout=30) as client:
logger.info("向远程服务器发送请求...")
response = await client.post(REMOTE_URL, headers=headers, files=files)
if response.status_code != 200:
raise RuntimeError(f"远程服务器错误: {response.status_code} - {response.text[:100]}")
result_data = {}
lines = response.text.split('\n')
found_config = False
for line in lines:
line = line.strip()
if not line:
continue
if line.startswith('1:'):
try:
json_part = line[2:] # 去掉 "1:"
data_obj = json.loads(json_part)
if "config" in data_obj:
config = data_obj["config"]
result_data = {
"xor_key": config.get("xor_key", ""),
"aes_key": config.get("aes_key", ""),
"nick_name": config.get("nick_name", ""),
"avatar_url": config.get("avatar_url", "")
}
found_config = True
break
except Exception as e:
logger.warning(f"解析响应行失败: {e}")
continue
if not found_config or not result_data.get("aes_key"):
logger.error(f"响应中未找到密钥信息。Full Response: {response.text[:500]}")
raise RuntimeError("解析失败: 服务器未返回 config 数据")
# 6. 处理并保存密钥
xor_raw = str(result_data["xor_key"])
aes_val = str(result_data["aes_key"])
# 转换 XOR Key (服务器返回的是十进制字符串 "178")
try:
if xor_raw.startswith("0x"):
xor_int = int(xor_raw, 16)
else:
xor_int = int(xor_raw)
xor_hex_str = f"0x{xor_int:02X}" # 格式化为 0xB2
except:
xor_hex_str = xor_raw # 转换失败则原样保留
# 保存到数据库/Store
upsert_account_keys_in_store(
account=wxid,
image_xor_key=xor_hex_str,
image_aes_key=aes_val
)
return {
"wxid": wxid,
"xor_key": xor_hex_str,
"aes_key": aes_val,
"nick_name": result_data["nick_name"]
}

View File

@@ -3,7 +3,7 @@ from typing import Optional
from fastapi import APIRouter from fastapi import APIRouter
from ..key_store import get_account_keys_from_store from ..key_store import get_account_keys_from_store
from ..key_service import get_db_key_workflow from ..key_service import get_db_key_workflow, fetch_and_save_remote_keys
from ..media_helpers import _load_media_keys, _resolve_account_dir from ..media_helpers import _load_media_keys, _resolve_account_dir
from ..path_fix import PathFixRoute from ..path_fix import PathFixRoute
@@ -86,3 +86,42 @@ async def get_wechat_db_key():
"errmsg": f"获取失败: {str(e)}", "errmsg": f"获取失败: {str(e)}",
"data": {} "data": {}
} }
@router.get("/api/get_image_key", summary="获取并保存微信图片密钥")
async def get_image_key(account: Optional[str] = None):
"""
通过模拟 Next.js Server Action 协议,利用本地微信配置文件换取 AES/XOR 密钥。
1. 读取 [wx_dir]/all_users/config/global_config (Blob 1)
2. 计算 本地 global_config (JSON) 的 SHA3-224 (Blob 2)
3. 构造 Multipart 包发送至远程服务器
4. 解析返回流,自动存入本地数据库
"""
try:
result = await fetch_and_save_remote_keys(account)
return {
"status": 0,
"errmsg": "ok",
"data": {
"xor_key": result["xor_key"],
"aes_key": result["aes_key"],
"nick_name": result.get("nick_name"),
"account": result["wxid"]
}
}
except FileNotFoundError as e:
return {
"status": -1,
"errmsg": f"文件缺失: {str(e)}",
"data": {}
}
except Exception as e:
import traceback
traceback.print_exc()
return {
"status": -1,
"errmsg": f"获取失败: {str(e)}",
"data": {}
}

View File

@@ -0,0 +1,2 @@
> 这里放wx_key模块的python预编译wheelhttps://github.com/H3CoF6/py_wx_key/releases/
> 解压放入即可

30
uv.lock generated
View File

@@ -230,6 +230,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]] [[package]]
name = "httptools" name = "httptools"
version = "0.6.4" version = "0.6.4"
@@ -259,6 +272,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
] ]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@@ -844,6 +872,7 @@ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" },
{ name = "loguru" }, { name = "loguru" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pilk" }, { name = "pilk" },
@@ -869,6 +898,7 @@ requires-dist = [
{ name = "aiofiles", specifier = ">=23.2.1" }, { name = "aiofiles", specifier = ">=23.2.1" },
{ name = "cryptography", specifier = ">=41.0.0" }, { name = "cryptography", specifier = ">=41.0.0" },
{ name = "fastapi", specifier = ">=0.104.0" }, { name = "fastapi", specifier = ">=0.104.0" },
{ name = "httpx" },
{ name = "loguru", specifier = ">=0.7.0" }, { name = "loguru", specifier = ">=0.7.0" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pilk", specifier = ">=0.2.4" }, { name = "pilk", specifier = ">=0.2.4" },