This commit is contained in:
xuncha
2026-02-28 21:18:57 +08:00
parent 83d501ae9b
commit b2ef8f5cd2
4 changed files with 72 additions and 28 deletions

View File

@@ -3438,9 +3438,10 @@ class ChatService {
const datPath = await this.findDatFile(actualAccountDir, baseName, sessionId)
if (!datPath) return { success: false, error: '未找到图片源文件 (.dat)' }
// 4. 获取解密密钥
const xorKeyRaw = this.configService.get('imageXorKey')
const aesKeyRaw = this.configService.get('imageAesKey') || msg.aesKey
// 4. 获取解密密钥(优先使用当前 wxid 对应的密钥)
const imageKeys = this.configService.getImageKeysForCurrentWxid()
const xorKeyRaw = imageKeys.xorKey
const aesKeyRaw = imageKeys.aesKey || msg.aesKey
if (!xorKeyRaw) return { success: false, error: '未配置图片 XOR 密钥,请在设置中自动获取' }

View File

@@ -637,6 +637,27 @@ export class ConfigService {
// === 工具方法 ===
/**
* 获取当前 wxid 对应的图片密钥,优先从 wxidConfigs 中取,找不到则回退到全局配置
*/
getImageKeysForCurrentWxid(): { xorKey: unknown; aesKey: string } {
const wxid = this.get('myWxid')
if (wxid) {
const wxidConfigs = this.get('wxidConfigs')
const cfg = wxidConfigs?.[wxid]
if (cfg && (cfg.imageXorKey !== undefined || cfg.imageAesKey)) {
return {
xorKey: cfg.imageXorKey ?? this.get('imageXorKey'),
aesKey: cfg.imageAesKey ?? this.get('imageAesKey')
}
}
}
return {
xorKey: this.get('imageXorKey'),
aesKey: this.get('imageAesKey')
}
}
getCacheBasePath(): string {
return join(app.getPath('userData'), 'cache')
}

View File

@@ -240,7 +240,9 @@ export class ImageDecryptService {
}
}
const xorKeyRaw = this.configService.get('imageXorKey') as unknown
// 优先使用当前 wxid 对应的密钥,找不到则回退到全局配置
const imageKeys = this.configService.getImageKeysForCurrentWxid()
const xorKeyRaw = imageKeys.xorKey
// 支持十六进制格式(如 0x53和十进制格式
let xorKey: number
if (typeof xorKeyRaw === 'number') {
@@ -257,7 +259,7 @@ export class ImageDecryptService {
return { success: false, error: '未配置图片解密密钥' }
}
const aesKeyRaw = this.configService.get('imageAesKey')
const aesKeyRaw = imageKeys.aesKey
const aesKey = this.resolveAesKey(aesKeyRaw)
this.logInfo('开始解密DAT文件', { datPath, xorKey, hasAesKey: !!aesKey })

View File

@@ -4,6 +4,7 @@ import { existsSync, copyFileSync, mkdirSync } from 'fs'
import { execFile, spawn } from 'child_process'
import { promisify } from 'util'
import os from 'os'
import crypto from 'crypto'
const execFileAsync = promisify(execFile)
@@ -637,7 +638,16 @@ export class KeyService {
return { success: false, error: '获取密钥超时', logs }
}
// --- Image Key (通过 DLL 从缓存目录直接获取) ---
// --- Image Key (通过 DLL 从缓存目录获取 code用前端 wxid 计算密钥) ---
private cleanWxid(wxid: string): string {
// 截断到第二个下划线: wxid_g4pshorcc0r529_da6c → wxid_g4pshorcc0r529
const first = wxid.indexOf('_')
if (first === -1) return wxid
const second = wxid.indexOf('_', first + 1)
if (second === -1) return wxid
return wxid.substring(0, second)
}
async autoGetImageKey(
manualDir?: string,
@@ -664,41 +674,51 @@ export class KeyService {
return { success: false, error: '解析密钥数据失败' }
}
// 从 manualDir 中提取 wxid 用于精确匹配
// 前端传入的格式是 dbPath/wxid_xxx_1234取最后一段目录名再清理后缀
let targetWxid: string | null = null
// 从任意账号提取 code 列表code 来自 kvcomm与 wxid 无关,所有账号都一样)
const accounts: any[] = parsed.accounts ?? []
if (!accounts.length || !accounts[0]?.keys?.length) {
return { success: false, error: '未找到有效的密钥码kvcomm 缓存为空)' }
}
const codes: number[] = accounts[0].keys.map((k: any) => k.code)
console.log('[ImageKey] codes:', codes, 'DLL wxids:', accounts.map((a: any) => a.wxid))
// 从 manualDir 提取前端已配置好的正确 wxid
// 格式: "D:\weixin\xwechat_files\wxid_xxx_1234" → "wxid_xxx_1234"
let targetWxid = ''
if (manualDir) {
const dirName = manualDir.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? ''
// 与 DLL 的 CleanWxid 逻辑一致wxid_a_b_c → wxid_a
const parts = dirName.split('_')
if (parts.length >= 3 && parts[0] === 'wxid') {
targetWxid = `${parts[0]}_${parts[1]}`
} else if (dirName.startsWith('wxid_')) {
if (dirName.startsWith('wxid_')) {
targetWxid = dirName
}
}
const accounts: any[] = parsed.accounts ?? []
if (!accounts.length) {
return { success: false, error: '未找到有效的密钥组合' }
if (!targetWxid) {
// 无法从 manualDir 提取 wxid回退到 DLL 发现的第一个
targetWxid = accounts[0].wxid
console.log('[ImageKey] 无法从 manualDir 提取 wxid使用 DLL 发现的:', targetWxid)
}
// 优先匹配 wxid找不到则回退到第一个
const matchedAccount = targetWxid
? (accounts.find((a: any) => a.wxid === targetWxid) ?? accounts[0])
: accounts[0]
// CleanWxid: 截断到第二个下划线,与 xkey 算法一致
const cleanedWxid = this.cleanWxid(targetWxid)
console.log('[ImageKey] wxid:', targetWxid, '→ cleaned:', cleanedWxid)
if (!matchedAccount?.keys?.length) {
return { success: false, error: '未找到有效的密钥组合' }
}
// 用 cleanedWxid + code 本地计算密钥
// xorKey = code & 0xFF
// aesKey = MD5(code.toString() + cleanedWxid).substring(0, 16)
const code = codes[0]
const xorKey = code & 0xFF
const dataToHash = code.toString() + cleanedWxid
const md5Full = crypto.createHash('md5').update(dataToHash).digest('hex')
const aesKey = md5Full.substring(0, 16)
const firstKey = matchedAccount.keys[0]
onProgress?.(`密钥获取成功 (wxid: ${matchedAccount.wxid}, code: ${firstKey.code})`)
onProgress?.(`密钥获取成功 (wxid: ${targetWxid}, code: ${code})`)
console.log('[ImageKey] 计算结果: xorKey=', xorKey, 'aesKey=', aesKey)
return {
success: true,
xorKey: firstKey.xorKey,
aesKey: firstKey.aesKey
xorKey,
aesKey
}
}
}