mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
fix(image-decrypt): 仅支持 WeChat 4.0,移除 V3 兜底 + 模板诊断
WeChat 4.0 dat 文件头为 07 08 56 32 08 07。V3 / 老版本(V1 头)不在支持范围。
image-decrypt-service:
- 移除 defaultV1AesKey ('cfcd208495d565ef') 字段。
- 移除 if (version === 1) 的 V1 默认 key 分支。
- getDatVersion 只识别 V2 头;其他返回 0 走 unsupported。
- 注释说明 decryptDatV4 方法名里的 V4 是历史命名,跟协议版本无关。
key-service-win._findTemplateData / autoGetImageKeyByMemoryScan:
- 统计扫描诊断信息:搜索到的 _t.dat 数量、V2 头数量、非 V2 数量、扫描根目录。
- 把模糊的「未找到 V2 模板文件」拆成三种具体提示:
1) 目录下完全没有 _t.dat → 让用户先在微信里点开图片大图(缩略图未生成)。
2) 有 _t.dat 但都不是 V2 头 → 让用户查看更多图片。
3) 有 V2 但没有有效长度 → 让用户查看更多图片。
- 每条都附带实际扫描根目录,便于远程排查时一眼看出路径是否正确。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string, number> = {}
|
||||
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 }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user