mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-03 18:33:14 +08:00
Merge pull request #31 from njueeRay/fix/windows-v4-image-key-derivation
fix: derive Windows image keys from local metadata
This commit is contained in:
+1
-1
@@ -924,7 +924,7 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
const result =
|
||||
process.platform === 'win32'
|
||||
? await keyServiceWin.autoGetImageKeyByMemoryScan(accountRoot, onStatus)
|
||||
? await keyServiceWin.autoGetImageKey(accountRoot, onStatus, wxid)
|
||||
: await keyServiceMac.autoGetImageKey(accountRoot, onStatus, wxid)
|
||||
|
||||
if (!result.success || !result.aesKey) return result
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { join, dirname, delimiter } from 'path'
|
||||
import { existsSync, copyFileSync, mkdirSync } from 'fs'
|
||||
import { existsSync, copyFileSync, mkdirSync, readdirSync } from 'fs'
|
||||
import { execFile } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import os from 'os'
|
||||
import crypto from 'crypto'
|
||||
import { getResourceRoots as getSharedResourceRoots } from './resource-paths'
|
||||
import {
|
||||
deriveV4ImageKeys,
|
||||
extractKvcommCode,
|
||||
normalizeV4AccountId
|
||||
} from '../shared/wechat-image-key-derivation'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
@@ -683,7 +688,8 @@ export class KeyService {
|
||||
if (loginRequired) {
|
||||
return {
|
||||
success: false,
|
||||
error: '微信可能已经启动并登录,请先在微信客户端保持未登录状态,具体点击上方“查看5分钟上手教程” ',
|
||||
error:
|
||||
'微信可能已经启动并登录,请先在微信客户端保持未登录状态,具体点击上方“查看5分钟上手教程” ',
|
||||
logs
|
||||
}
|
||||
}
|
||||
@@ -696,8 +702,7 @@ export class KeyService {
|
||||
onProgress?: (message: string) => void,
|
||||
wxidParam?: string
|
||||
): Promise<ImageKeyResult> {
|
||||
void wxidParam
|
||||
return this.autoGetImageKeyByMemoryScan(manualDir || '', onProgress)
|
||||
return this.autoGetImageKeyByMemoryScan(manualDir || '', onProgress, wxidParam)
|
||||
}
|
||||
|
||||
// --- 内存扫描备选方案(融合 Dart+Python 优点)---
|
||||
@@ -706,7 +711,8 @@ export class KeyService {
|
||||
|
||||
async autoGetImageKeyByMemoryScan(
|
||||
userDir: string,
|
||||
onProgress?: (message: string) => void
|
||||
onProgress?: (message: string) => void,
|
||||
wxidParam?: string
|
||||
): Promise<ImageKeyResult> {
|
||||
if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' }
|
||||
|
||||
@@ -781,6 +787,18 @@ export class KeyService {
|
||||
|
||||
onProgress?.(`XOR 密钥: 0x${xorKey.toString(16).padStart(2, '0')},正在查找微信进程...`)
|
||||
|
||||
const derived = this._deriveImageKeyByLocalMetadata(
|
||||
userDir,
|
||||
wxidParam,
|
||||
ciphertext,
|
||||
xorKey,
|
||||
onProgress
|
||||
)
|
||||
if (derived) {
|
||||
onProgress?.('通过本机账号元数据推导并验证图片密钥成功')
|
||||
return { success: true, xorKey: derived.xorKey, aesKey: derived.aesKey, verified: true }
|
||||
}
|
||||
|
||||
// 2. 找微信 PID
|
||||
const pid = await this.findWeChatPid()
|
||||
if (!pid) return { success: false, error: '微信进程未运行,请先启动微信' }
|
||||
@@ -811,6 +829,73 @@ export class KeyService {
|
||||
}
|
||||
}
|
||||
|
||||
private _deriveImageKeyByLocalMetadata(
|
||||
userDir: string,
|
||||
wxidParam: string | undefined,
|
||||
ciphertext: Buffer,
|
||||
expectedXorKey: number,
|
||||
onProgress?: (message: string) => void
|
||||
): { xorKey: number; aesKey: string } | null {
|
||||
const codes = new Set<number>()
|
||||
for (const directory of this._getKvcommCandidates(userDir)) {
|
||||
try {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
if (!entry.isFile()) continue
|
||||
const code = extractKvcommCode(entry.name)
|
||||
if (code !== null) codes.add(code)
|
||||
}
|
||||
} catch {
|
||||
// Candidate paths differ across WeChat releases and installations.
|
||||
}
|
||||
}
|
||||
|
||||
const accountIds = new Set<string>()
|
||||
for (const candidate of [wxidParam, userDir]) {
|
||||
const normalized = normalizeV4AccountId(candidate || '')
|
||||
if (normalized) accountIds.add(normalized)
|
||||
}
|
||||
onProgress?.(`正在校验本机账号元数据候选(code=${codes.size}, account=${accountIds.size})...`)
|
||||
|
||||
for (const accountId of accountIds) {
|
||||
for (const code of codes) {
|
||||
const derived = deriveV4ImageKeys(code, accountId)
|
||||
if (!derived || derived.xorKey !== expectedXorKey) continue
|
||||
if (this._verifyAesKey(Buffer.from(derived.aesKey, 'ascii'), ciphertext)) return derived
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private _getKvcommCandidates(userDir: string): string[] {
|
||||
const candidates: string[] = []
|
||||
const seen = new Set<string>()
|
||||
const add = (candidate: string | undefined) => {
|
||||
if (!candidate) return
|
||||
const normalized = candidate.replace(/[\\/]+$/, '')
|
||||
const identity = normalized.toLowerCase()
|
||||
if (!normalized || seen.has(identity)) return
|
||||
seen.add(identity)
|
||||
candidates.push(normalized)
|
||||
}
|
||||
|
||||
const roaming = process.env.APPDATA
|
||||
const local = process.env.LOCALAPPDATA
|
||||
add(roaming && join(roaming, 'Tencent', 'xwechat', 'net', 'kvcomm'))
|
||||
add(roaming && join(roaming, 'Tencent', 'xwechat_files', 'app_data', 'net', 'kvcomm'))
|
||||
add(local && join(local, 'Tencent', 'xwechat', 'net', 'kvcomm'))
|
||||
add(local && join(local, 'Tencent', 'xwechat_files', 'app_data', 'net', 'kvcomm'))
|
||||
add(local && join(local, 'Tencent', 'WeChat', 'xwechat', 'net', 'kvcomm'))
|
||||
|
||||
let cursor = userDir
|
||||
for (let depth = 0; cursor && depth < 6; depth++) {
|
||||
add(join(cursor, 'net', 'kvcomm'))
|
||||
const parent = dirname(cursor)
|
||||
if (parent === cursor) break
|
||||
cursor = parent
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
private async _findTemplateData(
|
||||
userDir: string,
|
||||
limit: number = 32
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
export type DerivedV4ImageKeys = { xorKey: number; aesKey: string }
|
||||
|
||||
export function normalizeV4AccountId(value: string): string {
|
||||
const leaf = String(value || '')
|
||||
.trim()
|
||||
.split(/[\\/]/)
|
||||
.filter(Boolean)
|
||||
.pop()
|
||||
if (!leaf) return ''
|
||||
const wxid = leaf.match(/^(wxid_[^_]+)/i)
|
||||
if (wxid) return wxid[1]
|
||||
return leaf.replace(/_[a-z0-9]{4}$/i, '')
|
||||
}
|
||||
|
||||
export function extractKvcommCode(fileName: string): number | null {
|
||||
const match = String(fileName || '').match(/^key_(\d+)_.+\.statistic$/i)
|
||||
if (!match) return null
|
||||
const code = Number.parseInt(match[1], 10)
|
||||
if (!Number.isSafeInteger(code) || code <= 0 || code > 0xffffffff) return null
|
||||
return code
|
||||
}
|
||||
|
||||
export function deriveV4ImageKeys(code: number, accountId: string): DerivedV4ImageKeys | null {
|
||||
const normalizedAccountId = normalizeV4AccountId(accountId)
|
||||
if (!Number.isSafeInteger(code) || code <= 0 || code > 0xffffffff || !normalizedAccountId) {
|
||||
return null
|
||||
}
|
||||
const digest = crypto
|
||||
.createHash('md5')
|
||||
.update(`${code}${normalizedAccountId}`, 'utf8')
|
||||
.digest('hex')
|
||||
return { xorKey: code & 0xff, aesKey: digest.slice(0, 16) }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
deriveV4ImageKeys,
|
||||
extractKvcommCode,
|
||||
normalizeV4AccountId
|
||||
} from '../../src/shared/wechat-image-key-derivation'
|
||||
|
||||
describe('WeChat 4 image key derivation', () => {
|
||||
it('normalizes account directories without leaking the per-install suffix', () => {
|
||||
expect(normalizeV4AccountId('C:\\data\\wxid_fixture_ab12')).toBe('wxid_fixture')
|
||||
expect(normalizeV4AccountId('/data/custom-account_ab12')).toBe('custom-account')
|
||||
})
|
||||
|
||||
it('extracts only valid uint32 kvcomm codes', () => {
|
||||
expect(extractKvcommCode('key_123456789_network.statistic')).toBe(123456789)
|
||||
expect(extractKvcommCode('key_0_network.statistic')).toBeNull()
|
||||
expect(extractKvcommCode('other_123_network.statistic')).toBeNull()
|
||||
})
|
||||
|
||||
it('derives the documented XOR byte and 16-character AES key', () => {
|
||||
expect(deriveV4ImageKeys(123456789, 'wxid_fixture_ab12')).toEqual({
|
||||
xorKey: 0x15,
|
||||
aesKey: '3bc2c5cbb10dfeda'
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user