Skip to content

Commit b0230dc

Browse files
committed
fix(key): 支持通过 db_storage_path 精确获取当前账号图片密钥
- 前端获取图片密钥时补充透传 db_storage_path / wxid_dir - 后端支持通过 db_storage_path 反推出目标 wxid_dir - 本地图片密钥匹配改为账号精确匹配,避免子串误命中 - 切换账号时重置并重新预填密钥,避免跨账号串用旧密钥 - 增加单测,覆盖精确匹配和未完成数据库解密时的远程获取场景
1 parent 7c201b4 commit b0230dc

5 files changed

Lines changed: 284 additions & 57 deletions

File tree

frontend/composables/useApi.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,8 @@ export const useApi = () => {
578578
const getImageKey = async (params = {}) => {
579579
const query = new URLSearchParams()
580580
if (params && params.account) query.set('account', params.account)
581+
if (params && params.db_storage_path) query.set('db_storage_path', params.db_storage_path)
582+
if (params && params.wxid_dir) query.set('wxid_dir', params.wxid_dir)
581583
const url = '/get_image_key' + (query.toString() ? `?${query.toString()}` : '')
582584

583585
return await request(url)

frontend/pages/decrypt.vue

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ const error = ref('')
448448
const warning = ref('') // 警告,用于密钥提示
449449
const currentStep = ref(0)
450450
const mediaAccount = ref('')
451+
const activeKeyAccount = ref('')
451452
const isGettingDbKey = ref(false)
452453
453454
// 步骤定义
@@ -485,6 +486,8 @@ const manualKeyErrors = reactive({
485486
aes_key: ''
486487
})
487488
489+
const normalizeAccountId = (value) => String(value || '').trim()
490+
488491
const normalizeXorKey = (value) => {
489492
const raw = String(value || '').trim()
490493
if (!raw) return { ok: false, value: '', message: '请输入 XOR 密钥' }
@@ -503,7 +506,7 @@ const normalizeAesKey = (value) => {
503506
}
504507
505508
const prefillKeysForAccount = async (account) => {
506-
const acc = String(account || '').trim()
509+
const acc = normalizeAccountId(account)
507510
if (!acc) return
508511
try {
509512
const resp = await getSavedKeys({ account: acc })
@@ -529,6 +532,44 @@ const prefillKeysForAccount = async (account) => {
529532
}
530533
}
531534
535+
const tryAutoFetchImageKeys = async (account) => {
536+
const acc = normalizeAccountId(account)
537+
if (!acc) return
538+
if (String(manualKeys.xor_key || '').trim() || String(manualKeys.aes_key || '').trim()) return
539+
540+
warning.value = '正在通过云端/本地算法自动获取图片密钥,请稍候...'
541+
try {
542+
const imgRes = await getImageKey({
543+
account: acc,
544+
db_storage_path: String(formData.db_storage_path || '').trim()
545+
})
546+
547+
if (imgRes && imgRes.status === 0) {
548+
if (imgRes.data?.xor_key) manualKeys.xor_key = imgRes.data.xor_key
549+
if (imgRes.data?.aes_key) manualKeys.aes_key = imgRes.data.aes_key
550+
warning.value = '已通过云端成功获取图片密钥!'
551+
setTimeout(() => { if (warning.value.includes('成功获取')) warning.value = '' }, 3000)
552+
} else {
553+
warning.value = '云端获取图片密钥失败,您可以尝试手动填写。'
554+
}
555+
} catch (e) {
556+
warning.value = '网络请求失败,请手动填写图片密钥。'
557+
}
558+
}
559+
560+
const ensureKeysForAccount = async (account) => {
561+
const acc = normalizeAccountId(account)
562+
if (!acc) return
563+
564+
if (activeKeyAccount.value && activeKeyAccount.value !== acc) {
565+
clearManualKeys()
566+
}
567+
568+
activeKeyAccount.value = acc
569+
await prefillKeysForAccount(acc)
570+
await tryAutoFetchImageKeys(acc)
571+
}
572+
532573
const handleGetDbKey = async () => {
533574
if (isGettingDbKey.value) return
534575
isGettingDbKey.value = true
@@ -605,6 +646,7 @@ const clearManualKeys = () => {
605646
manualKeyErrors.aes_key = ''
606647
mediaKeys.xor_key = ''
607648
mediaKeys.aes_key = ''
649+
activeKeyAccount.value = ''
608650
}
609651
610652
// 图片解密相关
@@ -759,26 +801,7 @@ const handleDecrypt = async () => {
759801
} catch (e) {}
760802
761803
currentStep.value = 1
762-
await prefillKeysForAccount(mediaAccount.value)
763-
764-
if (!manualKeys.xor_key && !manualKeys.aes_key) {
765-
warning.value = '正在通过云端/本地算法自动获取图片密钥,请稍候...'
766-
try {
767-
const params = mediaAccount.value ? { account: mediaAccount.value } : {}
768-
const imgRes = await getImageKey(params)
769-
770-
if (imgRes && imgRes.status === 0) {
771-
if (imgRes.data?.xor_key) manualKeys.xor_key = imgRes.data.xor_key
772-
if (imgRes.data?.aes_key) manualKeys.aes_key = imgRes.data.aes_key
773-
warning.value = '已通过云端成功获取图片密钥!'
774-
setTimeout(() => { if(warning.value.includes('成功获取')) warning.value = '' }, 3000)
775-
} else {
776-
warning.value = '云端获取图片密钥失败,您可以尝试手动填写。'
777-
}
778-
} catch (e) {
779-
warning.value = '网络请求失败,请手动填写图片密钥。'
780-
}
781-
}
804+
await ensureKeysForAccount(mediaAccount.value)
782805
783806
} else if (result.status === 'failed') {
784807
if (result.failure_count > 0 && result.success_count === 0) {
@@ -863,25 +886,7 @@ const handleDecrypt = async () => {
863886
864887
if (data.status === 'completed') {
865888
currentStep.value = 1
866-
await prefillKeysForAccount(mediaAccount.value)
867-
868-
if (!manualKeys.xor_key && !manualKeys.aes_key) {
869-
warning.value = '正在通过云端/本地算法自动获取图片密钥,请稍候...'
870-
try {
871-
const params = mediaAccount.value ? { account: mediaAccount.value } : {}
872-
const imgRes = await getImageKey(params)
873-
if (imgRes && imgRes.status === 0) {
874-
if (imgRes.data?.xor_key) manualKeys.xor_key = imgRes.data.xor_key
875-
if (imgRes.data?.aes_key) manualKeys.aes_key = imgRes.data.aes_key
876-
warning.value = '已通过云端成功获取图片密钥!'
877-
setTimeout(() => { if(warning.value.includes('成功获取')) warning.value = '' }, 3000)
878-
} else {
879-
warning.value = '云端获取图片密钥失败,您可以尝试手动填写。'
880-
}
881-
} catch (e) {
882-
warning.value = '网络请求失败,请手动填写图片密钥。'
883-
}
884-
}
889+
await ensureKeysForAccount(mediaAccount.value)
885890
} else if (data.status === 'failed') {
886891
error.value = data.message || '所有文件解密失败'
887892
} else {
@@ -1063,7 +1068,7 @@ onMounted(async () => {
10631068
}
10641069
// 清除sessionStorage
10651070
sessionStorage.removeItem('selectedAccount')
1066-
await prefillKeysForAccount(mediaAccount.value)
1071+
await ensureKeysForAccount(mediaAccount.value)
10671072
} catch (e) {
10681073
console.error('解析账户信息失败:', e)
10691074
}

src/wechat_decrypt_tool/key_service.py

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,43 @@
3030
logger = logging.getLogger(__name__)
3131

3232

33+
def _resolve_wxid_dir_for_image_key(
34+
account: Optional[str] = None,
35+
*,
36+
wxid_dir: Optional[str] = None,
37+
db_storage_path: Optional[str] = None,
38+
) -> Path:
39+
explicit_wxid_dir = str(wxid_dir or "").strip()
40+
if explicit_wxid_dir:
41+
candidate = Path(explicit_wxid_dir).expanduser()
42+
if candidate.exists() and candidate.is_dir():
43+
return candidate
44+
raise FileNotFoundError(f"指定的 wxid_dir 不存在或不是目录: {candidate}")
45+
46+
explicit_db_storage_path = str(db_storage_path or "").strip()
47+
if explicit_db_storage_path:
48+
db_storage_dir = Path(explicit_db_storage_path).expanduser()
49+
if db_storage_dir.exists() and db_storage_dir.is_dir():
50+
if db_storage_dir.name.lower() == "db_storage":
51+
candidate = db_storage_dir.parent
52+
if candidate.exists() and candidate.is_dir():
53+
return candidate
54+
nested_db_storage = db_storage_dir / "db_storage"
55+
if nested_db_storage.exists() and nested_db_storage.is_dir():
56+
return db_storage_dir
57+
58+
if account:
59+
try:
60+
account_dir = _resolve_account_dir(account)
61+
wx_id_dir = _resolve_account_wxid_dir(account_dir)
62+
if wx_id_dir:
63+
return wx_id_dir
64+
except Exception:
65+
pass
66+
67+
raise FileNotFoundError("无法定位该账号的 wxid_dir,请传入有效的 db_storage_path 或先完成数据库解密")
68+
69+
3370
# ====================== 以下是hook逻辑 ======================================
3471

3572
class WeChatKeyFetcher:
@@ -171,7 +208,12 @@ def try_get_local_image_keys() -> List[Dict[str, Any]]:
171208
return []
172209

173210

174-
async def get_image_key_integrated_workflow(account: Optional[str] = None) -> Dict[str, Any]:
211+
async def get_image_key_integrated_workflow(
212+
account: Optional[str] = None,
213+
*,
214+
wxid_dir: Optional[str] = None,
215+
db_storage_path: Optional[str] = None,
216+
) -> Dict[str, Any]:
175217
"""
176218
集成图片密钥获取流程:
177219
1. 优先尝试本地算法提取
@@ -181,22 +223,26 @@ async def get_image_key_integrated_workflow(account: Optional[str] = None) -> Di
181223
local_keys = try_get_local_image_keys()
182224

183225
target_account_wxid = None
184-
if account:
226+
if account or wxid_dir or db_storage_path:
185227
try:
186-
account_dir = _resolve_account_dir(account)
187-
wx_id_dir = _resolve_account_wxid_dir(account_dir)
188-
target_account_wxid = wx_id_dir.name
189-
except:
228+
resolved_wxid_dir = _resolve_wxid_dir_for_image_key(
229+
account,
230+
wxid_dir=wxid_dir,
231+
db_storage_path=db_storage_path,
232+
)
233+
target_account_wxid = resolved_wxid_dir.name
234+
except Exception:
190235
target_account_wxid = account
236+
target_account_wxid = str(target_account_wxid or "").strip().lower()
191237

192238
if local_keys:
193239
# 如果指定了账号,尝试在本地结果中找匹配的
194240
if target_account_wxid:
195241
for k in local_keys:
196-
if k['wxid'] in target_account_wxid:
197-
logger.info(f"成功通过本地算法匹配到账号 {target_account_wxid} 的图片密钥")
242+
local_wxid = str(k.get("wxid") or "").strip().lower()
243+
if local_wxid and local_wxid == target_account_wxid:
198244
upsert_account_keys_in_store(
199-
account=k['wxid'],
245+
account=str(k.get("wxid") or "").strip(),
200246
image_xor_key=k['xor_key'],
201247
image_aes_key=k['aes_key']
202248
)
@@ -215,12 +261,24 @@ async def get_image_key_integrated_workflow(account: Optional[str] = None) -> Di
215261

216262
# 2. 本地提取失败或不匹配,尝试远程解析
217263
logger.info("本地算法提取未命中,尝试远程 API 解析...")
218-
return await fetch_and_save_remote_keys(account)
264+
return await fetch_and_save_remote_keys(
265+
account,
266+
wxid_dir=wxid_dir,
267+
db_storage_path=db_storage_path,
268+
)
219269

220270

221-
async def fetch_and_save_remote_keys(account: Optional[str] = None) -> Dict[str, Any]:
222-
account_dir = _resolve_account_dir(account)
223-
wx_id_dir = _resolve_account_wxid_dir(account_dir)
271+
async def fetch_and_save_remote_keys(
272+
account: Optional[str] = None,
273+
*,
274+
wxid_dir: Optional[str] = None,
275+
db_storage_path: Optional[str] = None,
276+
) -> Dict[str, Any]:
277+
wx_id_dir = _resolve_wxid_dir_for_image_key(
278+
account,
279+
wxid_dir=wxid_dir,
280+
db_storage_path=db_storage_path,
281+
)
224282
wxid = wx_id_dir.name
225283

226284
url = "https://view.free.c3o.re/api/key"
@@ -274,4 +332,4 @@ async def fetch_and_save_remote_keys(account: Optional[str] = None) -> Dict[str,
274332
"xor_key": xor_hex_str,
275333
"aes_key": aes_val,
276334
"nick_name": config.get("nickName", config.get("nick_name", ""))
277-
}
335+
}

src/wechat_decrypt_tool/routers/keys.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@ async def get_wechat_db_key():
8787

8888

8989
@router.get("/api/get_image_key", summary="获取并保存微信图片密钥")
90-
async def get_image_key(account: Optional[str] = None):
90+
async def get_image_key(
91+
account: Optional[str] = None,
92+
db_storage_path: Optional[str] = None,
93+
wxid_dir: Optional[str] = None,
94+
):
9195
"""
9296
通过模拟 Next.js Server Action 协议,利用本地微信配置文件换取 AES/XOR 密钥。
9397
@@ -97,7 +101,11 @@ async def get_image_key(account: Optional[str] = None):
97101
4. 解析返回流,自动存入本地数据库
98102
"""
99103
try:
100-
result = await get_image_key_integrated_workflow(account)
104+
result = await get_image_key_integrated_workflow(
105+
account,
106+
db_storage_path=db_storage_path,
107+
wxid_dir=wxid_dir,
108+
)
101109

102110
return {
103111
"status": 0,

0 commit comments

Comments
 (0)