mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat: 初步实现linux上的密钥获取
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -68,3 +68,4 @@ CLAUDE.md
|
|||||||
resources/wx_send
|
resources/wx_send
|
||||||
概述.md
|
概述.md
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
/pnpm-workspace.yaml
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { annualReportService } from './services/annualReportService'
|
|||||||
import { exportService, ExportOptions, ExportProgress } from './services/exportService'
|
import { exportService, ExportOptions, ExportProgress } from './services/exportService'
|
||||||
import { KeyService } from './services/keyService'
|
import { KeyService } from './services/keyService'
|
||||||
import { KeyServiceMac } from './services/keyServiceMac'
|
import { KeyServiceMac } from './services/keyServiceMac'
|
||||||
|
import { KeyServiceLinux } from './services/keyServiceLinux';
|
||||||
import { voiceTranscribeService } from './services/voiceTranscribeService'
|
import { voiceTranscribeService } from './services/voiceTranscribeService'
|
||||||
import { videoService } from './services/videoService'
|
import { videoService } from './services/videoService'
|
||||||
import { snsService, isVideoUrl } from './services/snsService'
|
import { snsService, isVideoUrl } from './services/snsService'
|
||||||
@@ -89,9 +90,15 @@ let onboardingWindow: BrowserWindow | null = null
|
|||||||
let splashWindow: BrowserWindow | null = null
|
let splashWindow: BrowserWindow | null = null
|
||||||
const sessionChatWindows = new Map<string, BrowserWindow>()
|
const sessionChatWindows = new Map<string, BrowserWindow>()
|
||||||
const sessionChatWindowSources = new Map<string, 'chat' | 'export'>()
|
const sessionChatWindowSources = new Map<string, 'chat' | 'export'>()
|
||||||
const keyService = process.platform === 'darwin'
|
|
||||||
? new KeyServiceMac() as any
|
let keyService: KeyService | KeyServiceMac | KeyServiceLinux;
|
||||||
: new KeyService()
|
if (process.platform === 'darwin') {
|
||||||
|
keyService = new KeyServiceMac();
|
||||||
|
} else if (process.platform === 'linux') {
|
||||||
|
keyService = new KeyServiceLinux();
|
||||||
|
} else {
|
||||||
|
keyService = new KeyService();
|
||||||
|
}
|
||||||
|
|
||||||
let mainWindowReady = false
|
let mainWindowReady = false
|
||||||
let shouldShowMain = true
|
let shouldShowMain = true
|
||||||
|
|||||||
182
electron/services/keyServiceLinux.ts
Normal file
182
electron/services/keyServiceLinux.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
|
||||||
|
import { execFile, exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import sudo from 'sudo-prompt'
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
type DbKeyResult = { success: boolean; key?: string; error?: string; logs?: string[] }
|
||||||
|
type ImageKeyResult = { success: boolean; xorKey?: number; aesKey?: string; error?: string }
|
||||||
|
|
||||||
|
export class KeyServiceLinux {
|
||||||
|
|
||||||
|
private getHelperPath(): string {
|
||||||
|
const isPackaged = app.isPackaged
|
||||||
|
const candidates: string[] = []
|
||||||
|
if (process.env.WX_KEY_HELPER_PATH) candidates.push(process.env.WX_KEY_HELPER_PATH)
|
||||||
|
if (isPackaged) {
|
||||||
|
candidates.push(join(process.resourcesPath, 'resources', 'xkey_helper_linux'))
|
||||||
|
candidates.push(join(process.resourcesPath, 'xkey_helper_linux'))
|
||||||
|
} else {
|
||||||
|
candidates.push(join(app.getAppPath(), 'resources', 'xkey_helper_linux'))
|
||||||
|
candidates.push(join(app.getAppPath(), '..', 'Xkey', 'build', 'xkey_helper_linux'))
|
||||||
|
}
|
||||||
|
for (const p of candidates) {
|
||||||
|
if (existsSync(p)) return p
|
||||||
|
}
|
||||||
|
throw new Error('找不到 xkey_helper_linux,请检查路径')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async autoGetDbKey(): Promise<DbKeyResult> {
|
||||||
|
try {
|
||||||
|
console.log('[Linux KeyService] 1. 尝试结束当前微信进程...')
|
||||||
|
await execAsync('killall -9 wechat wechat-bin xwechat').catch(() => {})
|
||||||
|
// 稍微等待进程完全退出
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
|
||||||
|
console.log('[Linux KeyService] 2. 尝试拉起微信...')
|
||||||
|
const startCmds = [
|
||||||
|
'nohup wechat >/dev/null 2>&1 &',
|
||||||
|
'nohup wechat-bin >/dev/null 2>&1 &',
|
||||||
|
'nohup xwechat >/dev/null 2>&1 &'
|
||||||
|
]
|
||||||
|
for (const cmd of startCmds) execAsync(cmd).catch(() => {})
|
||||||
|
|
||||||
|
console.log('[Linux KeyService] 3. 等待微信进程出现...')
|
||||||
|
let pid = 0
|
||||||
|
for (let i = 0; i < 15; i++) { // 最多等 15 秒
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
const { stdout } = await execAsync('pidof wechat wechat-bin xwechat').catch(() => ({ stdout: '' }))
|
||||||
|
const pids = stdout.trim().split(/\s+/).filter(p => p)
|
||||||
|
if (pids.length > 0) {
|
||||||
|
pid = parseInt(pids[0], 10)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pid) {
|
||||||
|
return { success: false, error: '未能自动启动微信,请手动启动并登录。' }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Linux KeyService] 4. 捕获到微信 PID: ${pid},准备获取密钥...`)
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 2000))
|
||||||
|
|
||||||
|
return await this.getDbKey(pid)
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: '自动获取微信 PID 失败: ' + err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDbKey(pid: number): Promise<DbKeyResult> {
|
||||||
|
try {
|
||||||
|
const helperPath = this.getHelperPath()
|
||||||
|
|
||||||
|
const { stdout: scanOut } = await execFileAsync(helperPath, ['db_scan', pid.toString()])
|
||||||
|
const scanRes = JSON.parse(scanOut.trim())
|
||||||
|
|
||||||
|
if (!scanRes.success) {
|
||||||
|
return { success: false, error: scanRes.result || '扫描失败,请确保微信已完全登录' }
|
||||||
|
}
|
||||||
|
const targetAddr = scanRes.target_addr
|
||||||
|
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const options = { name: 'WeFlow' }
|
||||||
|
const command = `"${helperPath}" db_hook ${pid} ${targetAddr}`
|
||||||
|
|
||||||
|
sudo.exec(command, options, (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
resolve({ success: false, error: `授权失败或被取消: ${error.message}` })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const hookRes = JSON.parse((stdout as string).trim())
|
||||||
|
if (hookRes.success) {
|
||||||
|
resolve({ success: true, key: hookRes.key })
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: hookRes.result })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ success: false, error: '解析 Hook 结果失败' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async autoGetImageKey(_accountPath: string, wxid?: string): Promise<ImageKeyResult> {
|
||||||
|
try {
|
||||||
|
const helperPath = this.getHelperPath()
|
||||||
|
const { stdout } = await execFileAsync(helperPath, ['image_local'])
|
||||||
|
const res = JSON.parse(stdout.trim())
|
||||||
|
if (!res.success) return { success: false, error: res.result }
|
||||||
|
|
||||||
|
const accounts = res.data.accounts || []
|
||||||
|
let account = accounts.find((a: any) => a.wxid === wxid)
|
||||||
|
if (!account && accounts.length > 0) account = accounts[0]
|
||||||
|
|
||||||
|
if (account && account.keys && account.keys.length > 0) {
|
||||||
|
const keyObj = account.keys[0]
|
||||||
|
return { success: true, xorKey: keyObj.xorKey, aesKey: keyObj.aesKey }
|
||||||
|
}
|
||||||
|
return { success: false, error: '未在缓存中找到匹配的图片密钥' }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async autoGetImageKeyByMemoryScan(accountPath: string): Promise<ImageKeyResult> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('pidof wechat wechat-bin xwechat').catch(() => ({ stdout: '' }))
|
||||||
|
const pids = stdout.trim().split(/\s+/).filter(p => p)
|
||||||
|
if (pids.length === 0) return { success: false, error: '微信未运行,无法扫描内存' }
|
||||||
|
const pid = parseInt(pids[0], 10)
|
||||||
|
|
||||||
|
const ciphertextHex = this.findAnyDatCiphertext(accountPath)
|
||||||
|
if (!ciphertextHex) {
|
||||||
|
return { success: false, error: '未在 FileStorage/Image 中找到 .dat 图片,请在微信中随便点开一张大图后重试' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const helperPath = this.getHelperPath()
|
||||||
|
const { stdout: memOut } = await execFileAsync(helperPath, ['image_mem', pid.toString(), ciphertextHex])
|
||||||
|
const res = JSON.parse(memOut.trim())
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
return { success: true, aesKey: res.key }
|
||||||
|
}
|
||||||
|
return { success: false, error: res.result }
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findAnyDatCiphertext(accountPath: string): string | null {
|
||||||
|
try {
|
||||||
|
const imgDir = join(accountPath, 'FileStorage', 'Image')
|
||||||
|
if (!existsSync(imgDir)) return null
|
||||||
|
|
||||||
|
const months = readdirSync(imgDir).filter(f => !f.startsWith('.') && statSync(join(imgDir, f)).isDirectory())
|
||||||
|
months.sort((a, b) => b.localeCompare(a))
|
||||||
|
|
||||||
|
for (const month of months) {
|
||||||
|
const monthDir = join(imgDir, month)
|
||||||
|
const files = readdirSync(monthDir).filter(f => f.endsWith('.dat'))
|
||||||
|
if (files.length > 0) {
|
||||||
|
const target = join(monthDir, files[0])
|
||||||
|
const buffer = readFileSync(target)
|
||||||
|
if (buffer.length >= 16) {
|
||||||
|
return buffer.subarray(0, 16).toString('hex')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Linux KeyService] 查找 .dat 失败:', e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sherpa-onnx-node": "^1.10.38",
|
"sherpa-onnx-node": "^1.10.38",
|
||||||
"silk-wasm": "^3.7.1",
|
"silk-wasm": "^3.7.1",
|
||||||
|
"sudo-prompt": "^9.2.1",
|
||||||
"wechat-emojis": "^1.0.2",
|
"wechat-emojis": "^1.0.2",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
resources/xkey_helper_linux
Executable file
BIN
resources/xkey_helper_linux
Executable file
Binary file not shown.
@@ -30,6 +30,16 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
|||||||
{ id: 'about', label: '关于', icon: Info }
|
{ id: 'about', label: '关于', icon: Info }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||||
|
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||||
|
|
||||||
|
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
|
||||||
|
const dbPathPlaceholder = isMac
|
||||||
|
? '例如: ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9'
|
||||||
|
: isLinux
|
||||||
|
? '例如: ~/.local/share/WeChat/xwechat_files 或者 ~/Documents/xwechat_files'
|
||||||
|
: '例如: C:\\Users\\xxx\\Documents\\xwechat_files'
|
||||||
|
|
||||||
|
|
||||||
interface WxidOption {
|
interface WxidOption {
|
||||||
wxid: string
|
wxid: string
|
||||||
@@ -1371,7 +1381,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<span className="form-hint">xwechat_files 目录</span>
|
<span className="form-hint">xwechat_files 目录</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="例如: C:\Users\xxx\Documents\xwechat_files"
|
placeholder={dbPathPlaceholder}
|
||||||
value={dbPath}
|
value={dbPath}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
|
|||||||
@@ -11,9 +11,19 @@ import {
|
|||||||
import ConfirmDialog from '../components/ConfirmDialog'
|
import ConfirmDialog from '../components/ConfirmDialog'
|
||||||
import './WelcomePage.scss'
|
import './WelcomePage.scss'
|
||||||
|
|
||||||
|
const isMac = navigator.userAgent.toLowerCase().includes('mac')
|
||||||
|
const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||||
|
|
||||||
|
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
|
||||||
|
const dbPathPlaceholder = isMac
|
||||||
|
? '例如: ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9'
|
||||||
|
: isLinux
|
||||||
|
? '例如: ~/.local/share/WeChat/xwechat_files 或者 ~/Documents/xwechat_files'
|
||||||
|
: '例如: C:\\Users\\xxx\\Documents\\xwechat_files'
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ id: 'intro', title: '欢迎', desc: '准备开始你的本地数据探索' },
|
{ id: 'intro', title: '欢迎', desc: '准备开始你的本地数据探索' },
|
||||||
{ id: 'db', title: '数据库目录', desc: '定位 xwechat_files 目录' },
|
{ id: 'db', title: '数据库目录', desc: `定位 ${dbDirName}` },
|
||||||
{ id: 'cache', title: '缓存目录', desc: '设置本地缓存存储位置(可选)' },
|
{ id: 'cache', title: '缓存目录', desc: '设置本地缓存存储位置(可选)' },
|
||||||
{ id: 'key', title: '解密密钥', desc: '获取密钥与自动识别账号' },
|
{ id: 'key', title: '解密密钥', desc: '获取密钥与自动识别账号' },
|
||||||
{ id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' },
|
{ id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' },
|
||||||
|
|||||||
Reference in New Issue
Block a user