fix: jpg解密修复

This commit is contained in:
Forrest
2026-01-17 01:23:38 +08:00
parent 6eabd707f8
commit f97e102dbd
2 changed files with 186 additions and 4 deletions

View File

@@ -8,6 +8,19 @@ import { Worker } from 'worker_threads'
import { ConfigService } from './config'
import { wcdbService } from './wcdbService'
// 获取 ffmpeg-static 的路径
function getStaticFfmpegPath(): string | null {
try {
const ffmpegStatic = require('ffmpeg-static')
if (typeof ffmpegStatic === 'string') {
return ffmpegStatic
}
return null
} catch {
return null
}
}
type DecryptResult = {
success: boolean
localPath?: string
@@ -238,20 +251,39 @@ export class ImageDecryptService {
const aesKey = this.resolveAesKey(aesKeyRaw)
this.logInfo('开始解密DAT文件', { datPath, xorKey, hasAesKey: !!aesKey })
const decrypted = await this.decryptDatAuto(datPath, xorKey, aesKey)
let decrypted = await this.decryptDatAuto(datPath, xorKey, aesKey)
const ext = this.detectImageExtension(decrypted) || '.jpg'
// 检查是否是 wxgf 格式,如果是则尝试提取真实图片数据
const wxgfResult = await this.unwrapWxgf(decrypted)
decrypted = wxgfResult.data
const outputPath = this.getCacheOutputPathFromDat(datPath, ext, payload.sessionId)
let ext = this.detectImageExtension(decrypted)
// 如果是 wxgf 格式且没检测到扩展名
if (wxgfResult.isWxgf && !ext) {
ext = '.hevc'
}
const finalExt = ext || '.jpg'
const outputPath = this.getCacheOutputPathFromDat(datPath, finalExt, payload.sessionId)
await writeFile(outputPath, decrypted)
this.logInfo('解密成功', { outputPath, size: decrypted.length })
// 对于 hevc 格式,返回错误提示
if (finalExt === '.hevc') {
return {
success: false,
error: '此图片为微信新格式(wxgf),需要安装 ffmpeg 才能显示',
isThumb: this.isThumbnailPath(datPath)
}
}
const isThumb = this.isThumbnailPath(datPath)
this.cacheResolvedPaths(cacheKey, payload.imageMd5, payload.imageDatName, outputPath)
if (!isThumb) {
this.clearUpdateFlags(cacheKey, payload.imageMd5, payload.imageDatName)
}
const dataUrl = this.bufferToDataUrl(decrypted, ext)
const dataUrl = this.bufferToDataUrl(decrypted, finalExt)
const localPath = dataUrl || this.filePathToUrl(outputPath)
this.emitCacheResolved(payload, cacheKey, localPath)
return { success: true, localPath, isThumb }
@@ -1406,6 +1438,152 @@ export class ImageDecryptService {
return mostCommonKey
}
/**
* 解包 wxgf 格式
* wxgf 是微信的图片格式,内部使用 HEVC 编码
*/
private async unwrapWxgf(buffer: Buffer): Promise<{ data: Buffer; isWxgf: boolean }> {
// 检查是否是 wxgf 格式 (77 78 67 66 = "wxgf")
if (buffer.length < 20 ||
buffer[0] !== 0x77 || buffer[1] !== 0x78 ||
buffer[2] !== 0x67 || buffer[3] !== 0x66) {
return { data: buffer, isWxgf: false }
}
// 先尝试搜索内嵌的传统图片签名
for (let i = 4; i < Math.min(buffer.length - 12, 4096); i++) {
if (buffer[i] === 0xff && buffer[i + 1] === 0xd8 && buffer[i + 2] === 0xff) {
return { data: buffer.subarray(i), isWxgf: false }
}
if (buffer[i] === 0x89 && buffer[i + 1] === 0x50 &&
buffer[i + 2] === 0x4e && buffer[i + 3] === 0x47) {
return { data: buffer.subarray(i), isWxgf: false }
}
}
// 提取 HEVC NALU 裸流
const hevcData = this.extractHevcNalu(buffer)
if (!hevcData || hevcData.length < 100) {
return { data: buffer, isWxgf: true }
}
// 尝试用 ffmpeg 转换
try {
const jpgData = await this.convertHevcToJpg(hevcData)
if (jpgData && jpgData.length > 0) {
return { data: jpgData, isWxgf: false }
}
} catch {
// ffmpeg 转换失败
}
return { data: hevcData, isWxgf: true }
}
/**
* 从 wxgf 数据中提取 HEVC NALU 裸流
*/
private extractHevcNalu(buffer: Buffer): Buffer | null {
const nalUnits: Buffer[] = []
let i = 4
while (i < buffer.length - 4) {
if (buffer[i] === 0x00 && buffer[i + 1] === 0x00 &&
buffer[i + 2] === 0x00 && buffer[i + 3] === 0x01) {
let nalStart = i
let nalEnd = buffer.length
for (let j = i + 4; j < buffer.length - 3; j++) {
if (buffer[j] === 0x00 && buffer[j + 1] === 0x00) {
if (buffer[j + 2] === 0x01 ||
(buffer[j + 2] === 0x00 && j + 3 < buffer.length && buffer[j + 3] === 0x01)) {
nalEnd = j
break
}
}
}
const nalUnit = buffer.subarray(nalStart, nalEnd)
if (nalUnit.length > 3) {
nalUnits.push(nalUnit)
}
i = nalEnd
} else {
i++
}
}
if (nalUnits.length === 0) {
for (let j = 4; j < buffer.length - 4; j++) {
if (buffer[j] === 0x00 && buffer[j + 1] === 0x00 &&
buffer[j + 2] === 0x00 && buffer[j + 3] === 0x01) {
return buffer.subarray(j)
}
}
return null
}
return Buffer.concat(nalUnits)
}
/**
* 获取 ffmpeg 可执行文件路径
*/
private getFfmpegPath(): string {
const staticPath = getStaticFfmpegPath()
if (staticPath) {
const unpackedPath = staticPath.replace('app.asar', 'app.asar.unpacked')
if (existsSync(unpackedPath)) {
return unpackedPath
}
if (existsSync(staticPath)) {
return staticPath
}
}
return 'ffmpeg'
}
/**
* 使用 ffmpeg 将 HEVC 裸流转换为 JPG
*/
private convertHevcToJpg(hevcData: Buffer): Promise<Buffer | null> {
const ffmpeg = this.getFfmpegPath()
return new Promise((resolve) => {
const { spawn } = require('child_process')
const chunks: Buffer[] = []
const proc = spawn(ffmpeg, [
'-hide_banner',
'-loglevel', 'error',
'-f', 'hevc',
'-i', 'pipe:0',
'-vframes', '1',
'-q:v', '3',
'-f', 'mjpeg',
'pipe:1'
], {
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true
})
proc.stdout.on('data', (chunk: Buffer) => chunks.push(chunk))
proc.on('close', (code: number) => {
if (code === 0 && chunks.length > 0) {
resolve(Buffer.concat(chunks))
} else {
resolve(null)
}
})
proc.on('error', () => resolve(null))
proc.stdin.write(hevcData)
proc.stdin.end()
})
}
// 保留原有的解密到文件方法(用于兼容)
async decryptToFile(inputPath: string, outputPath: string, xorKey: number, aesKey?: Buffer): Promise<void> {
const version = this.getDatVersion(inputPath)