Merge pull request #24 from H3CoF6/feat/wx-key

feat(wx-key):support get db key and img key without other tools.
This commit is contained in:
2977094657
2026-02-09 11:32:42 +08:00
committed by GitHub
15 changed files with 708 additions and 22 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ wheels/
# Local config templates # Local config templates
/wechat_db_config_template.json /wechat_db_config_template.json
.ace-tool/ .ace-tool/
pnpm-lock.yaml
# Local dev repos and data # Local dev repos and data
/WxDatDecrypt/ /WxDatDecrypt/

View File

@@ -376,6 +376,21 @@ export const useApi = () => {
return await request(url) return await request(url)
} }
// 获取微信进程状态
const getWxStatus = async () => {
return await request('/wechat/status')
}
// 获取数据库密钥
const getDbKey = async () => {
return await request('/get_db_key')
}
// 获取图片密钥
const getImageKey = async () => {
return await request('/get_image_key')
}
return { return {
detectWechat, detectWechat,
detectCurrentAccount, detectCurrentAccount,
@@ -408,6 +423,9 @@ export const useApi = () => {
exportChatContacts, exportChatContacts,
getWrappedAnnual, getWrappedAnnual,
getWrappedAnnualMeta, getWrappedAnnualMeta,
getWrappedAnnualCard getWrappedAnnualCard,
getDbKey,
getImageKey,
getWxStatus,
} }
} }

View File

@@ -19,5 +19,8 @@
"ogl": "^1.0.11", "ogl": "^1.0.11",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
},
"devDependencies": {
"tailwindcss": "3.4.17"
} }
} }

View File

