Merge branch 'dev' into dev

This commit is contained in:
xuncha
2026-01-25 18:04:50 +08:00
committed by GitHub
10 changed files with 932 additions and 10459 deletions

View File

@@ -209,10 +209,11 @@ function createOnboardingWindow() {
: join(process.resourcesPath, 'icon.ico')
onboardingWindow = new BrowserWindow({
width: 1100,
height: 720,
width: 960,
height: 680,
minWidth: 900,
minHeight: 600,
minHeight: 620,
resizable: false,
frame: false,
transparent: true,
backgroundColor: '#00000000',

View File

@@ -882,16 +882,17 @@ export class KeyService {
return null
}
private isAlphaNumAscii(byte: number): boolean {
return (byte >= 0x61 && byte <= 0x7a) || (byte >= 0x41 && byte <= 0x5a) || (byte >= 0x30 && byte <= 0x39)
private isAlphaNumLower(byte: number): boolean {
// 只匹配小写字母 a-z 和数字 0-9AES密钥格式
return (byte >= 0x61 && byte <= 0x7a) || (byte >= 0x30 && byte <= 0x39)
}
private isUtf16AsciiKey(buf: Buffer, start: number): boolean {
private isUtf16LowerKey(buf: Buffer, start: number): boolean {
if (start + 64 > buf.length) return false
for (let j = 0; j < 32; j++) {
const charByte = buf[start + j * 2]
const nullByte = buf[start + j * 2 + 1]
if (nullByte !== 0x00 || !this.isAlphaNumAscii(charByte)) {
if (nullByte !== 0x00 || !this.isAlphaNumLower(charByte)) {
return false
}
}
@@ -924,8 +925,6 @@ export class KeyService {
const regions: Array<[number, number]> = []
const MEM_COMMIT = 0x1000
const MEM_PRIVATE = 0x20000
const MEM_MAPPED = 0x40000
const MEM_IMAGE = 0x1000000
const PAGE_NOACCESS = 0x01
const PAGE_GUARD = 0x100
@@ -940,10 +939,9 @@ export class KeyService {
const protect = info.Protect
const type = info.Type
const regionSize = Number(info.RegionSize)
if (state === MEM_COMMIT && (protect & PAGE_NOACCESS) === 0 && (protect & PAGE_GUARD) === 0) {
if (type === MEM_PRIVATE || type === MEM_MAPPED || type === MEM_IMAGE) {
regions.push([Number(info.BaseAddress), regionSize])
}
// 只收集已提交的私有内存(大幅减少扫描区域)
if (state === MEM_COMMIT && type === MEM_PRIVATE && (protect & PAGE_NOACCESS) === 0 && (protect & PAGE_GUARD) === 0) {
regions.push([Number(info.BaseAddress), regionSize])
}
const nextAddress = address + regionSize
@@ -972,86 +970,51 @@ export class KeyService {
try {
const allRegions = this.getMemoryRegions(hProcess)
const totalRegions = allRegions.length
let scannedCount = 0
let skippedCount = 0
// 优化1: 只保留小内存区域(< 10MB- 密钥通常在小区域,可大幅减少扫描时间
const filteredRegions = allRegions.filter(([_, size]) => size <= 10 * 1024 * 1024)
for (const [baseAddress, regionSize] of allRegions) {
// 跳过太大的内存区域(> 100MB
if (regionSize > 100 * 1024 * 1024) {
skippedCount++
continue
}
// 优化2: 优先级排序 - 按大小升序,先扫描小区域(密钥通常在较小区域)
const sortedRegions = filteredRegions.sort((a, b) => a[1] - b[1])
scannedCount++
if (scannedCount % 10 === 0) {
onProgress?.(scannedCount, totalRegions, `正在扫描微信内存... (${scannedCount}/${totalRegions})`)
await new Promise(resolve => setImmediate(resolve))
}
// 优化3: 计算总字节数用于精确进度报告
const totalBytes = sortedRegions.reduce((sum, [_, size]) => sum + size, 0)
let processedBytes = 0
const memory = this.readProcessMemory(hProcess, baseAddress, regionSize)
if (!memory) continue
// 优化4: 减小分块大小到 1MB参考 wx_key 项目)
const chunkSize = 1 * 1024 * 1024
const overlap = 65
let currentRegion = 0
// 直接在原始字节中搜索32字节的小写字母数字序列
for (let i = 0; i < memory.length - 34; i++) {
// 检查前导字符(不是小写字母或数字)
if (this.isAlphaNumLower(memory[i])) continue
for (const [baseAddress, regionSize] of sortedRegions) {
currentRegion++
const progress = totalBytes > 0 ? Math.floor((processedBytes / totalBytes) * 100) : 0
onProgress?.(progress, 100, `扫描内存 ${progress}% (${currentRegion}/${sortedRegions.length})`)
// 检查接下来32个字节是否都是小写字母或数字
let valid = true
for (let j = 1; j <= 32; j++) {
if (!this.isAlphaNumLower(memory[i + j])) {
valid = false
break
}
}
if (!valid) continue
// 每个区域都让出主线程确保UI流畅
await new Promise(resolve => setImmediate(resolve))
let offset = 0
let trailing: Buffer | null = null
while (offset < regionSize) {
const remaining = regionSize - offset
const currentChunkSize = remaining > chunkSize ? chunkSize : remaining
const chunk = this.readProcessMemory(hProcess, baseAddress + offset, currentChunkSize)
if (!chunk || !chunk.length) {
offset += currentChunkSize
trailing = null
// 检查尾部字符(不是小写字母或数字)
if (i + 33 < memory.length && this.isAlphaNumLower(memory[i + 33])) {
continue
}
let dataToScan: Buffer
if (trailing && trailing.length) {
dataToScan = Buffer.concat([trailing, chunk])
} else {
dataToScan = chunk
const keyBytes = memory.subarray(i + 1, i + 33)
if (this.verifyKey(ciphertext, keyBytes)) {
return keyBytes.toString('ascii')
}
for (let i = 0; i < dataToScan.length - 34; i++) {
if (this.isAlphaNumAscii(dataToScan[i])) continue
let valid = true
for (let j = 1; j <= 32; j++) {
if (!this.isAlphaNumAscii(dataToScan[i + j])) {
valid = false
break
}
}
if (valid && this.isAlphaNumAscii(dataToScan[i + 33])) {
valid = false
}
if (valid) {
const keyBytes = dataToScan.subarray(i + 1, i + 33)
if (this.verifyKey(ciphertext, keyBytes)) {
return keyBytes.toString('ascii')
}
}
}
for (let i = 0; i < dataToScan.length - 65; i++) {
if (!this.isUtf16AsciiKey(dataToScan, i)) continue
const keyBytes = Buffer.alloc(32)
for (let j = 0; j < 32; j++) {
keyBytes[j] = dataToScan[i + j * 2]
}
if (this.verifyKey(ciphertext, keyBytes)) {
return keyBytes.toString('ascii')
}
}
const start = dataToScan.length - overlap
trailing = dataToScan.subarray(start < 0 ? 0 : start)
offset += currentChunkSize
}
// 更新已处理字节数
processedBytes += regionSize
}
return null
} finally {

View File

@@ -20,6 +20,7 @@ export class WcdbCore {
private currentWxid: string | null = null
// 函数引用
private wcdbInitProtection: any = null
private wcdbInit: any = null
private wcdbShutdown: any = null
private wcdbOpenAccount: any = null
@@ -243,6 +244,18 @@ export class WcdbCore {
this.lib = this.koffi.load(dllPath)
// InitProtection (Added for security)
try {
this.wcdbInitProtection = this.lib.func('bool InitProtection(const char* resourcePath)')
const protectionOk = this.wcdbInitProtection(dllDir)
if (!protectionOk) {
console.error('Core security check failed')
return false
}
} catch (e) {
console.warn('InitProtection symbol not found:', e)
}
// 定义类型
// wcdb_status wcdb_init()
this.wcdbInit = this.lib.func('int32 wcdb_init()')

9797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"description": "WeFlow",
"main": "dist-electron/main.js",
"author": "cc",
"//": "二改不应改变此处的作者与应用信息",
"scripts": {
"postinstall": "echo 'No native modules to rebuild'",
"rebuild": "echo 'No native modules to rebuild'",

Binary file not shown.

View File

@@ -1,24 +1,24 @@
.sidebar {
width: 200px;
width: 220px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 16px 0;
transition: width 0.25s ease;
&.collapsed {
width: 64px;
.nav-menu,
.sidebar-footer {
padding: 0 8px;
}
.nav-label {
display: none;
}
.nav-item {
justify-content: center;
padding: 10px;
@@ -32,14 +32,14 @@
display: flex;
flex-direction: column;
gap: 4px;
padding: 0 8px;
padding: 0 12px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
padding: 10px 12px;
border-radius: 9999px;
color: var(--text-secondary);
text-decoration: none;
@@ -49,13 +49,12 @@
background: transparent;
cursor: pointer;
font-family: inherit;
width: 100%;
&:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
&.active {
background: var(--primary);
color: white;
@@ -99,9 +98,9 @@
border-radius: 9999px;
transition: all 0.2s ease;
margin-top: 4px;
&:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
}
}

View File

@@ -1076,8 +1076,7 @@
align-items: center;
justify-content: center;
gap: 10px;
background: rgba(10, 10, 10, 0.28);
backdrop-filter: blur(6px);
background: var(--bg-tertiary);
transition: opacity 200ms ease;
z-index: 2;
}

File diff suppressed because it is too large Load Diff

View File

@@ -312,6 +312,67 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
if (isDbConnected) {
return (
<div className={rootClassName}>
<div className="welcome-container">
{showWindowControls && (
<div className="window-controls">
<button type="button" className="window-btn" onClick={handleMinimize} aria-label="最小化">
<Minus size={14} />
</button>
<button type="button" className="window-btn is-close" onClick={handleCloseWindow} aria-label="关闭">
<X size={14} />
</button>
</div>
)}
<div className="welcome-sidebar">
<div className="sidebar-header">
<img src="./logo.png" alt="WeFlow" className="sidebar-logo" />
<div className="sidebar-brand">
<span className="brand-name">WeFlow</span>
<span className="brand-tag">Connected</span>
</div>
</div>
<div className="sidebar-spacer" style={{ flex: 1 }} />
<div className="sidebar-footer">
<ShieldCheck size={14} />
<span></span>
</div>
</div>
<div className="welcome-content success-content">
<div className="success-body">
<div className="success-icon">
<CheckCircle2 size={48} />
</div>
<h1 className="success-title"></h1>
<p className="success-desc">使</p>
<button
className="btn btn-primary btn-large"
onClick={() => {
if (standalone) {
setIsClosing(true)
setTimeout(() => {
window.electronAPI.window.completeOnboarding()
}, 450)
} else {
navigate('/home')
}
}}
>
<ArrowRight size={18} />
</button>
</div>
</div>
</div>
</div>
)
}
return (
<div className={rootClassName}>
<div className="welcome-container">
{showWindowControls && (
<div className="window-controls">
<button type="button" className="window-btn" onClick={handleMinimize} aria-label="最小化">
@@ -322,187 +383,179 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
</button>
</div>
)}
<div className="welcome-shell">
<div className="welcome-panel">
<div className="panel-header">
<img src="./logo.png" alt="WeFlow" className="panel-logo" />
<div>
<p className="panel-kicker">WeFlow</p>
<h1></h1>
</div>
<div className="welcome-sidebar">
<div className="sidebar-header">
<img src="./logo.png" alt="WeFlow" className="sidebar-logo" />
<div className="sidebar-brand">
<span className="brand-name">WeFlow</span>
<span className="brand-tag">Setup</span>
</div>
<div className="panel-note">
<CheckCircle2 size={16} />
<span></span>
</div>
<button
className="btn btn-primary btn-full"
onClick={() => {
if (standalone) {
setIsClosing(true)
setTimeout(() => {
window.electronAPI.window.completeOnboarding()
}, 450)
} else {
navigate('/home')
}
}}
>
</button>
</div>
</div>
</div>
)
}
return (
<div className={rootClassName}>
{showWindowControls && (
<div className="window-controls">
<button type="button" className="window-btn" onClick={handleMinimize} aria-label="最小化">
<Minus size={14} />
</button>
<button type="button" className="window-btn is-close" onClick={handleCloseWindow} aria-label="关闭">
<X size={14} />
</button>
</div>
)}
<div className="welcome-shell">
<div className="welcome-panel">
<div className="panel-header">
<img src="./logo.png" alt="WeFlow" className="panel-logo" />
<div>
<p className="panel-kicker"></p>
<h1>WeFlow </h1>
<p className="panel-subtitle"></p>
</div>
</div>
<div className="step-list">
<div className="sidebar-nav">
{steps.map((step, index) => (
<div key={step.id} className={`step-item ${index === stepIndex ? 'active' : ''} ${index < stepIndex ? 'done' : ''}`}>
<div className="step-index">{index < stepIndex ? <CheckCircle2 size={14} /> : index + 1}</div>
<div>
<div className="step-title">{step.title}</div>
<div className="step-desc">{step.desc}</div>
<div key={step.id} className={`nav-item ${index === stepIndex ? 'active' : ''} ${index < stepIndex ? 'completed' : ''}`}>
<div className="nav-indicator">
{index < stepIndex ? <CheckCircle2 size={14} /> : <div className="dot" />}
</div>
<div className="nav-info">
<div className="nav-title">{step.title}</div>
<div className="nav-desc">{step.desc}</div>
</div>
</div>
))}
</div>
<div className="panel-foot">
<ShieldCheck size={16} />
<div className="sidebar-footer">
<ShieldCheck size={14} />
<span></span>
</div>
</div>
<div className="setup-card">
<div className="setup-header">
<div className="setup-icon">
{currentStep.id === 'intro' && <Sparkles size={18} />}
{currentStep.id === 'db' && <Database size={18} />}
{currentStep.id === 'cache' && <HardDrive size={18} />}
{currentStep.id === 'key' && <KeyRound size={18} />}
{currentStep.id === 'image' && <ShieldCheck size={18} />}
</div>
<div className="welcome-content">
<div className="content-header">
<div>
<h2>{currentStep.title}</h2>
<p>{currentStep.desc}</p>
<p className="header-desc">{currentStep.desc}</p>
</div>
</div>
{currentStep.id === 'intro' && (
<div className="setup-body">
<div className="intro-card">
<Wand2 size={18} />
<div>
<h3></h3>
<p></p>
</div>
<div className="content-body">
{currentStep.id === 'intro' && (
<div className="intro-block">
{/* 内容移至底部 */}
</div>
</div>
)}
)}
{currentStep.id === 'db' && (
<div className="setup-body">
<label className="field-label"></label>
<input
type="text"
className="field-input"
placeholder="例如C:\\Users\\xxx\\Documents\\xwechat_files"
value={dbPath}
onChange={(e) => setDbPath(e.target.value)}
/>
<div className="button-row">
<button className="btn btn-secondary" onClick={handleAutoDetectPath} disabled={isDetectingPath}>
<FolderSearch size={16} /> {isDetectingPath ? '检测中...' : '自动检测'}
</button>
<button className="btn btn-primary" onClick={handleSelectPath}>
<FolderOpen size={16} />
</button>
{currentStep.id === 'db' && (
<div className="form-group">
<label className="field-label"></label>
<div className="input-group">
<input
type="text"
className="field-input"
placeholder="例如C:\\Users\\xxx\\Documents\\xwechat_files"
value={dbPath}
onChange={(e) => setDbPath(e.target.value)}
/>
</div>
<div className="action-row">
<button className="btn btn-secondary" onClick={handleAutoDetectPath} disabled={isDetectingPath}>
<FolderSearch size={16} /> {isDetectingPath ? '检测中...' : '自动检测'}
</button>
<button className="btn btn-secondary" onClick={handleSelectPath}>
<FolderOpen size={16} /> ...
</button>
</div>
<div className="field-hint">--</div>
<div className="field-hint warning">
</div>
</div>
<div className="field-hint" style={{ color: '#ff6b6b', fontSize: '14px', fontWeight: '500' }}>--</div>
<div className="field-hint" style={{ color: '#ff6b6b', marginTop: '4px', fontSize: '14px', fontWeight: '500' }}> --</div>
</div>
)}
{currentStep.id === 'cache' && (
<div className="setup-body">
<label className="field-label"></label>
<input
type="text"
className="field-input"
placeholder="留空使用默认目录"
value={cachePath}
onChange={(e) => setCachePath(e.target.value)}
/>
<div className="button-row">
<button className="btn btn-primary" onClick={handleSelectCachePath}>
<FolderOpen size={16} />
</button>
<button className="btn btn-secondary" onClick={() => setCachePath('')}>
<RotateCcw size={16} /> 使
</button>
</div>
<div className="field-hint">使</div>
</div>
)}
{currentStep.id === 'key' && (
<div className="setup-body">
<label className="field-label"> wxid</label>
<input
type="text"
className="field-input"
placeholder="获取密钥后将自动填充"
value={wxid}
onChange={(e) => setWxid(e.target.value)}
/>
<label className="field-label"></label>
<div className="field-with-toggle">
<input
type={showDecryptKey ? 'text' : 'password'}
className="field-input"
placeholder="64 位十六进制密钥"
value={decryptKey}
onChange={(e) => setDecryptKey(e.target.value.trim())}
/>
<button type="button" className="toggle-btn" onClick={() => setShowDecryptKey(!showDecryptKey)}>
{showDecryptKey ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
</div>
{isManualStartPrompt ? (
<div className="manual-prompt">
<p className="prompt-text"></p>
<button className="btn btn-primary" onClick={handleManualConfirm}>
{currentStep.id === 'cache' && (
<div className="form-group">
<label className="field-label"></label>
<div className="input-group">
<input
type="text"
className="field-input"
placeholder="留空即使用默认目录"
value={cachePath}
onChange={(e) => setCachePath(e.target.value)}
/>
</div>
<div className="action-row">
<button className="btn btn-secondary" onClick={handleSelectCachePath}>
<FolderOpen size={16} />
</button>
<button className="btn btn-secondary" onClick={() => setCachePath('')}>
<RotateCcw size={16} />
</button>
</div>
) : (
<button className="btn btn-secondary btn-inline" onClick={handleAutoGetDbKey} disabled={isFetchingDbKey}>
{isFetchingDbKey ? '获取中...' : '自动获取密钥'}
<div className="field-hint"></div>
</div>
)}
{currentStep.id === 'key' && (
<div className="form-group">
<label className="field-label"> (Wxid)</label>
<input
type="text"
className="field-input"
placeholder="等待获取..."
value={wxid}
readOnly
onChange={(e) => setWxid(e.target.value)}
/>
<label className="field-label mt-4"></label>
<div className="field-with-toggle">
<input
type={showDecryptKey ? 'text' : 'password'}
className="field-input"
placeholder="64 位十六进制密钥"
value={decryptKey}
onChange={(e) => setDecryptKey(e.target.value.trim())}
/>
<button type="button" className="toggle-btn" onClick={() => setShowDecryptKey(!showDecryptKey)}>
{showDecryptKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
<div className="key-actions">
{isManualStartPrompt ? (
<div className="manual-prompt">
<p></p>
<button className="btn btn-primary" onClick={handleManualConfirm}>
</button>
</div>
) : (
<button className="btn btn-secondary btn-block" onClick={handleAutoGetDbKey} disabled={isFetchingDbKey}>
{isFetchingDbKey ? '正在获取...' : '自动获取密钥'}
</button>
)}
</div>
{dbKeyStatus && <div className="status-message">{dbKeyStatus}</div>}
<div className="field-hint"></div>
</div>
)}
{currentStep.id === 'image' && (
<div className="form-group">
<div className="grid-2">
<div>
<label className="field-label"> XOR </label>
<input
type="text"
className="field-input"
placeholder="0x..."
value={imageXorKey}
onChange={(e) => setImageXorKey(e.target.value)}
/>
</div>
<div>
<label className="field-label"> AES </label>
<input
type="text"
className="field-input"
placeholder="16位密钥"
value={imageAesKey}
onChange={(e) => setImageAesKey(e.target.value)}
/>
</div>
</div>
<button className="btn btn-secondary btn-block mt-4" onClick={handleAutoGetImageKey} disabled={isFetchingImageKey}>
{isFetchingImageKey ? '扫描中...' : '自动获取图片密钥'}
</button>
)}
{dbKeyStatus && <div className="field-hint status-text">{dbKeyStatus}</div>}
<div className="field-hint"></div>
@@ -510,46 +563,27 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
</div>
)}
{currentStep.id === 'image' && (
<div className="setup-body">
<label className="field-label"> XOR </label>
<input
type="text"
className="field-input"
placeholder="例如0xA4"
value={imageXorKey}
onChange={(e) => setImageXorKey(e.target.value)}
/>
<label className="field-label"> AES </label>
<input
type="text"
className="field-input"
placeholder="16 位密钥"
value={imageAesKey}
onChange={(e) => setImageAesKey(e.target.value)}
/>
<button className="btn btn-secondary btn-inline" onClick={handleAutoGetImageKey} disabled={isFetchingImageKey}>
{isFetchingImageKey ? '获取中...' : '自动获取图片密钥'}
</button>
{imageKeyStatus && <div className="field-hint status-text">{imageKeyStatus}</div>}
<div className="field-hint"></div>
{isFetchingImageKey && <div className="field-hint status-text">...</div>}
{error && <div className="error-message">{error}</div>}
{currentStep.id === 'intro' && (
<div className="intro-footer">
<p></p>
<p>WeFlow 访</p>
</div>
)}
{error && <div className="error-message">{error}</div>}
<div className="setup-actions">
<button className="btn btn-tertiary" onClick={handleBack} disabled={stepIndex === 0}>
<div className="content-actions">
<button className="btn btn-ghost" onClick={handleBack} disabled={stepIndex === 0}>
<ArrowLeft size={16} />
</button>
{stepIndex < steps.length - 1 ? (
<button className="btn btn-primary" onClick={handleNext} disabled={!canGoNext()}>
<ArrowRight size={16} />
</button>
) : (
<button className="btn btn-primary" onClick={handleConnect} disabled={isConnecting || !canGoNext()}>
{isConnecting ? '连接中...' : '测试并完成'}
{isConnecting ? '连接中...' : '完成配置'} <ArrowRight size={16} />
</button>
)}
</div>