diff --git a/src/main/image-decrypt-service.ts b/src/main/image-decrypt-service.ts index 8ed1aac..b1cab03 100644 --- a/src/main/image-decrypt-service.ts +++ b/src/main/image-decrypt-service.ts @@ -10,8 +10,6 @@ const imageDecryptLog = (...args: unknown[]): void => { } export class ImageDecryptService { - private readonly defaultV1AesKey = 'cfcd208495d565ef' - private xorKey: number = 0 private aesKey: string = '' private wcdb4Client: Wcdb4Client | null = null @@ -80,9 +78,11 @@ export class ImageDecryptService { findImageFile( md5?: string, imageDatName?: string, - options?: { allowThumbnail?: boolean } + options?: { allowThumbnail?: boolean; accountDir?: string } ): string | null { - const accountDir = this.getAccountDir() + // 测试场景下可显式指定根目录;不传则维持原 getAccountDir() 行为 + const accountDir = + options?.accountDir && existsSync(options.accountDir) ? options.accountDir : this.getAccountDir() if (!accountDir) return null const allowThumbnail = options?.allowThumbnail !== false @@ -273,12 +273,9 @@ export class ImageDecryptService { ) let decrypted: Buffer - if (version === 1) { - imageDecryptLog('[ImageDecrypt] using V1 (default AES key)') - const key = Buffer.from(this.defaultV1AesKey, 'ascii') - decrypted = this.decryptDatV4(datPath, key) - } else if (version === 2) { - imageDecryptLog('[ImageDecrypt] using V2 (user AES key)') + if (version === 2) { + // WeChat 4.0 标准 dat 头: 07 08 56 32 08 07 + imageDecryptLog('[ImageDecrypt] using WeChat 4.0 (user AES key)') if (!this.aesKey) { imageDecryptLog('[ImageDecrypt] no AES key configured') return null @@ -286,7 +283,8 @@ export class ImageDecryptService { const key = Buffer.from(this.aesKey, 'ascii').slice(0, 16) decrypted = this.decryptDatV4(datPath, key) } else { - imageDecryptLog('[ImageDecrypt] unsupported dat version:', version) + // 仅支持 WeChat 4.0:版本不匹配直接返回 null,不做 V3/老版本兜底。 + imageDecryptLog('[ImageDecrypt] unsupported dat version (WeChat 4.0 only):', version) return null } @@ -356,7 +354,8 @@ export class ImageDecryptService { } /** - * 检测 DAT 文件版本 + * 检测 DAT 文件版本(仅识别 WeChat 4.0 头 V2)。 + * 老 V1 头(V3 及以下)直接返回 0,由调用方走"不支持"分支。 */ private getDatVersion(inputPath: string): number { const bytes = readFileSync(inputPath) @@ -365,9 +364,6 @@ export class ImageDecryptService { } const signature = bytes.subarray(0, 6) - if (this.compareBytes(signature, Buffer.from([0x07, 0x08, 0x56, 0x31, 0x08, 0x07]))) { - return 1 - } if (this.compareBytes(signature, Buffer.from([0x07, 0x08, 0x56, 0x32, 0x08, 0x07]))) { return 2 } diff --git a/src/main/key-service-win.ts b/src/main/key-service-win.ts index e5bd6b4..33b917d 100644 --- a/src/main/key-service-win.ts +++ b/src/main/key-service-win.ts @@ -889,7 +889,8 @@ export class KeyService { const dirName = normalized.split(/[\\/]/).pop() ?? '' if (dirName.startsWith('wxid_')) pushUnique(dirName) - const marker = normalized.match(/[\\/]xwechat_files/i) || normalized.match(/[\\/]WeChat Files/i) + // 仅支持 WeChat 4.0:路径识别只匹配 xwechat_files + const marker = normalized.match(/[\\/]xwechat_files/i) if (marker) { const root = normalized.slice(0, marker.index! + marker[0].length) try { @@ -934,15 +935,49 @@ export class KeyService { onProgress?.('正在查找模板文件...') let result = await this._findTemplateData(userDir, 32) let { ciphertext, xorKey } = result - + const firstDiag = (this as { _imageTemplateDiag?: { + userDir: string; totalTFiles: number; v2Count: number; nonV2Count: number + } })._imageTemplateDiag + // 如果找不到密钥,尝试扫描更多文件 if (ciphertext && xorKey === null) { onProgress?.('未找到有效密钥,尝试扫描更多文件...') result = await this._findTemplateData(userDir, 100) xorKey = result.xorKey } - - if (!ciphertext) return { success: false, error: '未找到 V2 模板文件,请先在微信中查看几张图片' } + + if (!ciphertext) { + // 用诊断信息给具体提示 + const diag = (this as { _imageTemplateDiag?: { + userDir: string; totalTFiles: number; v2Count: number; nonV2Count: number + } })._imageTemplateDiag || firstDiag + if (!diag || diag.totalTFiles === 0) { + return { + success: false, + error: + '在账号目录下未找到任何 _t.dat 图片文件。\n' + + `扫描路径:${diag?.userDir || userDir || '(空)'}\n` + + '原因:微信没在本地生成缩略图。\n' + + '请让用户在微信里打开任意聊天的图片大图(等"原图"按钮可点击),然后再试。' + } + } + if (diag.v2Count === 0 && diag.nonV2Count > 0) { + return { + success: false, + error: + `找到 ${diag.totalTFiles} 个 _t.dat,但都不是 V2 头(可能图片尚未解密到本地,或微信版本不同)。\n` + + `扫描路径:${diag.userDir}\n` + + '请让用户在微信里打开 2-3 张不同的图片大图,等"原图"按钮可点击后再试。' + } + } + return { + success: false, + error: + `找到 ${diag.totalTFiles} 个 _t.dat,其中 ${diag.v2Count} 个是 V2 头,但没有长度 ≥ 0x1F 的有效模板。\n` + + `扫描路径:${diag.userDir}\n` + + '请在微信中查看更多图片后再试。' + } + } if (xorKey === null) return { success: false, error: '未能从模板文件中计算出有效的 XOR 密钥,请确保在微信中查看了多张不同的图片' } onProgress?.(`XOR 密钥: 0x${xorKey.toString(16).padStart(2, '0')},正在查找微信进程...`) @@ -1005,6 +1040,8 @@ export class KeyService { let ciphertext: Buffer | null = null const tailCounts: Record = {} + let v2Count = 0 + let nonV2Count = 0 for (const f of files.slice(0, 32)) { try { @@ -1013,8 +1050,11 @@ export class KeyService { // 统计末尾两字节用于 XOR 密钥 if (data.subarray(0, 6).equals(V2_MAGIC) && data.length >= 2) { + v2Count++ const key = `${data[data.length - 2]}_${data[data.length - 1]}` tailCounts[key] = (tailCounts[key] ?? 0) + 1 + } else { + nonV2Count++ } // 提取密文(取第一个有效的) @@ -1031,6 +1071,15 @@ export class KeyService { if (count > maxCount) { maxCount = count; const [x, y] = key.split('_').map(Number); const k = x ^ 0xFF; if (k === (y ^ 0xD9)) xorKey = k } } + // 诊断信息:远程排查时让 UI 直接告诉用户搜到了什么 + const diag = { + userDir, + totalTFiles: files.length, + v2Count, + nonV2Count + } + ;(this as { _imageTemplateDiag?: unknown })._imageTemplateDiag = diag + return { ciphertext, xorKey } }