@@ -26,24 +26,40 @@
<!-- 密钥输入 --> <!-- 密钥输入 -->
<div> <div>
<label for="key" class="block text-sm font-medium text-[#000000e6] mb-2"> <label for="key" class="block text-sm font-medium text-[#000000e6] mb-2">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
解密密钥 <span class="text-red-500">*</span> 解密密钥 <span class="text-red-500">*</span>
</label> </label>
<div class="relative">
<input <div class="flex gap-3">
id="key" <div class="relative flex-1">
v-model="formData.key" <input
type="text" id="key"
placeholder="请输入64位十六进制密钥" v-model="formData.key"
class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200" type="text"
:class="{ 'border-red-500': formErrors.key }" placeholder="请输入64位十六进制密钥"
required class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
/> :class="{ 'border-red-500': formErrors.key }"
<div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2"> required
<span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span> />
<div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span>
</div>
</div> </div>
<button
type="button"
@click="handleGetDbKey"
:disabled="isGettingDbKey"
class="flex-none inline-flex items-center px-4 py-3 bg-[#07C160] text-white rounded-lg text-sm font-medium hover:bg-[#06AD56] transition-all duration-200 disabled:opacity-50 disabled:cursor-wait whitespace-nowrap"
>
<svg v-if="isGettingDbKey" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<svg v-else class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ isGettingDbKey ? '获取中...' : '自动获取' }}
</button>
</div> </div>
<p v-if="formErrors.key" class="mt-1 text-sm text-red-600 flex items-center"> <p v-if="formErrors.key" class="mt-1 text-sm text-red-600 flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -55,7 +71,7 @@
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串 尝试自动获取或者使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串
</p> </p>
</div> </div>
@@ -131,6 +147,26 @@
<!-- 填写密钥 --> <!-- 填写密钥 -->
<div class="mb-6"> <div class="mb-6">
<div class="bg-gray-50 rounded-lg p-4"> <div class="bg-gray-50 rounded-lg p-4">
<div class="flex justify-between items-center mb-4 pb-3 border-b border-gray-200">
<span class="text-sm font-medium text-gray-500">手动输入或通过微信获取</span>
<button
type="button"
@click="handleGetImageKey"
:disabled="isGettingImageKey"
class="flex-none inline-flex items-center px-4 py-3 bg-[#07C160] text-white rounded-lg text-sm font-medium hover:bg-[#06AD56] transition-all duration-200 disabled:opacity-50 disabled:cursor-wait whitespace-nowrap"
>
<svg v-if="isGettingImageKey" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<svg v-else class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ isGettingImageKey ? '正在获取...' : '自动获取' }}
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-[#000000e6] mb-2">XOR必填</label> <label class="block text-sm font-medium text-[#000000e6] mb-2">XOR必填</label>
@@ -158,7 +194,7 @@
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 获取图片密钥AES 可选V4-V2 需要 尝试自动获取使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 获取图片密钥AES 可选V4-V2 需要
</p> </p>
</div> </div>
</div> </div>
@@ -326,6 +362,19 @@
</div> </div>
</div> </div>
<!-- 警告渲染 -->
<transition name="fade">
<div v-if="warning" class="bg-amber-50 border border-amber-200 rounded-lg p-4 mt-6 flex items-start">
<svg class="h-5 w-5 mr-2 flex-shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<p class="font-semibold text-amber-800">温馨提示</p>
<p class="text-sm mt-1 text-amber-700">{{ warning }}</p>
</div>
</div>
</transition>
<!-- 错误提示 --> <!-- 错误提示 -->
<transition name="fade"> <transition name="fade">
<div v-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mt-6 animate-shake flex items-start"> <div v-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mt-6 animate-shake flex items-start">
@@ -367,12 +416,15 @@
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { useApi } from '~/composables/useApi' import { useApi } from '~/composables/useApi'
const { decryptDatabase, saveMediaKeys, getSavedKeys } = useApi() const { decryptDatabase, saveMediaKeys, getSavedKeys, getDbKey, getImageKey, getWxStatus } = useApi()
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const warning = ref('') // 警告,用于密钥提示
const currentStep = ref(0) const currentStep = ref(0)
const mediaAccount = ref('') const mediaAccount = ref('')
const isGettingDbKey = ref(false)
const isGettingImageKey = ref(false)
// 步骤定义 // 步骤定义
const steps = [ const steps = [
@@ -453,10 +505,89 @@ const prefillKeysForAccount = async (account) => {
} }
} }
const handleGetDbKey = async () => {
if (isGettingDbKey.value) return
isGettingDbKey.value = true
error.value = ''
warning.value = ''
formErrors.key = ''
try {
const statusRes = await getWxStatus() // pid不是主进程但是没关系
const wxStatus = statusRes?.wx_status
if (wxStatus?.is_running) {
warning.value = '检测到微信正在运行5秒后将终止进程并重启以获取密钥'
await new Promise(resolve => setTimeout(resolve, 5000))
} else {
// 没有逻辑
}
warning.value = '正在启动微信以获取密钥,请确保微信未开启“自动登录”,并在启动后 1 分钟内完成登录操作。'
const res = await getDbKey()
if (res && res.status === 0) {
if (res.data?.db_key) {
formData.key = res.data.db_key
warning.value = ''
}
if (res.errmsg && res.errmsg !== 'ok') {
warning.value = res.errmsg
}
} else {
error.value = '获取失败: ' + (res?.errmsg || '未知错误')
}
} catch (e) {
console.error(e)
error.value = '系统错误: ' + e.message
} finally {
isGettingDbKey.value = false
}
}
const handleGetImageKey = async () => {
if (isGettingImageKey.value) return
isGettingImageKey.value = true
manualKeyErrors.xor_key = ''
manualKeyErrors.aes_key = ''
error.value = ''
warning.value = ''
try {
const res = await getImageKey()
if (res && res.status === 0) {
if (res.data?.aes_key) {
manualKeys.aes_key = res.data.aes_key
}
if (res.data?.xor_key) {
// 后端记得处理为16进制再返回
manualKeys.xor_key = res.data.xor_key
}
if (res.errmsg && res.errmsg !== 'ok') {
error.value = res.errmsg
}
} else {
error.value = '获取失败: ' + (res?.errmsg || '未知错误')
}
} catch (e) {
console.error(e)
error.value = '系统错误: ' + e.message
} finally {
isGettingImageKey.value = false
}
}
const applyManualKeys = () => { const applyManualKeys = () => {
manualKeyErrors.xor_key = '' manualKeyErrors.xor_key = ''
manualKeyErrors.aes_key = '' manualKeyErrors.aes_key = ''
error.value = '' error.value = ''
warning.value = ''
const aes = normalizeAesKey(manualKeys.aes_key) const aes = normalizeAesKey(manualKeys.aes_key)
if (!aes.ok) { if (!aes.ok) {
@@ -550,6 +681,7 @@ const handleDecrypt = async () => {
loading.value = true loading.value = true
error.value = '' error.value = ''
warning.value = ''
try { try {
const result = await decryptDatabase({ const result = await decryptDatabase({
@@ -596,6 +728,7 @@ const decryptAllImages = async () => {
mediaDecrypting.value = true mediaDecrypting.value = true
mediaDecryptResult.value = null mediaDecryptResult.value = null
error.value = '' error.value = ''
warning.value = ''
// 重置进度 // 重置进度
decryptProgress.current = 0 decryptProgress.current = 0
@@ -671,6 +804,7 @@ const decryptAllImages = async () => {
// 从密钥步骤进入图片解密步骤 // 从密钥步骤进入图片解密步骤
const goToMediaDecryptStep = async () => { const goToMediaDecryptStep = async () => {
error.value = '' error.value = ''
warning.value = ''
// 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示) // 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示)
const ok = applyManualKeys() const ok = applyManualKeys()
if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return

View File

@@ -19,6 +19,9 @@ dependencies = [
"zstandard>=0.23.0", "zstandard>=0.23.0",
"pilk>=0.2.4", "pilk>=0.2.4",
"pypinyin>=0.53.0", "pypinyin>=0.53.0",
"wx_key",
"packaging",
"httpx",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -40,3 +43,6 @@ include = [
"src/wechat_decrypt_tool/native/wcdb_api.dll", "src/wechat_decrypt_tool/native/wcdb_api.dll",
"src/wechat_decrypt_tool/native/WCDB.dll", "src/wechat_decrypt_tool/native/WCDB.dll",
] ]
[tool.uv]
find-links = ["./tools/key_wheels/"]

View File

@@ -0,0 +1,357 @@
# import sys
# import requests
try:
import wx_key
except ImportError:
print('[!] 环境中未安装wx_key依赖可能无法自动获取数据库密钥')
wx_key = None
# sys.exit(1)
import time
import psutil
import subprocess
import hashlib
import os
import json
import random
import logging
import httpx
from pathlib import Path
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
from packaging import version as pkg_version # 建议使用 packaging 库处理版本比较
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__)
# ====================== 以下是hook逻辑 ======================================
@dataclass
class HookConfig:
min_version: str
pattern: str # 用 00 不要用 ? !!!! 否则C++内存会炸
mask: str
offset: int
class WeChatKeyFetcher:
def __init__(self):
self.process_name = "Weixin.exe"
self.timeout_seconds = 60
@staticmethod
def _hex_array_to_str(hex_array: List[int]) -> str:
return " ".join([f"{b:02X}" for b in hex_array])
def _get_hook_config(self, version_str: str) -> Optional[HookConfig]:
"""搬运自wx_key代码未来用ida脚本直接获取即可"""
try:
v_curr = pkg_version.parse(version_str)
except Exception as e:
logger.error(f"版本号解析失败: {version_str} || {e}")
return None
if v_curr > pkg_version.parse("4.1.6.14"):
return HookConfig(
min_version=">4.1.6.14",
pattern=self._hex_array_to_str([
0x24, 0x50, 0x48, 0xC7, 0x45, 0x00, 0xFE, 0xFF, 0xFF, 0xFF,
0x44, 0x89, 0xCF, 0x44, 0x89, 0xC3, 0x49, 0x89, 0xD6, 0x48,
0x89, 0xCE, 0x48, 0x89
]),
mask="xxxxxxxxxxxxxxxxxxxxxxxx",
offset=-3
)
if pkg_version.parse("4.1.4") <= v_curr <= pkg_version.parse("4.1.6.14"):
return HookConfig(
min_version="4.1.4-4.1.6.14",
pattern=self._hex_array_to_str([
0x24, 0x08, 0x48, 0x89, 0x6c, 0x24, 0x10, 0x48, 0x89, 0x74,
0x00, 0x18, 0x48, 0x89, 0x7c, 0x00, 0x20, 0x41, 0x56, 0x48,
0x83, 0xec, 0x50, 0x41
]),
mask="xxxxxxxxxx?xxxx?xxxxxxxx",
offset=-3
)
if v_curr < pkg_version.parse("4.1.4"):
return HookConfig(
min_version="<4.1.4",
pattern=self._hex_array_to_str([
0x24, 0x50, 0x48, 0xc7, 0x45, 0x00, 0xfe, 0xff, 0xff, 0xff,
0x44, 0x89, 0xcf, 0x44, 0x89, 0xc3, 0x49, 0x89, 0xd6, 0x48,
0x89, 0xce, 0x48, 0x89
]),
mask="xxxxxxxxxxxxxxxxxxxxxxxx",
offset=-15 # -0xf
)
return None
def kill_wechat(self):
"""检测并查杀微信进程"""
killed = False
for proc in psutil.process_iter(['pid', 'name']):
try:
if proc.info['name'] == self.process_name:
logger.info(f"Killing WeChat process: {proc.info['pid']}")
proc.terminate()
killed = True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
if killed:
time.sleep(1) # 等待完全退出
def launch_wechat(self, exe_path: str) -> int:
"""启动微信并返回 PID"""
try:
process = subprocess.Popen(exe_path)
time.sleep(2)
candidates = []
for proc in psutil.process_iter(['pid', 'name', 'create_time']):
if proc.info['name'] == self.process_name:
candidates.append(proc)
if candidates:
candidates.sort(key=lambda x: x.info['create_time'], reverse=True)
target_pid = candidates[0].info['pid']
return target_pid
return process.pid
except Exception as e:
logger.error(f"启动微信失败: {e}")
raise RuntimeError(f"无法启动微信: {e}")
def fetch_key(self) -> str:
"""没有wx_key模块无法自动获取密钥"""
if wx_key is None:
raise RuntimeError("wx_key 模块未安装或加载失败")
install_info = detect_wechat_installation()
exe_path = install_info.get('wechat_exe_path')
version = install_info.get('wechat_version')
if not exe_path or not version:
raise RuntimeError("无法自动定位微信安装路径或版本")
logger.info(f"Detect WeChat: {version} at {exe_path}")
config = self._get_hook_config(version)
if not config:
raise RuntimeError(f"不支持的微信版本: {version}")
self.kill_wechat()
pid = self.launch_wechat(exe_path)
logger.info(f"WeChat launched, PID: {pid}")
logger.info(f"Initializing Hook with pattern: {config.pattern[:20]}... Offset: {config.offset}")
if not wx_key.initialize_hook(pid, "", config.pattern, config.mask, config.offset):
err = wx_key.get_last_error_msg()
raise RuntimeError(f"Hook初始化失败: {err}")
start_time = time.time()
try:
while True:
if time.time() - start_time > self.timeout_seconds:
raise TimeoutError("获取密钥超时 (60s)")
key = wx_key.poll_key_data()
if key:
found_key = key
break
while True:
msg, level = wx_key.get_status_message()
if msg is None:
break
if level == 2:
logger.error(f"[Hook Error] {msg}")
time.sleep(0.1)
finally:
logger.info("Cleaning up hook...")
wx_key.cleanup_hook()
if found_key:
return found_key
else:
raise RuntimeError("未知错误,未获取到密钥")
def get_db_key_workflow():
fetcher = WeChatKeyFetcher()
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:
"""
读微信目录下的主配置文件
"""
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]:
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}")
blob3_bytes = b""
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"])
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}"
except:
xor_hex_str = xor_raw
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,6 +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, 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
@@ -51,3 +52,76 @@ async def get_saved_keys(account: Optional[str] = None):
"keys": result, "keys": result,
} }
@router.get("/api/get_db_key", summary="自动获取微信数据库密钥")
async def get_wechat_db_key():
"""
自动流程:
1. 结束微信进程
2. 启动微信
3. 根据版本注入 Hook
4. 抓取密钥并返回
"""
try:
# 不需要async吧我相信fastapi的线程池
db_key = get_db_key_workflow()
return {
"status": 0,
"errmsg": "ok",
"data": {
"db_key": db_key
}
}
except TimeoutError:
return {
"status": -1,
"errmsg": "获取超时,请确保微信没有开启自动登录 或者 加快手速",
"data": {}
}
except Exception as e:
return {
"status": -1,
"errmsg": f"获取失败: {str(e)}",
"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.crc
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

@@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
import psutil
from fastapi import APIRouter from fastapi import APIRouter
from ..logging_config import get_logger from ..logging_config import get_logger
@@ -71,3 +71,49 @@ async def detect_current_account(data_root_path: Optional[str] = None):
'error': str(e), 'error': str(e),
'data': None, 'data': None,
} }
@router.get("/api/wechat/status", summary="检查微信运行状态")
async def check_wechat_status():
"""
检查系统中是否有 Weixin.exe 或 WeChat.exe 进程在运行
返回: status=0 成功, wx_status={is_running: bool, pid: int, ...}
"""
process_name_targets = ["Weixin.exe", "WeChat.exe"]
wx_status = {
"is_running": False,
"pid": None,
"exe_path": None,
"memory_usage_mb": 0.0
}
try:
for proc in psutil.process_iter(['pid', 'name', 'exe', 'memory_info']):
try:
if proc.info['name'] and proc.info['name'] in process_name_targets:
wx_status["is_running"] = True
wx_status["pid"] = proc.info['pid']
wx_status["exe_path"] = proc.info['exe']
mem = proc.info['memory_info']
if mem:
wx_status["memory_usage_mb"] = round(mem.rss / (1024 * 1024), 2)
break
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
return {
"status": 0,
"errmsg": "ok",
"wx_status": wx_status
}
except Exception as e:
# 即使出错也返回 JSON但 status 非 0
return {
"status": -1,
"errmsg": f"检查进程失败: {str(e)}",
"wx_status": wx_status
}

View File

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

47
uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.11" requires-python = ">=3.11"
[[package]] [[package]]
@@ -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,7 +872,9 @@ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" },
{ name = "loguru" }, { name = "loguru" },
{ name = "packaging" },
{ name = "pilk" }, { name = "pilk" },
{ name = "psutil" }, { name = "psutil" },
{ name = "pycryptodome" }, { name = "pycryptodome" },
@@ -854,6 +884,7 @@ dependencies = [
{ name = "requests" }, { name = "requests" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
{ name = "wx-key" },
{ name = "zstandard" }, { name = "zstandard" },
] ]
@@ -867,7 +898,9 @@ 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 = "pilk", specifier = ">=0.2.4" }, { name = "pilk", specifier = ">=0.2.4" },
{ name = "psutil", specifier = ">=7.0.0" }, { name = "psutil", specifier = ">=7.0.0" },
{ name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pycryptodome", specifier = ">=3.23.0" },
@@ -878,6 +911,7 @@ requires-dist = [
{ name = "requests", specifier = ">=2.32.4" }, { name = "requests", specifier = ">=2.32.4" },
{ name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-extensions", specifier = ">=4.8.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
{ name = "wx-key" },
{ name = "zstandard", specifier = ">=0.23.0" }, { name = "zstandard", specifier = ">=0.23.0" },
] ]
provides-extras = ["build"] provides-extras = ["build"]
@@ -891,6 +925,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
] ]
[[package]]
name = "wx-key"
version = "1.0.0"
source = { registry = "tools/key_wheels" }
wheels = [
{ path = "wx_key-1.0.0-cp311-cp311-win_amd64.whl" },
{ path = "wx_key-1.0.0-cp312-cp312-win_amd64.whl" },
{ path = "wx_key-1.0.0-cp313-cp313-win_amd64.whl" },
{ path = "wx_key-1.0.0-cp314-cp314-win_amd64.whl" },
]
[[package]] [[package]]
name = "zstandard" name = "zstandard"
version = "0.25.0" version = "0.25.0"