From 3e303fadd7684ab01c75e998e2228fddee7c5797 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:27:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=87=B4=E8=B0=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bf9cadf..3ff9912 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ WeFlow 是一个**完全本地**的微信**实时**聊天记录查看、分析 ## 主要功能 - 本地实时查看聊天记录 +- 朋友圈图片、视频、**实况**的预览和解密 - 统计分析与群聊画像 - 年度报告与可视化概览 - 导出聊天记录为 HTML 等格式 @@ -86,6 +87,7 @@ npm run build ## 致谢 - [密语 CipherTalk](https://github.com/ILoveBingLu/miyu) 为本项目提供了基础框架 +- [WeChat-Channels-Video-File-Decryption](https://github.com/Evil0ctal/WeChat-Channels-Video-File-Decryption) 提供了视频解密相关的技术参考 ## 支持我们 From 8e28016e5e236ad2f5ce018d1256c8557ef483a2 Mon Sep 17 00:00:00 2001 From: cc <98377878+hicccc77@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:14:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E8=A7=A3=E5=AF=86=E7=9A=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/config.ts | 4 +-- electron/services/isaac64.ts | 32 ++++++++++++-------- electron/services/snsService.ts | 37 +++++++++++++---------- electron/services/wasmService.ts | 31 +++++++++++-------- electron/services/wcdbCore.ts | 44 ++------------------------- electron/services/wcdbService.ts | 7 +---- electron/wcdbWorker.ts | 4 +-- src/pages/SettingsPage.tsx | 51 +++----------------------------- src/services/config.ts | 11 +------ 9 files changed, 69 insertions(+), 152 deletions(-) diff --git a/electron/services/config.ts b/electron/services/config.ts index be606f2..d9eda16 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -14,7 +14,7 @@ interface ConfigSchema { // 缓存相关 cachePath: string - weixinDllPath: string + lastOpenedDb: string lastSession: string @@ -75,7 +75,7 @@ export class ConfigService { imageAesKey: '', wxidConfigs: {}, cachePath: '', - weixinDllPath: '', + lastOpenedDb: '', lastSession: '', theme: 'system', diff --git a/electron/services/isaac64.ts b/electron/services/isaac64.ts index 731f6db..8be407b 100644 --- a/electron/services/isaac64.ts +++ b/electron/services/isaac64.ts @@ -99,23 +99,29 @@ export class Isaac64 { this.isaac64(); this.randcnt = 256; } - return this.randrsl[256 - (this.randcnt--)]; + return this.randrsl[--this.randcnt]; } /** - * Generates a keystream of the specified size (in bytes). - * @param size Size of the keystream in bytes (must be multiple of 8) - * @returns Buffer containing the keystream + * Generates a keystream where each 64-bit block is Big-Endian. + * This matches WeChat's behavior (Reverse index order + byte reversal). */ - public generateKeystream(size: number): Buffer { - const stream = new BigUint64Array(size / 8); - for (let i = 0; i < stream.length; i++) { - stream[i] = this.getNext(); + public generateKeystreamBE(size: number): Buffer { + const buffer = Buffer.allocUnsafe(size); + const fullBlocks = Math.floor(size / 8); + + for (let i = 0; i < fullBlocks; i++) { + buffer.writeBigUInt64BE(this.getNext(), i * 8); } - // WeChat's logic specifically reverses the entire byte array - const buffer = Buffer.from(stream.buffer); - // 注意:根据 worker.html 的逻辑,它是对 Uint8Array 执行 reverse() - // Array.from(wasmArray).reverse() - return buffer.reverse(); + + const remaining = size % 8; + if (remaining > 0) { + const lastK = this.getNext(); + const temp = Buffer.allocUnsafe(8); + temp.writeBigUInt64BE(lastK, 0); + temp.copy(buffer, fullBlocks * 8, 0, remaining); + } + + return buffer; } } diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index 61abbd2..f4b7fff 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -292,7 +292,6 @@ class SnsService { // 视频专用下载逻辑 (下载 -> 解密 -> 缓存) return new Promise(async (resolve) => { const tmpPath = join(require('os').tmpdir(), `sns_video_${Date.now()}_${Math.random().toString(36).slice(2)}.enc`) - console.log(`[SnsService] 开始下载视频到临时文件: ${tmpPath}`) try { const https = require('https') @@ -325,7 +324,6 @@ class SnsService { fileStream.on('finish', async () => { fileStream.close() - console.log(`[SnsService] 视频下载完成,开始解密... Key: ${key}`) try { const encryptedBuffer = await readFile(tmpPath) @@ -334,7 +332,6 @@ class SnsService { if (key && String(key).trim().length > 0) { try { - console.log(`[SnsService] 使用 WASM Isaac64 解密视频... Key: ${key}`) const keyText = String(key).trim() let keystream: Buffer @@ -344,9 +341,8 @@ class SnsService { keystream = await wasmService.getKeystream(keyText, 131072) } catch (wasmErr) { // 打包漏带 wasm 或 wasm 初始化异常时,回退到纯 TS ISAAC64 - console.warn(`[SnsService] WASM 解密不可用,回退 Isaac64: ${wasmErr}`) const isaac = new Isaac64(keyText) - keystream = isaac.generateKeystream(131072) + keystream = isaac.generateKeystreamBE(131072) } const decryptLen = Math.min(keystream.length, raw.length) @@ -358,23 +354,16 @@ class SnsService { // 验证 MP4 签名 ('ftyp' at offset 4) const ftyp = raw.subarray(4, 8).toString('ascii') - if (ftyp === 'ftyp') { - console.log(`[SnsService] 视频解密成功: ${url}`) - } else { - console.warn(`[SnsService] 视频解密可能失败: ${url}, 未找到 ftyp 签名: ${ftyp}`) - // 打印前 32 字节用于调试 - console.warn(`[SnsService] Decrypted Header (first 32 bytes): ${raw.subarray(0, 32).toString('hex')}`) + if (ftyp !== 'ftyp') { + // 可以在此处记录解密可能失败的标记,但不打印详细 hex } } catch (err) { console.error(`[SnsService] 视频解密出错: ${err}`) } - } else { - console.warn(`[SnsService] 未提供 Key,跳过解密,直接保存`) } // 写入最终缓存 (覆盖) await writeFile(cachePath, raw) - console.log(`[SnsService] 视频已保存到缓存: ${cachePath}`) // 删除临时文件 try { await import('fs/promises').then(fs => fs.unlink(tmpPath)) } catch (e) { } @@ -444,8 +433,24 @@ class SnsService { // 图片逻辑 const shouldDecrypt = (xEnc === '1' || !!key) && key !== undefined && key !== null && String(key).trim().length > 0 if (shouldDecrypt) { - const decrypted = await wcdbService.decryptSnsImage(raw, String(key)) - decoded = Buffer.from(decrypted) + try { + const keyStr = String(key).trim() + if (/^\d+$/.test(keyStr)) { + // 使用 WASM 版本的 Isaac64 解密图片 + // 修正逻辑:使用带 reverse 且修正了 8字节对齐偏移的 getKeystream + const wasmService = WasmService.getInstance() + const keystream = await wasmService.getKeystream(keyStr, raw.length) + + const decrypted = Buffer.allocUnsafe(raw.length) + for (let i = 0; i < raw.length; i++) { + decrypted[i] = raw[i] ^ keystream[i] + } + + decoded = decrypted + } + } catch (e) { + console.error('[SnsService] TS Decrypt Error:', e) + } } // 写入磁盘缓存 diff --git a/electron/services/wasmService.ts b/electron/services/wasmService.ts index 79575d9..2a5a1ed 100644 --- a/electron/services/wasmService.ts +++ b/electron/services/wasmService.ts @@ -46,7 +46,6 @@ export class WasmService { const wasmPath = path.join(basePath, 'wasm_video_decode.wasm'); const jsPath = path.join(basePath, 'wasm_video_decode.js'); - console.log('[WasmService] Loading WASM from:', wasmPath); if (!fs.existsSync(wasmPath) || !fs.existsSync(jsPath)) { throw new Error(`WASM files not found at ${basePath}`); @@ -88,7 +87,6 @@ export class WasmService { // Define Module mockGlobal.Module = { onRuntimeInitialized: () => { - console.log("[WasmService] WASM Runtime Initialized"); this.wasmLoaded = true; resolve(); }, @@ -133,10 +131,24 @@ export class WasmService { } public async getKeystream(key: string, size: number = 131072): Promise { + // ISAAC-64 uses 8-byte blocks. If size is not a multiple of 8, + // the global reverse() will cause a shift in alignment. + const alignSize = Math.ceil(size / 8) * 8; + const buffer = await this.getRawKeystream(key, alignSize); + + // Reverse the entire aligned buffer + const reversed = new Uint8Array(buffer); + reversed.reverse(); + + // Return exactly the requested size from the beginning of the reversed stream. + // Since we reversed the 'aligned' buffer, index 0 is the last byte of the last block. + return Buffer.from(reversed).subarray(0, size); + } + + public async getRawKeystream(key: string, size: number = 131072): Promise { await this.init(); if (!this.module || !this.module.WxIsaac64) { - // Fallback check for asm.WxIsaac64 logic if needed, but debug showed it on Module if (this.module.asm && this.module.asm.WxIsaac64) { this.module.WxIsaac64 = this.module.asm.WxIsaac64; } @@ -149,26 +161,19 @@ export class WasmService { try { this.capturedKeystream = null; const isaac = new this.module.WxIsaac64(key); - isaac.generate(size); // This triggers the global.wasm_isaac_generate callback + isaac.generate(size); - // Cleanup if possible? isaac.delete()? - // In worker code: p.decryptor.delete() if (isaac.delete) { isaac.delete(); } if (this.capturedKeystream) { - // The worker_release.js logic does: - // p.decryptor_array.set(r.reverse()) - // So the actual keystream is the REVERSE of what is passed to the callback. - const reversed = new Uint8Array(this.capturedKeystream); - reversed.reverse(); - return Buffer.from(reversed); + return Buffer.from(this.capturedKeystream); } else { throw new Error('[WasmService] Failed to capture keystream (callback not called)'); } } catch (error) { - console.error('[WasmService] Error generating keystream:', error); + console.error('[WasmService] Error generating raw keystream:', error); throw error; } } diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index ca08869..d6634d2 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -66,7 +66,7 @@ export class WcdbCore { private wcdbStopMonitorPipe: any = null private monitorPipeClient: any = null - private wcdbDecryptSnsImage: any = null + private avatarUrlCache: Map = new Map() private readonly avatarCacheTtlMs = 10 * 60 * 1000 @@ -144,42 +144,7 @@ export class WcdbCore { } } - /** - * 解密朋友圈图片 - */ - async decryptSnsImage(encryptedData: Buffer, key: string): Promise { - if (!this.initialized) { - const initOk = await this.initialize() - if (!initOk) return encryptedData - } - if (!this.wcdbDecryptSnsImage) return encryptedData - - try { - if (!this.wcdbDecryptSnsImage) { - console.error('[WCDB] wcdbDecryptSnsImage func is null') - return encryptedData - } - - const outPtr = [null as any] - // Koffi pass Buffer as char* pointer - const result = this.wcdbDecryptSnsImage(encryptedData, encryptedData.length, key, outPtr) - - if (result === 0 && outPtr[0]) { - const hex = this.decodeJsonPtr(outPtr[0]) - if (hex) { - return Buffer.from(hex, 'hex') - } - } else { - console.error(`[WCDB] Decrypt SNS image failed with code: ${result}`) - // 主动获取 DLL 内部日志以诊断问题 - await this.printLogs(true) - } - } catch (e) { - console.error('解密图片失败:', e) - } - return encryptedData - } stopMonitor(): void { if (this.monitorPipeClient) { @@ -602,12 +567,7 @@ export class WcdbCore { this.wcdbVerifyUser = null } - // wcdb_status wcdb_decrypt_sns_image(const char* encrypted_data, int32_t data_len, const char* key, char** out_hex) - try { - this.wcdbDecryptSnsImage = this.lib.func('int32 wcdb_decrypt_sns_image(const char* data, int32 len, const char* key, _Out_ void** outHex)') - } catch { - this.wcdbDecryptSnsImage = null - } + // 初始化 const initResult = this.wcdbInit() diff --git a/electron/services/wcdbService.ts b/electron/services/wcdbService.ts index e0464fc..516c11b 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -431,12 +431,7 @@ export class WcdbService { return this.callWorker('verifyUser', { message, hwnd }) } - /** - * 解密朋友圈图片 - */ - async decryptSnsImage(encryptedData: Buffer, key: string): Promise { - return this.callWorker('decryptSnsImage', { encryptedData, key }) - } + } diff --git a/electron/wcdbWorker.ts b/electron/wcdbWorker.ts index 9c7c771..a18acc9 100644 --- a/electron/wcdbWorker.ts +++ b/electron/wcdbWorker.ts @@ -150,9 +150,7 @@ if (parentPort) { case 'verifyUser': result = await core.verifyUser(payload.message, payload.hwnd) break - case 'decryptSnsImage': - result = await core.decryptSnsImage(payload.encryptedData, payload.key) - break + default: result = { success: false, error: `Unknown method: ${type}` } } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 593e67a..1eefbda 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -74,7 +74,7 @@ function SettingsPage() { const exportExcelColumnsDropdownRef = useRef(null) const exportConcurrencyDropdownRef = useRef(null) const [cachePath, setCachePath] = useState('') - const [weixinDllPath, setWeixinDllPath] = useState('') + const [logEnabled, setLogEnabled] = useState(false) const [whisperModelName, setWhisperModelName] = useState('base') const [whisperModelDir, setWhisperModelDir] = useState('') @@ -250,7 +250,7 @@ function SettingsPage() { const savedPath = await configService.getDbPath() const savedWxid = await configService.getMyWxid() const savedCachePath = await configService.getCachePath() - const savedWeixinDllPath = await configService.getWeixinDllPath() + const savedExportPath = await configService.getExportPath() const savedLogEnabled = await configService.getLogEnabled() const savedImageXorKey = await configService.getImageXorKey() @@ -279,7 +279,7 @@ function SettingsPage() { if (savedPath) setDbPath(savedPath) if (savedWxid) setWxid(savedWxid) if (savedCachePath) setCachePath(savedCachePath) - if (savedWeixinDllPath) setWeixinDllPath(savedWeixinDllPath) + const wxidConfig = savedWxid ? await configService.getWxidConfig(savedWxid) : null const decryptKeyToUse = wxidConfig?.decryptKey ?? savedKey ?? '' @@ -616,29 +616,7 @@ function SettingsPage() { await applyWxidSelection(selectedWxid) } - const handleSelectWeixinDllPath = async () => { - try { - const result = await dialog.openFile({ - title: '选择 Weixin.dll 文件', - properties: ['openFile'], - filters: [{ name: 'DLL', extensions: ['dll'] }] - }) - if (!result.canceled && result.filePaths.length > 0) { - const selectedPath = result.filePaths[0] - setWeixinDllPath(selectedPath) - await configService.setWeixinDllPath(selectedPath) - showMessage('已选择 Weixin.dll 路径', true) - } - } catch { - showMessage('选择 Weixin.dll 失败', false) - } - } - const handleResetWeixinDllPath = async () => { - setWeixinDllPath('') - await configService.setWeixinDllPath('') - showMessage('已清空 Weixin.dll 路径', true) - } const handleSelectCachePath = async () => { try { const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] }) @@ -1332,28 +1310,7 @@ function SettingsPage() { -
- - 用于朋友圈在线图片原生解密,优先使用这里配置的 DLL - { - const value = e.target.value - setWeixinDllPath(value) - scheduleConfigSave('weixinDllPath', () => configService.setWeixinDllPath(value)) - }} - /> -
- - -
-
+
diff --git a/src/services/config.ts b/src/services/config.ts index 684bdcf..089a78e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -12,7 +12,7 @@ export const CONFIG_KEYS = { LAST_SESSION: 'lastSession', WINDOW_BOUNDS: 'windowBounds', CACHE_PATH: 'cachePath', - WEIXIN_DLL_PATH: 'weixinDllPath', + EXPORT_PATH: 'exportPath', AGREEMENT_ACCEPTED: 'agreementAccepted', LOG_ENABLED: 'logEnabled', @@ -163,16 +163,7 @@ export async function setCachePath(path: string): Promise { } -// 获取 Weixin.dll 路径 -export async function getWeixinDllPath(): Promise { - const value = await config.get(CONFIG_KEYS.WEIXIN_DLL_PATH) - return value as string | null -} -// 设置 Weixin.dll 路径 -export async function setWeixinDllPath(path: string): Promise { - await config.set(CONFIG_KEYS.WEIXIN_DLL_PATH, path) -} // 获取导出路径 export async function getExportPath(): Promise {