import { app } from 'electron' import { join, dirname, basename } from 'path' import { existsSync, readdirSync, readFileSync, statSync, copyFileSync, mkdirSync } from 'fs' import { execFile, spawn } from 'child_process' import { promisify } from 'util' import { Worker } from 'worker_threads' import os from 'os' const execFileAsync = promisify(execFile) type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] } type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; error?: string } export class KeyService { private koffi: any = null private lib: any = null private initialized = false private initHook: any = null private pollKeyData: any = null private getStatusMessage: any = null private cleanupHook: any = null private getLastErrorMsg: any = null // Win32 APIs private kernel32: any = null private user32: any = null private advapi32: any = null // Kernel32 (已移除内存扫描相关的 API) private OpenProcess: any = null private CloseHandle: any = null private TerminateProcess: any = null private QueryFullProcessImageNameW: any = null // User32 private EnumWindows: any = null private GetWindowTextW: any = null private GetWindowTextLengthW: any = null private GetClassNameW: any = null private GetWindowThreadProcessId: any = null private IsWindowVisible: any = null private EnumChildWindows: any = null private PostMessageW: any = null private WNDENUMPROC_PTR: any = null // Advapi32 private RegOpenKeyExW: any = null private RegQueryValueExW: any = null private RegCloseKey: any = null // Constants private readonly PROCESS_ALL_ACCESS = 0x1F0FFF private readonly PROCESS_TERMINATE = 0x0001 private readonly KEY_READ = 0x20019 private readonly HKEY_LOCAL_MACHINE = 0x80000002 private readonly HKEY_CURRENT_USER = 0x80000001 private readonly ERROR_SUCCESS = 0 private readonly WM_CLOSE = 0x0010 private getDllPath(): string { const isPackaged = typeof app !== 'undefined' && app ? app.isPackaged : process.env.NODE_ENV === 'production' const candidates: string[] = [] if (process.env.WX_KEY_DLL_PATH) { candidates.push(process.env.WX_KEY_DLL_PATH) } if (isPackaged) { candidates.push(join(process.resourcesPath, 'resources', 'wx_key.dll')) candidates.push(join(process.resourcesPath, 'wx_key.dll')) } else { const cwd = process.cwd() candidates.push(join(cwd, 'resources', 'wx_key.dll')) candidates.push(join(app.getAppPath(), 'resources', 'wx_key.dll')) } for (const path of candidates) { if (existsSync(path)) return path } return candidates[0] } private isNetworkPath(path: string): boolean { if (path.startsWith('\\\\')) return true return false } private localizeNetworkDll(originalPath: string): string { try { const tempDir = join(os.tmpdir(), 'weflow_dll_cache') if (!existsSync(tempDir)) { mkdirSync(tempDir, { recursive: true }) } const localPath = join(tempDir, 'wx_key.dll') if (existsSync(localPath)) return localPath copyFileSync(originalPath, localPath) return localPath } catch (e) { console.error('DLL 本地化失败:', e) return originalPath } } private ensureLoaded(): boolean { if (this.initialized) return true let dllPath = '' try { this.koffi = require('koffi') dllPath = this.getDllPath() if (!existsSync(dllPath)) { console.error(`wx_key.dll 不存在于路径: ${dllPath}`) return false } if (this.isNetworkPath(dllPath)) { dllPath = this.localizeNetworkDll(dllPath) } this.lib = this.koffi.load(dllPath) this.initHook = this.lib.func('bool InitializeHook(uint32 targetPid)') this.pollKeyData = this.lib.func('bool PollKeyData(_Out_ char *keyBuffer, int bufferSize)') this.getStatusMessage = this.lib.func('bool GetStatusMessage(_Out_ char *msgBuffer, int bufferSize, _Out_ int *outLevel)') this.cleanupHook = this.lib.func('bool CleanupHook()') this.getLastErrorMsg = this.lib.func('const char* GetLastErrorMsg()') this.initialized = true return true } catch (e) { const errorMsg = e instanceof Error ? e.message : String(e) console.error(`加载 wx_key.dll 失败\n 路径: ${dllPath}\n 错误: ${errorMsg}`) return false } } private ensureWin32(): boolean { return process.platform === 'win32' } private ensureKernel32(): boolean { if (this.kernel32) return true try { this.koffi = require('koffi') this.kernel32 = this.koffi.load('kernel32.dll') this.OpenProcess = this.kernel32.func('OpenProcess', 'HANDLE', ['uint32', 'bool', 'uint32']) this.CloseHandle = this.kernel32.func('CloseHandle', 'bool', ['HANDLE']) this.TerminateProcess = this.kernel32.func('TerminateProcess', 'bool', ['HANDLE', 'uint32']) this.QueryFullProcessImageNameW = this.kernel32.func('QueryFullProcessImageNameW', 'bool', ['HANDLE', 'uint32', this.koffi.out('uint16*'), this.koffi.out('uint32*')]) return true } catch (e) { console.error('初始化 kernel32 失败:', e) return false } } private decodeUtf8(buf: Buffer): string { const nullIdx = buf.indexOf(0) return buf.toString('utf8', 0, nullIdx > -1 ? nullIdx : undefined).trim() } private ensureUser32(): boolean { if (this.user32) return true try { this.koffi = require('koffi') this.user32 = this.koffi.load('user32.dll') const WNDENUMPROC = this.koffi.proto('bool __stdcall (void *hWnd, intptr_t lParam)') this.WNDENUMPROC_PTR = this.koffi.pointer(WNDENUMPROC) this.EnumWindows = this.user32.func('EnumWindows', 'bool', [this.WNDENUMPROC_PTR, 'intptr_t']) this.EnumChildWindows = this.user32.func('EnumChildWindows', 'bool', ['void*', this.WNDENUMPROC_PTR, 'intptr_t']) this.PostMessageW = this.user32.func('PostMessageW', 'bool', ['void*', 'uint32', 'uintptr_t', 'intptr_t']) this.GetWindowTextW = this.user32.func('GetWindowTextW', 'int', ['void*', this.koffi.out('uint16*'), 'int']) this.GetWindowTextLengthW = this.user32.func('GetWindowTextLengthW', 'int', ['void*']) this.GetClassNameW = this.user32.func('GetClassNameW', 'int', ['void*', this.koffi.out('uint16*'), 'int']) this.GetWindowThreadProcessId = this.user32.func('GetWindowThreadProcessId', 'uint32', ['void*', this.koffi.out('uint32*')]) this.IsWindowVisible = this.user32.func('IsWindowVisible', 'bool', ['void*']) return true } catch (e) { console.error('初始化 user32 失败:', e) return false } } private ensureAdvapi32(): boolean { if (this.advapi32) return true try { this.koffi = require('koffi') this.advapi32 = this.koffi.load('advapi32.dll') const HKEY = this.koffi.alias('HKEY', 'intptr_t') const HKEY_PTR = this.koffi.pointer(HKEY) this.RegOpenKeyExW = this.advapi32.func('RegOpenKeyExW', 'long', [HKEY, 'uint16*', 'uint32', 'uint32', this.koffi.out(HKEY_PTR)]) this.RegQueryValueExW = this.advapi32.func('RegQueryValueExW', 'long', [HKEY, 'uint16*', 'uint32*', this.koffi.out('uint32*'), this.koffi.out('uint8*'), this.koffi.out('uint32*')]) this.RegCloseKey = this.advapi32.func('RegCloseKey', 'long', [HKEY]) return true } catch (e) { console.error('初始化 advapi32 失败:', e) return false } } private decodeCString(ptr: any): string { try { if (typeof ptr === 'string') return ptr return this.koffi.decode(ptr, 'char', -1) } catch { return '' } } // --- WeChat Process & Path Finding --- private readRegistryString(rootKey: number, subKey: string, valueName: string): string | null { if (!this.ensureAdvapi32()) return null const subKeyBuf = Buffer.from(subKey + '\0', 'ucs2') const valueNameBuf = valueName ? Buffer.from(valueName + '\0', 'ucs2') : null const phkResult = Buffer.alloc(8) if (this.RegOpenKeyExW(rootKey, subKeyBuf, 0, this.KEY_READ, phkResult) !== this.ERROR_SUCCESS) return null const hKey = this.koffi.decode(phkResult, 'uintptr_t') try { const lpcbData = Buffer.alloc(4) lpcbData.writeUInt32LE(0, 0) let ret = this.RegQueryValueExW(hKey, valueNameBuf, null, null, null, lpcbData) if (ret !== this.ERROR_SUCCESS) return null const size = lpcbData.readUInt32LE(0) if (size === 0) return null const dataBuf = Buffer.alloc(size) ret = this.RegQueryValueExW(hKey, valueNameBuf, null, null, dataBuf, lpcbData) if (ret !== this.ERROR_SUCCESS) return null let str = dataBuf.toString('ucs2') if (str.endsWith('\0')) str = str.slice(0, -1) return str } finally { this.RegCloseKey(hKey) } } private async getProcessExecutablePath(pid: number): Promise { if (!this.ensureKernel32()) return null const hProcess = this.OpenProcess(0x1000, false, pid) if (!hProcess) return null try { const sizeBuf = Buffer.alloc(4) sizeBuf.writeUInt32LE(1024, 0) const pathBuf = Buffer.alloc(1024 * 2) const ret = this.QueryFullProcessImageNameW(hProcess, 0, pathBuf, sizeBuf) if (ret) { const len = sizeBuf.readUInt32LE(0) return pathBuf.toString('ucs2', 0, len * 2) } return null } catch (e) { console.error('获取进程路径失败:', e) return null } finally { this.CloseHandle(hProcess) } } private async findWeChatInstallPath(): Promise { try { const pid = await this.findWeChatPid() if (pid) { const runPath = await this.getProcessExecutablePath(pid) if (runPath && existsSync(runPath)) return runPath } } catch (e) { console.error('尝试获取运行中微信路径失败:', e) } const uninstallKeys = [ 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall', 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall' ] const roots = [this.HKEY_LOCAL_MACHINE, this.HKEY_CURRENT_USER] const tencentKeys = [ 'Software\\Tencent\\WeChat', 'Software\\WOW6432Node\\Tencent\\WeChat', 'Software\\Tencent\\Weixin', ] for (const root of roots) { for (const key of tencentKeys) { const path = this.readRegistryString(root, key, 'InstallPath') if (path && existsSync(join(path, 'Weixin.exe'))) return join(path, 'Weixin.exe') if (path && existsSync(join(path, 'WeChat.exe'))) return join(path, 'WeChat.exe') } } for (const root of roots) { for (const parent of uninstallKeys) { const path = this.readRegistryString(root, parent + '\\WeChat', 'InstallLocation') if (path && existsSync(join(path, 'Weixin.exe'))) return join(path, 'Weixin.exe') } } const drives = ['C', 'D', 'E', 'F'] const commonPaths = [ 'Program Files\\Tencent\\WeChat\\WeChat.exe', 'Program Files (x86)\\Tencent\\WeChat\\WeChat.exe', 'Program Files\\Tencent\\Weixin\\Weixin.exe', 'Program Files (x86)\\Tencent\\Weixin\\Weixin.exe' ] for (const drive of drives) { for (const p of commonPaths) { const full = join(drive + ':\\', p) if (existsSync(full)) return full } } return null } private async findPidByImageName(imageName: string): Promise { try { const { stdout } = await execFileAsync('tasklist', ['/FI', `IMAGENAME eq ${imageName}`, '/FO', 'CSV', '/NH']) const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) for (const line of lines) { if (line.startsWith('INFO:')) continue const parts = line.split('","').map((p) => p.replace(/^"|"$/g, '')) if (parts[0]?.toLowerCase() === imageName.toLowerCase()) { const pid = Number(parts[1]) if (!Number.isNaN(pid)) return pid } } return null } catch (e) { return null } } private async findWeChatPid(): Promise { const names = ['Weixin.exe', 'WeChat.exe'] for (const name of names) { const pid = await this.findPidByImageName(name) if (pid) return pid } const fallbackPid = await this.waitForWeChatWindow(5000) return fallbackPid ?? null } private async waitForWeChatExit(timeoutMs = 8000): Promise { const start = Date.now() while (Date.now() - start < timeoutMs) { const weixinPid = await this.findPidByImageName('Weixin.exe') const wechatPid = await this.findPidByImageName('WeChat.exe') if (!weixinPid && !wechatPid) return true await new Promise(r => setTimeout(r, 400)) } return false } private async closeWeChatWindows(): Promise { if (!this.ensureUser32()) return false let requested = false const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => { if (!this.IsWindowVisible(hWnd)) return true const title = this.getWindowTitle(hWnd) const className = this.getClassName(hWnd) const classLower = (className || '').toLowerCase() const isWeChatWindow = this.isWeChatWindowTitle(title) || classLower.includes('wechat') || classLower.includes('weixin') if (!isWeChatWindow) return true requested = true try { this.PostMessageW?.(hWnd, this.WM_CLOSE, 0, 0) } catch { } return true }, this.WNDENUMPROC_PTR) this.EnumWindows(enumWindowsCallback, 0) this.koffi.unregister(enumWindowsCallback) return requested } private async killWeChatProcesses(): Promise { const requested = await this.closeWeChatWindows() if (requested) { const gracefulOk = await this.waitForWeChatExit(1500) if (gracefulOk) return true } try { await execFileAsync('taskkill', ['/F', '/T', '/IM', 'Weixin.exe']) await execFileAsync('taskkill', ['/F', '/T', '/IM', 'WeChat.exe']) } catch (e) { } return await this.waitForWeChatExit(5000) } // --- Window Detection --- private getWindowTitle(hWnd: any): string { const len = this.GetWindowTextLengthW(hWnd) if (len === 0) return '' const buf = Buffer.alloc((len + 1) * 2) this.GetWindowTextW(hWnd, buf, len + 1) return buf.toString('ucs2', 0, len * 2) } private getClassName(hWnd: any): string { const buf = Buffer.alloc(512) const len = this.GetClassNameW(hWnd, buf, 256) return buf.toString('ucs2', 0, len * 2) } private isWeChatWindowTitle(title: string): boolean { const normalized = title.trim() if (!normalized) return false const lower = normalized.toLowerCase() return normalized === '微信' || lower === 'wechat' || lower === 'weixin' } private async waitForWeChatWindow(timeoutMs = 25000): Promise { if (!this.ensureUser32()) return null const startTime = Date.now() while (Date.now() - startTime < timeoutMs) { let foundPid: number | null = null const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => { if (!this.IsWindowVisible(hWnd)) return true const title = this.getWindowTitle(hWnd) if (!this.isWeChatWindowTitle(title)) return true const pidBuf = Buffer.alloc(4) this.GetWindowThreadProcessId(hWnd, pidBuf) const pid = pidBuf.readUInt32LE(0) if (pid) { foundPid = pid return false } return true }, this.WNDENUMPROC_PTR) this.EnumWindows(enumWindowsCallback, 0) this.koffi.unregister(enumWindowsCallback) if (foundPid) return foundPid await new Promise(r => setTimeout(r, 500)) } return null } private collectChildWindowInfos(parent: any): Array<{ title: string; className: string }> { const children: Array<{ title: string; className: string }> = [] const enumChildCallback = this.koffi.register((hChild: any, lp: any) => { const title = this.getWindowTitle(hChild).trim() const className = this.getClassName(hChild).trim() children.push({ title, className }) return true }, this.WNDENUMPROC_PTR) this.EnumChildWindows(parent, enumChildCallback, 0) this.koffi.unregister(enumChildCallback) return children } private hasReadyComponents(children: Array<{ title: string; className: string }>): boolean { if (children.length === 0) return false const readyTexts = ['聊天', '登录', '账号'] const readyClassMarkers = ['WeChat', 'Weixin', 'TXGuiFoundation', 'Qt5', 'ChatList', 'MainWnd', 'BrowserWnd', 'ListView'] const readyChildCountThreshold = 14 let classMatchCount = 0 let titleMatchCount = 0 let hasValidClassName = false for (const child of children) { const normalizedTitle = child.title.replace(/\s+/g, '') if (normalizedTitle) { if (readyTexts.some(marker => normalizedTitle.includes(marker))) return true titleMatchCount += 1 } const className = child.className if (className) { if (readyClassMarkers.some(marker => className.includes(marker))) return true if (className.length > 5) { classMatchCount += 1 hasValidClassName = true } } } if (classMatchCount >= 3 || titleMatchCount >= 2) return true if (children.length >= readyChildCountThreshold) return true if (hasValidClassName && children.length >= 5) return true return false } private async waitForWeChatWindowComponents(pid: number, timeoutMs = 15000): Promise { if (!this.ensureUser32()) return true const startTime = Date.now() while (Date.now() - startTime < timeoutMs) { let ready = false const enumWindowsCallback = this.koffi.register((hWnd: any, lParam: any) => { if (!this.IsWindowVisible(hWnd)) return true const title = this.getWindowTitle(hWnd) if (!this.isWeChatWindowTitle(title)) return true const pidBuf = Buffer.alloc(4) this.GetWindowThreadProcessId(hWnd, pidBuf) const windowPid = pidBuf.readUInt32LE(0) if (windowPid !== pid) return true const children = this.collectChildWindowInfos(hWnd) if (this.hasReadyComponents(children)) { ready = true return false } return true }, this.WNDENUMPROC_PTR) this.EnumWindows(enumWindowsCallback, 0) this.koffi.unregister(enumWindowsCallback) if (ready) return true await new Promise(r => setTimeout(r, 500)) } return true } // --- DB Key Logic (Unchanged core flow) --- async autoGetDbKey( timeoutMs = 60_000, onStatus?: (message: string, level: number) => void ): Promise { if (!this.ensureWin32()) return { success: false, error: '仅支持 Windows' } if (!this.ensureLoaded()) return { success: false, error: 'wx_key.dll 未加载' } if (!this.ensureKernel32()) return { success: false, error: 'Kernel32 Init Failed' } const logs: string[] = [] onStatus?.('正在定位微信安装路径...', 0) let wechatPath = await this.findWeChatInstallPath() if (!wechatPath) { const err = '未找到微信安装路径,请确认已安装PC微信' onStatus?.(err, 2) return { success: false, error: err } } onStatus?.('正在关闭微信以进行获取...', 0) const closed = await this.killWeChatProcesses() if (!closed) { const err = '无法自动关闭微信,请手动退出后重试' onStatus?.(err, 2) return { success: false, error: err } } onStatus?.('正在启动微信...', 0) const sub = spawn(wechatPath, { detached: true, stdio: 'ignore', cwd: dirname(wechatPath) }) sub.unref() onStatus?.('等待微信界面就绪...', 0) const pid = await this.waitForWeChatWindow() if (!pid) return { success: false, error: '启动微信失败或等待界面就绪超时' } onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0) onStatus?.('正在检测微信界面组件...', 0) await this.waitForWeChatWindowComponents(pid, 15000) const ok = this.initHook(pid) if (!ok) { const error = this.getLastErrorMsg ? this.decodeCString(this.getLastErrorMsg()) : '' if (error) { if (error.includes('0xC0000022') || error.includes('ACCESS_DENIED') || error.includes('打开目标进程失败')) { const friendlyError = '权限不足:无法访问微信进程。\n\n解决方法:\n1. 右键 WeFlow 图标,选择"以管理员身份运行"\n2. 关闭可能拦截的安全软件(如360、火绒等)\n3. 确保微信没有以管理员权限运行' return { success: false, error: friendlyError } } return { success: false, error } } const statusBuffer = Buffer.alloc(256) const levelOut = [0] const status = this.getStatusMessage && this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut) ? this.decodeUtf8(statusBuffer) : '' return { success: false, error: status || '初始化失败' } } const keyBuffer = Buffer.alloc(128) const start = Date.now() try { while (Date.now() - start < timeoutMs) { if (this.pollKeyData(keyBuffer, keyBuffer.length)) { const key = this.decodeUtf8(keyBuffer) if (key.length === 64) { onStatus?.('密钥获取成功', 1) return { success: true, key, logs } } } for (let i = 0; i < 5; i++) { const statusBuffer = Buffer.alloc(256) const levelOut = [0] if (!this.getStatusMessage(statusBuffer, statusBuffer.length, levelOut)) break const msg = this.decodeUtf8(statusBuffer) const level = levelOut[0] ?? 0 if (msg) { logs.push(msg) onStatus?.(msg, level) } } await new Promise((resolve) => setTimeout(resolve, 120)) } } finally { try { this.cleanupHook() } catch { } } return { success: false, error: '获取密钥超时', logs } } // --- Image Key Stuff (Refactored to Multi-core Crypto Brute Force) --- private isAccountDir(dirPath: string): boolean { return ( existsSync(join(dirPath, 'db_storage')) || existsSync(join(dirPath, 'FileStorage', 'Image')) || existsSync(join(dirPath, 'FileStorage', 'Image2')) ) } private isPotentialAccountName(name: string): boolean { const lower = name.toLowerCase() if (lower.startsWith('all') || lower.startsWith('applet') || lower.startsWith('backup') || lower.startsWith('wmpf')) return false if (lower.startsWith('wxid_')) return true if (/^\d+$/.test(name) && name.length >= 6) return true return name.length > 5 } private listAccountDirs(rootDir: string): string[] { try { const entries = readdirSync(rootDir) const high: string[] = [] const low: string[] = [] for (const entry of entries) { const fullPath = join(rootDir, entry) try { if (!statSync(fullPath).isDirectory()) continue } catch { continue } if (!this.isPotentialAccountName(entry)) continue if (this.isAccountDir(fullPath)) high.push(fullPath) else low.push(fullPath) } return high.length ? high.sort() : low.sort() } catch { return [] } } private normalizeExistingDir(inputPath: string): string | null { const trimmed = inputPath.replace(/[\\\\/]+$/, '') if (!existsSync(trimmed)) return null try { const stats = statSync(trimmed) if (stats.isFile()) return dirname(trimmed) } catch { return null } return trimmed } private resolveAccountDirFromPath(inputPath: string): string | null { const normalized = this.normalizeExistingDir(inputPath) if (!normalized) return null if (this.isAccountDir(normalized)) return normalized const lower = normalized.toLowerCase() if (lower.endsWith('db_storage') || lower.endsWith('filestorage') || lower.endsWith('image') || lower.endsWith('image2')) { const parent = dirname(normalized) if (this.isAccountDir(parent)) return parent const grandParent = dirname(parent) if (this.isAccountDir(grandParent)) return grandParent } const candidates = this.listAccountDirs(normalized) if (candidates.length) return candidates[0] return null } private resolveAccountDir(manualDir?: string): string | null { if (manualDir) { const resolved = this.resolveAccountDirFromPath(manualDir) if (resolved) return resolved } const userProfile = process.env.USERPROFILE if (!userProfile) return null const roots = [ join(userProfile, 'Documents', 'xwechat_files'), join(userProfile, 'Documents', 'WeChat Files') ] for (const root of roots) { if (!existsSync(root)) continue const candidates = this.listAccountDirs(root) if (candidates.length) return candidates[0] } return null } private findTemplateDatFiles(rootDir: string): string[] { const files: string[] = [] const stack = [rootDir] const maxFiles = 256 while (stack.length && files.length < maxFiles) { const dir = stack.pop() as string let entries: string[] try { entries = readdirSync(dir) } catch { continue } for (const entry of entries) { const fullPath = join(dir, entry) let stats: any try { stats = statSync(fullPath) } catch { continue } if (stats.isDirectory()) { stack.push(fullPath) } else if (entry.endsWith('_t.dat')) { files.push(fullPath) if (files.length >= maxFiles) break } } } if (!files.length) return [] const dateReg = /(\d{4}-\d{2})/ files.sort((a, b) => { const ma = a.match(dateReg)?.[1] const mb = b.match(dateReg)?.[1] if (ma && mb) return mb.localeCompare(ma) return 0 }) return files.slice(0, 128) } private getXorKey(templateFiles: string[]): number | null { const counts = new Map() const tailSignatures = [ Buffer.from([0xFF, 0xD9]), Buffer.from([0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]) ] for (const file of templateFiles) { try { const bytes = readFileSync(file) for (const signature of tailSignatures) { if (bytes.length < signature.length) continue const tail = bytes.subarray(bytes.length - signature.length) const xorKey = tail[0] ^ signature[0] let valid = true for (let i = 1; i < signature.length; i++) { if ((tail[i] ^ xorKey) !== signature[i]) { valid = false break } } if (valid) counts.set(xorKey, (counts.get(xorKey) ?? 0) + 1) } } catch { } } if (!counts.size) return null let bestKey: number | null = null let bestCount = 0 for (const [key, count] of counts) { if (count > bestCount) { bestCount = count bestKey = key } } return bestKey } // 改为返回 Buffer 数组,收集最多2个样本用于双重校验 private getCiphertextsFromTemplate(templateFiles: string[]): Buffer[] { const ciphertexts: Buffer[] = [] for (const file of templateFiles) { try { const bytes = readFileSync(file) if (bytes.length < 0x1f) continue // 匹配微信 DAT 文件的特定头部特征 if ( bytes[0] === 0x07 && bytes[1] === 0x08 && bytes[2] === 0x56 && bytes[3] === 0x32 && bytes[4] === 0x08 && bytes[5] === 0x07 ) { ciphertexts.push(bytes.subarray(0x0f, 0x1f)) // 收集到 2 个样本就足够做双重校验了 if (ciphertexts.length >= 2) break } } catch { } } return ciphertexts } private async bruteForceAesKey( xorKey: number, wxid: string, ciphertexts: Buffer[], onProgress?: (msg: string) => void ): Promise { const numCores = os.cpus().length || 4 const totalCombinations = 1 << 24 // 16,777,216 种可能性 const chunkSize = Math.ceil(totalCombinations / numCores) onProgress?.(`准备启动 ${numCores} 个线程进行极速爆破...`) const workerCode = ` const { parentPort, workerData } = require('worker_threads'); const crypto = require('crypto'); const { start, end, xorKey, wxid, cipherHexList } = workerData; const ciphertexts = cipherHexList.map(hex => Buffer.from(hex, 'hex')); function verifyKey(cipher, keyStr) { try { const decipher = crypto.createDecipheriv('aes-128-ecb', keyStr, null); decipher.setAutoPadding(false); const decrypted = Buffer.concat([decipher.update(cipher), decipher.final()]); const isJpeg = decrypted.length >= 3 && decrypted[0] === 0xff && decrypted[1] === 0xd8 && decrypted[2] === 0xff; const isPng = decrypted.length >= 8 && decrypted[0] === 0x89 && decrypted[1] === 0x50 && decrypted[2] === 0x4e && decrypted[3] === 0x47; return isJpeg || isPng; } catch { return false; } } let found = null; for (let upper = end; upper > start; upper--) { // 我就写 -- if (upper % 100000 === 0 && upper !== start) { parentPort.postMessage({ type: 'progress', scanned: 100000 }); } const number = (upper * 256) + xorKey; // 1. 无符号整数校验 const strUnsigned = number.toString(10) + wxid; const md5Unsigned = crypto.createHash('md5').update(strUnsigned).digest('hex').slice(0, 16); let isValidUnsigned = true; for (const cipher of ciphertexts) { if (!verifyKey(cipher, md5Unsigned)) { isValidUnsigned = false; break; } } if (isValidUnsigned) { found = md5Unsigned; break; } // 2. 带符号整数校验 (溢出边界情况) if (number > 0x7FFFFFFF) { const strSigned = (number | 0).toString(10) + wxid; const md5Signed = crypto.createHash('md5').update(strSigned).digest('hex').slice(0, 16); let isValidSigned = true; for (const cipher of ciphertexts) { if (!verifyKey(cipher, md5Signed)) { isValidSigned = false; break; } } if (isValidSigned) { found = md5Signed; break; } } } if (found) { parentPort.postMessage({ type: 'success', key: found }); } else { parentPort.postMessage({ type: 'done' }); } ` return new Promise((resolve) => { let activeWorkers = numCores let resolved = false let totalScanned = 0 // 总进度计数器 const workers: Worker[] = [] const cleanup = () => { for (const w of workers) w.terminate() } for (let i = 0; i < numCores; i++) { const start = i * chunkSize const end = Math.min(start + chunkSize, totalCombinations) const worker = new Worker(workerCode, { eval: true, workerData: { start, end, xorKey, wxid, cipherHexList: ciphertexts.map(c => c.toString('hex')) // 传入数组 } }) workers.push(worker) worker.on('message', (msg) => { if (!msg) return if (msg.type === 'progress') { totalScanned += msg.scanned const percent = ((totalScanned / totalCombinations) * 100).toFixed(1) // 优化文案,并确保包含 (xx.x%) 供前端解析 onProgress?.(`多核爆破引擎运行中:已扫描 ${(totalScanned / 10000).toFixed(0)} 万个密钥空间 (${percent}%)`) } else if (msg.type === 'success' && !resolved) { resolved = true cleanup() resolve(msg.key) } else if (msg.type === 'done') { // 单个 worker 跑完了没有找到 activeWorkers-- if (activeWorkers === 0 && !resolved) resolve(null) } }) worker.on('error', (err) => { console.error('Worker error:', err) activeWorkers-- if (activeWorkers === 0 && !resolved) resolve(null) }) worker.on('exit', () => { activeWorkers-- if (activeWorkers === 0 && !resolved) resolve(null) }) } }) } async autoGetImageKey( manualDir?: string, onProgress?: (message: string) => void ): Promise { onProgress?.('正在定位微信账号数据目录...') const accountDir = this.resolveAccountDir(manualDir) if (!accountDir) return { success: false, error: '未找到微信账号目录' } let wxid = basename(accountDir) wxid = wxid.replace(/_[0-9a-fA-F]{4}$/, '') onProgress?.('正在收集并分析加密模板文件...') const templateFiles = this.findTemplateDatFiles(accountDir) if (!templateFiles.length) return { success: false, error: '未找到模板文件' } onProgress?.('正在计算特征 XOR 密钥...') const xorKey = this.getXorKey(templateFiles) if (xorKey == null) return { success: false, error: '无法计算 XOR 密钥' } onProgress?.('正在读取加密模板区块...') const ciphertexts = this.getCiphertextsFromTemplate(templateFiles) if (ciphertexts.length === 0) return { success: false, error: '无法读取加密模板数据' } onProgress?.(`成功提取 ${ciphertexts.length} 个特征样本,准备交叉校验...`) onProgress?.(`准备启动 ${os.cpus().length || 4} 线程并发爆破引擎 (基于 wxid: ${wxid})...`) const aesKey = await this.bruteForceAesKey(xorKey, wxid, ciphertexts, (msg) => { onProgress?.(msg) }) if (!aesKey) { return { success: false, error: 'AES 密钥爆破失败,请确认该账号近期是否有接收过图片,或更换账号目录重试' } } return { success: true, xorKey, aesKey } } }