Skip to content

Commit 0987167

Browse files
committed
fix(key): 支持手动指定微信安装目录并校验 db key 来源
- /api/get_keys 支持传入 wechat_install_path,兼容安装目录与 Weixin.exe / WeChat.exe - 解密完成后保存 db key 的来源路径与别名,避免历史密钥被错误账号复用 - 解密页按 account + db_storage_path 回填已保存密钥,并补充相关测试覆盖
1 parent ec2a84a commit 0987167

12 files changed

Lines changed: 733 additions & 81 deletions

frontend/composables/useApi.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ export const useApi = () => {
397397
const getSavedKeys = async (params = {}) => {
398398
const query = new URLSearchParams()
399399
if (params && params.account) query.set('account', params.account)
400+
if (params && params.db_storage_path) query.set('db_storage_path', params.db_storage_path)
400401
const url = '/keys' + (query.toString() ? `?${query.toString()}` : '')
401402
return await request(url)
402403
}
@@ -547,8 +548,11 @@ export const useApi = () => {
547548
}
548549

549550
// 获取数据库密钥
550-
const getKeys = async () => {
551-
return await request('/get_keys')
551+
const getKeys = async (params = {}) => {
552+
const query = new URLSearchParams()
553+
if (params && params.wechat_install_path) query.set('wechat_install_path', params.wechat_install_path)
554+
const url = '/get_keys' + (query.toString() ? `?${query.toString()}` : '')
555+
return await request(url)
552556
}
553557

554558
// 获取图片密钥
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const WECHAT_INSTALL_PATH_STORAGE_KEY = 'decrypt.wechatInstallPath'
2+
3+
export const normalizeWechatInstallPath = (value) => String(value || '').trim()
4+
5+
export const readStoredWechatInstallPath = () => {
6+
if (!process.client || typeof window === 'undefined') return ''
7+
try {
8+
return normalizeWechatInstallPath(window.localStorage.getItem(WECHAT_INSTALL_PATH_STORAGE_KEY) || '')
9+
} catch {
10+
return ''
11+
}
12+
}
13+
14+
export const writeStoredWechatInstallPath = (value) => {
15+
if (!process.client || typeof window === 'undefined') return
16+
try {
17+
const normalized = normalizeWechatInstallPath(value)
18+
if (normalized) {
19+
window.localStorage.setItem(WECHAT_INSTALL_PATH_STORAGE_KEY, normalized)
20+
} else {
21+
window.localStorage.removeItem(WECHAT_INSTALL_PATH_STORAGE_KEY)
22+
}
23+
} catch {}
24+
}

frontend/pages/decrypt.vue

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
</svg>
7474
点击按钮将自动获取【数据库解密密钥】。您也可以手动输入已知的64位密钥。
7575
</p>
76+
<p v-if="formData.wechat_install_path" class="mt-2 text-xs text-[#7F7F7F] flex items-start">
77+
<svg class="w-4 h-4 mr-1 mt-0.5 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78+
<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"/>
79+
</svg>
80+
<span>当前将使用第一步检测时保存的微信安装目录:<span class="font-mono break-all">{{ formData.wechat_install_path }}</span>。</span>
81+
</p>
7682
</div>
7783

7884
<!-- 数据库路径输入 -->
@@ -655,6 +661,7 @@
655661
<script setup>
656662
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
657663
import { useApi } from '~/composables/useApi'
664+
import { normalizeWechatInstallPath, readStoredWechatInstallPath } from '~/lib/wechat-install-path'
658665
659666
const { decryptDatabase, saveMediaKeys, getSavedKeys, getKeys, getImageKey, getWxStatus } = useApi()
660667
@@ -677,7 +684,8 @@ const steps = [
677684
// 表单数据
678685
const formData = reactive({
679686
key: '',
680-
db_storage_path: ''
687+
db_storage_path: '',
688+
wechat_install_path: ''
681689
})
682690
683691
// 表单错误
@@ -764,7 +772,10 @@ const prefillKeysForAccount = async (account) => {
764772
if (!acc) return
765773
logDecryptDebug('prefill:start', { account: acc })
766774
try {
767-
const resp = await getSavedKeys({ account: acc })
775+
const resp = await getSavedKeys({
776+
account: acc,
777+
db_storage_path: String(formData.db_storage_path || '').trim()
778+
})
768779
if (!resp || resp.status !== 'success') return
769780
const keys = resp.keys || {}
770781
@@ -786,6 +797,9 @@ const prefillKeysForAccount = async (account) => {
786797
request_account: acc,
787798
response_account: String(resp.account || '').trim(),
788799
db_key_present: !!dbKey,
800+
db_key_store_account: String(keys.db_key_store_account || '').trim(),
801+
db_key_source_wxid_dir: String(keys.db_key_source_wxid_dir || '').trim(),
802+
db_key_blocked_reason: String(keys.db_key_blocked_reason || '').trim(),
789803
...summarizeKeyStateForLog(
790804
String(keys.image_xor_key || '').trim(),
791805
String(keys.image_aes_key || '').trim()
@@ -873,6 +887,8 @@ const handleGetDbKey = async () => {
873887
formErrors.key = ''
874888
875889
try {
890+
const wechatInstallPath = normalizeWechatInstallPath(formData.wechat_install_path || readStoredWechatInstallPath())
891+
formData.wechat_install_path = wechatInstallPath
876892
const statusRes = await getWxStatus()
877893
const wxStatus = statusRes?.wx_status
878894
@@ -883,7 +899,9 @@ const handleGetDbKey = async () => {
883899
884900
warning.value = '正在启动微信,请确保微信未开启“自动登录”,并在弹窗中正常登录。'
885901
886-
const res = await getKeys()
902+
const res = await getKeys({
903+
wechat_install_path: wechatInstallPath
904+
})
887905
888906
if (res && res.status === 0) {
889907
if (res.data?.db_key) {
@@ -1617,6 +1635,7 @@ const skipToChat = async () => {
16171635
// 页面加载时检查是否有选中的账户
16181636
onMounted(async () => {
16191637
if (process.client && typeof window !== 'undefined') {
1638+
formData.wechat_install_path = readStoredWechatInstallPath()
16201639
const selectedAccount = sessionStorage.getItem('selectedAccount')
16211640
logDecryptDebug('mounted:selected-account-raw', { raw: selectedAccount || '' })
16221641
if (selectedAccount) {

frontend/pages/detection-result.vue

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@
3535
<span v-if="customPath">当前指定检测路径:<span class="font-mono bg-gray-50 px-1 rounded text-[#000000e6]">{{ customPath }}</span></span>
3636
<span v-else>如果自动检测漏了,您可以手动指定微信数据根目录 (通常名为 xwechat_files) 让系统重新扫描。</span>
3737
</p>
38+
<div class="mt-3">
39+
<label for="wechatInstallPath" class="block text-xs font-medium text-[#000000e6]">微信安装目录(可选)</label>
40+
<div class="mt-2 flex flex-col lg:flex-row gap-3">
41+
<input
42+
id="wechatInstallPath"
43+
v-model="wechatInstallPath"
44+
type="text"
45+
placeholder="例如: D:\Program Files\Tencent\WeChat 或 D:\Program Files\Tencent\WeChat\Weixin.exe"
46+
class="flex-1 px-3 py-2 bg-white border border-[#EDEDED] rounded-lg font-mono text-xs focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
47+
@blur="persistWechatInstallPath"
48+
/>
49+
<button
50+
type="button"
51+
@click="pickWechatInstallDirectory"
52+
:disabled="isPickingWechatInstallPath"
53+
class="shrink-0 px-4 py-2 bg-white border border-[#EDEDED] text-[#000000e6] rounded-lg text-xs font-medium hover:bg-gray-50 disabled:opacity-50 disabled:cursor-wait transition-all duration-200"
54+
>
55+
{{ isPickingWechatInstallPath ? '选择中...' : '选择微信目录' }}
56+
</button>
57+
</div>
58+
<p class="text-xs text-[#7F7F7F] mt-2">
59+
一键获取数据库密钥会优先使用这里填写的路径。支持安装目录或 Weixin.exe / WeChat.exe 路径。
60+
</p>
61+
</div>
3862
</div>
3963
<button @click="handlePickDirectory" :disabled="loading"
4064
class="shrink-0 px-5 py-2.5 bg-[#07C160] text-white rounded-xl text-sm font-medium hover:bg-[#06AD56] focus:ring-2 focus:ring-[#07C160] focus:ring-offset-1 disabled:opacity-50 transition-all duration-200 flex items-center justify-center">
@@ -238,13 +262,16 @@
238262
<script setup>
239263
import {computed, onMounted, ref} from 'vue'
240264
import {useApi} from '~/composables/useApi'
265+
import {normalizeWechatInstallPath, readStoredWechatInstallPath, writeStoredWechatInstallPath} from '~/lib/wechat-install-path'
241266
import {useAppStore} from '~/stores/app'
242267
243268
const { detectWechat, pickSystemDirectory } = useApi()
244269
const appStore = useAppStore()
245270
const loading = ref(false)
246271
const detectionResult = ref(null)
247272
const customPath = ref('')
273+
const wechatInstallPath = ref('')
274+
const isPickingWechatInstallPath = ref(false)
248275
const STORAGE_KEY = 'wechat_data_root_path'
249276
250277
const isDesktopShell = () => {
@@ -289,6 +316,52 @@ const handlePickDirectory = async () => {
289316
}
290317
291318
// 计算属性:将当前登录账号排在第一位
319+
const persistWechatInstallPath = () => {
320+
const normalized = normalizeWechatInstallPath(wechatInstallPath.value)
321+
wechatInstallPath.value = normalized
322+
writeStoredWechatInstallPath(normalized)
323+
}
324+
325+
const pickWechatInstallDirectory = async () => {
326+
if (isPickingWechatInstallPath.value) return
327+
isPickingWechatInstallPath.value = true
328+
329+
try {
330+
let path = ''
331+
332+
if (isDesktopShell()) {
333+
const res = await window.wechatDesktop.chooseDirectory({
334+
title: '请选择微信安装目录'
335+
})
336+
if (!res || res.canceled || !res.filePaths?.length) return
337+
path = res.filePaths[0]
338+
} else {
339+
try {
340+
const res = await pickSystemDirectory({
341+
title: '请选择微信安装目录',
342+
initial_dir: normalizeWechatInstallPath(wechatInstallPath.value)
343+
})
344+
if (!res || !res.path) return
345+
path = res.path
346+
} catch (e) {
347+
console.error('通过API唤起微信安装目录选择器失败:', e)
348+
path = window.prompt('无法直接唤起窗口,请输入微信安装目录或 Weixin.exe / WeChat.exe 的绝对路径:')
349+
if (!path) return
350+
}
351+
}
352+
353+
const normalized = normalizeWechatInstallPath(path)
354+
if (!normalized) return
355+
wechatInstallPath.value = normalized
356+
persistWechatInstallPath()
357+
} catch (e) {
358+
console.error('选择微信安装目录失败:', e)
359+
} finally {
360+
isPickingWechatInstallPath.value = false
361+
}
362+
}
363+
364+
// ?????????????????
292365
const sortedAccounts = computed(() => {
293366
if (!detectionResult.value?.data?.accounts) return []
294367
const accounts = [...detectionResult.value.data.accounts]
@@ -384,6 +457,8 @@ const startDetection = async () => {
384457
385458
// 跳转到解密页面并传递账户信息
386459
const goToDecrypt = (account) => {
460+
persistWechatInstallPath()
461+
387462
if (process.client && typeof window !== 'undefined') {
388463
sessionStorage.setItem('selectedAccount', JSON.stringify({
389464
account_name: account.account_name,
@@ -412,6 +487,7 @@ onMounted(() => {
412487
const saved = String(localStorage.getItem(STORAGE_KEY) || '').trim()
413488
if (saved) customPath.value = saved
414489
} catch {}
490+
wechatInstallPath.value = readStoredWechatInstallPath()
415491
}
416492
startDetection()
417493
})

src/wechat_decrypt_tool/key_service.py

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
logger = logging.getLogger(__name__)
3131

32+
WECHAT_EXECUTABLE_NAMES = ("Weixin.exe", "WeChat.exe")
33+
3234

3335
def _summarize_aes_key(value: Any) -> str:
3436
raw = str(value or "").strip()
@@ -109,19 +111,72 @@ def _resolve_wxid_dir_for_image_key(
109111
raise FileNotFoundError("无法定位该账号的 wxid_dir,请传入有效的 db_storage_path 或先完成数据库解密")
110112

111113

114+
def _normalize_user_path(value: Any) -> str:
115+
raw = str(value or "").strip().strip('"').strip("'")
116+
if not raw:
117+
return ""
118+
try:
119+
return os.path.normpath(os.path.expandvars(raw))
120+
except Exception:
121+
return raw
122+
123+
124+
def _read_wechat_version_from_exe(exe_path: str) -> str:
125+
normalized = _normalize_user_path(exe_path)
126+
if not normalized:
127+
return ""
128+
try:
129+
import win32api
130+
131+
version_info = win32api.GetFileVersionInfo(normalized, "\\")
132+
return (
133+
f"{version_info['FileVersionMS'] >> 16}."
134+
f"{version_info['FileVersionMS'] & 0xFFFF}."
135+
f"{version_info['FileVersionLS'] >> 16}."
136+
f"{version_info['FileVersionLS'] & 0xFFFF}"
137+
)
138+
except Exception:
139+
return ""
140+
141+
142+
def _resolve_manual_wechat_exe_path(wechat_install_path: Optional[str] = None) -> str:
143+
normalized = _normalize_user_path(wechat_install_path)
144+
if not normalized:
145+
return ""
146+
147+
candidate = Path(normalized).expanduser()
148+
executable_names = {name.lower() for name in WECHAT_EXECUTABLE_NAMES}
149+
if candidate.is_file():
150+
if candidate.name.lower() not in executable_names:
151+
raise RuntimeError("手动路径必须指向微信安装目录,或直接指向 Weixin.exe / WeChat.exe")
152+
return str(candidate)
153+
154+
if candidate.is_dir():
155+
for exe_name in WECHAT_EXECUTABLE_NAMES:
156+
exe_path = candidate / exe_name
157+
if exe_path.is_file():
158+
return str(exe_path)
159+
raise RuntimeError("手动指定的微信安装目录中未找到 Weixin.exe 或 WeChat.exe")
160+
161+
raise RuntimeError(f"手动指定的微信安装目录不存在: {candidate}")
162+
163+
112164
# ====================== 以下是hook逻辑 ======================================
113165

114166
class WeChatKeyFetcher:
115167
def __init__(self):
116-
self.process_name = "Weixin.exe"
168+
self.process_names = {name.lower() for name in WECHAT_EXECUTABLE_NAMES}
117169
self.timeout_seconds = 60
118170

171+
def _is_wechat_process(self, name: Any) -> bool:
172+
return str(name or "").strip().lower() in self.process_names
173+
119174
def kill_wechat(self):
120175
"""检测并查杀微信进程"""
121176
killed = False
122177
for proc in psutil.process_iter(['pid', 'name']):
123178
try:
124-
if proc.info['name'] == self.process_name:
179+
if self._is_wechat_process(proc.info['name']):
125180
logger.info(f"Killing WeChat process: {proc.info['pid']}")
126181
proc.terminate()
127182
killed = True
@@ -134,11 +189,14 @@ def kill_wechat(self):
134189
def launch_wechat(self, exe_path: str) -> int:
135190
"""启动微信并返回 PID"""
136191
try:
137-
process = subprocess.Popen(exe_path)
192+
normalized_exe_path = _normalize_user_path(exe_path)
193+
process = subprocess.Popen(normalized_exe_path)
138194
time.sleep(2)
139195
candidates = []
196+
target_process_name = Path(normalized_exe_path).name.lower()
140197
for proc in psutil.process_iter(['pid', 'name', 'create_time']):
141-
if proc.info['name'] == self.process_name:
198+
proc_name = str(proc.info.get('name') or "").strip().lower()
199+
if proc_name == target_process_name or self._is_wechat_process(proc_name):
142200
candidates.append(proc)
143201

144202
if candidates:
@@ -152,19 +210,32 @@ def launch_wechat(self, exe_path: str) -> int:
152210
logger.error(f"启动微信失败: {e}")
153211
raise RuntimeError(f"无法启动微信: {e}")
154212

155-
def fetch_db_key(self) -> dict:
213+
def fetch_db_key(self, wechat_install_path: Optional[str] = None) -> dict:
156214
"""调用 wx_key 仅获取数据库密钥 (Hook 模式)"""
157215
if wx_key is None:
158216
raise RuntimeError("wx_key 模块未安装或加载失败")
159217

160-
install_info = detect_wechat_installation()
161-
exe_path = install_info.get('wechat_exe_path')
162-
version = install_info.get('wechat_version')
218+
manual_path = _normalize_user_path(wechat_install_path)
219+
if manual_path:
220+
exe_path = _resolve_manual_wechat_exe_path(manual_path)
221+
version = _read_wechat_version_from_exe(exe_path)
222+
logger.info(
223+
"[db_key] 使用手动指定的微信安装路径: input=%s exe_path=%s version=%s",
224+
manual_path,
225+
exe_path,
226+
version or "unknown",
227+
)
228+
else:
229+
install_info = detect_wechat_installation()
230+
exe_path = _normalize_user_path(install_info.get('wechat_exe_path'))
231+
version = str(install_info.get('wechat_version') or "").strip()
163232

164-
if not exe_path or not version:
165-
raise RuntimeError("无法自动定位微信安装路径或版本")
233+
if not exe_path:
234+
raise RuntimeError("无法自动定位微信安装路径,请手动填写微信安装目录")
235+
if not Path(exe_path).is_file():
236+
raise RuntimeError(f"微信可执行文件不存在: {exe_path}")
166237

167-
logger.info(f"Detect WeChat: {version} at {exe_path}")
238+
logger.info(f"Detect WeChat: {version or 'unknown'} at {exe_path}")
168239

169240
self.kill_wechat()
170241
pid = self.launch_wechat(exe_path)
@@ -204,9 +275,9 @@ def fetch_db_key(self) -> dict:
204275
"db_key": found_db_key
205276
}
206277

207-
def get_db_key_workflow():
278+
def get_db_key_workflow(wechat_install_path: Optional[str] = None):
208279
fetcher = WeChatKeyFetcher()
209-
return fetcher.fetch_db_key()
280+
return fetcher.fetch_db_key(wechat_install_path=wechat_install_path)
210281

211282

212283
# ============================== 以下是图片密钥逻辑 =====================================

0 commit comments

Comments
 (0)