|
1 | 1 | # import sys |
| 2 | +# import requests |
2 | 3 |
|
3 | 4 | try: |
4 | 5 | import wx_key |
|
10 | 11 | import time |
11 | 12 | import psutil |
12 | 13 | import subprocess |
| 14 | +import hashlib |
| 15 | +import os |
| 16 | +import json |
| 17 | +import random |
13 | 18 | import logging |
14 | | -from typing import Optional, List |
| 19 | +import httpx |
| 20 | +from pathlib import Path |
| 21 | +from typing import Optional, List, Dict, Any |
15 | 22 | from dataclasses import dataclass |
16 | 23 | from packaging import version as pkg_version # 建议使用 packaging 库处理版本比较 |
17 | 24 | 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 |
18 | 27 |
|
19 | 28 | logger = logging.getLogger(__name__) |
20 | 29 |
|
21 | 30 |
|
| 31 | +# ====================== 以下是hook逻辑 ====================================== |
| 32 | + |
22 | 33 | @dataclass |
23 | 34 | class HookConfig: |
24 | 35 | min_version: str |
@@ -185,3 +196,169 @@ def fetch_key(self) -> str: |
185 | 196 | def get_db_key_workflow(): |
186 | 197 | fetcher = WeChatKeyFetcher() |
187 | 198 | 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 | + |
0 commit comments