mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 12:07:01 +08:00
实现图片解密设置页
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import path from 'path'
|
||||
import type { ImageKeyConfigResult, SaveImageKeyRequest } from '../../shared/image-decryption'
|
||||
import { ImageKeyStore } from '../image-key-store'
|
||||
import * as chat from './chat-service'
|
||||
import { loadSettings, saveSettings, type AppSettings } from './settings-store'
|
||||
|
||||
export class ImageKeyConfigService {
|
||||
constructor(private readonly store = new ImageKeyStore()) {}
|
||||
|
||||
getConfig(): ImageKeyConfigResult {
|
||||
const settings = loadSettings()
|
||||
const context = this.getContext(settings)
|
||||
const stored = this.store.get(context.accountId)
|
||||
if (!stored.success) {
|
||||
return {
|
||||
success: false,
|
||||
configured: false,
|
||||
saved: false,
|
||||
encryptionAvailable: stored.encryptionAvailable,
|
||||
source: 'none',
|
||||
accountId: context.accountId,
|
||||
resourceRoot: context.resourceRoot,
|
||||
error: stored.error
|
||||
}
|
||||
}
|
||||
if (stored.entry) {
|
||||
return {
|
||||
success: true,
|
||||
configured: true,
|
||||
saved: true,
|
||||
encryptionAvailable: stored.encryptionAvailable,
|
||||
source: 'secure-storage',
|
||||
accountId: context.accountId,
|
||||
resourceRoot: context.resourceRoot,
|
||||
xorKey: stored.entry.xorKey,
|
||||
aesKey: stored.entry.aesKey,
|
||||
updatedAt: stored.entry.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.imageAesKey.trim()) {
|
||||
const legacy = this.buildResult({
|
||||
source: 'legacy-settings',
|
||||
xorKey: settings.imageXorKey || '0x40',
|
||||
aesKey: settings.imageAesKey,
|
||||
context,
|
||||
encryptionAvailable: stored.encryptionAvailable
|
||||
})
|
||||
if (!stored.encryptionAvailable) return legacy
|
||||
const migrated = this.save({
|
||||
resourceRoot: context.resourceRoot,
|
||||
xorKey: legacy.xorKey || '0x40',
|
||||
aesKey: legacy.aesKey || ''
|
||||
})
|
||||
return migrated.success ? this.getConfig() : legacy
|
||||
}
|
||||
|
||||
const envAesKey = String(import.meta.env.VITE_IMAGE_AES_KEY || '').trim()
|
||||
if (!settings.imageKeyFallbackDisabled && envAesKey) {
|
||||
return this.buildResult({
|
||||
source: 'environment',
|
||||
xorKey: String(import.meta.env.VITE_IMAGE_XOR_KEY || '0x40'),
|
||||
aesKey: envAesKey,
|
||||
context,
|
||||
encryptionAvailable: stored.encryptionAvailable
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
configured: false,
|
||||
saved: false,
|
||||
encryptionAvailable: stored.encryptionAvailable,
|
||||
source: 'none',
|
||||
accountId: context.accountId,
|
||||
resourceRoot: context.resourceRoot
|
||||
}
|
||||
}
|
||||
|
||||
save(request: SaveImageKeyRequest): ImageKeyConfigResult {
|
||||
const normalized = validateImageKeyRequest(request)
|
||||
if (!normalized.success) return { ...this.getEmptyConfig(), error: normalized.error }
|
||||
const settings = loadSettings()
|
||||
const context = this.getContext(settings)
|
||||
if (!chat.getSelfAccountInfo()?.wxid) {
|
||||
return { ...this.getEmptyConfig(), error: '当前微信账号尚未识别' }
|
||||
}
|
||||
const result = this.store.save(context.accountId, {
|
||||
xorKey: normalized.xorKey,
|
||||
aesKey: normalized.aesKey
|
||||
})
|
||||
if (!result.success || !result.entry) {
|
||||
return { ...this.getEmptyConfig(), error: result.error || '图片密钥保存失败' }
|
||||
}
|
||||
saveSettings({
|
||||
...settings,
|
||||
imageKeyRoot: normalized.resourceRoot,
|
||||
imageXorKey: '',
|
||||
imageAesKey: '',
|
||||
imageKeyFallbackDisabled: false
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
configured: true,
|
||||
saved: true,
|
||||
encryptionAvailable: true,
|
||||
source: 'secure-storage',
|
||||
accountId: context.accountId,
|
||||
resourceRoot: normalized.resourceRoot,
|
||||
xorKey: result.entry.xorKey,
|
||||
aesKey: result.entry.aesKey,
|
||||
updatedAt: result.entry.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
clear(): { success: boolean; error?: string } {
|
||||
const settings = loadSettings()
|
||||
const context = this.getContext(settings)
|
||||
const cleared = this.store.clear(context.accountId)
|
||||
if (!cleared.success) return cleared
|
||||
saveSettings({
|
||||
...settings,
|
||||
imageXorKey: '',
|
||||
imageAesKey: '',
|
||||
imageKeyFallbackDisabled: true
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
getLegacySettingsView(): AppSettings {
|
||||
const settings = loadSettings()
|
||||
const config = this.getConfig()
|
||||
return {
|
||||
...settings,
|
||||
imageXorKey: config.xorKey || '',
|
||||
imageAesKey: config.aesKey || ''
|
||||
}
|
||||
}
|
||||
|
||||
private getContext(settings: AppSettings): { accountId: string; resourceRoot: string } {
|
||||
const self = chat.getSelfAccountInfo()
|
||||
const accountRoot = self?.accountRoot || chat.getCurrentAccountRoot() || settings.dbRoot
|
||||
return {
|
||||
accountId: self?.wxid || path.basename(accountRoot || '') || 'unbound',
|
||||
resourceRoot: settings.imageKeyRoot || accountRoot || settings.dbRoot
|
||||
}
|
||||
}
|
||||
|
||||
private buildResult(input: {
|
||||
source: 'legacy-settings' | 'environment'
|
||||
xorKey: string
|
||||
aesKey: string
|
||||
context: { accountId: string; resourceRoot: string }
|
||||
encryptionAvailable: boolean
|
||||
}): ImageKeyConfigResult {
|
||||
return {
|
||||
success: true,
|
||||
configured: true,
|
||||
saved: false,
|
||||
encryptionAvailable: input.encryptionAvailable,
|
||||
source: input.source,
|
||||
accountId: input.context.accountId,
|
||||
resourceRoot: input.context.resourceRoot,
|
||||
xorKey: normalizeImageXorKey(input.xorKey),
|
||||
aesKey: input.aesKey.trim()
|
||||
}
|
||||
}
|
||||
|
||||
private getEmptyConfig(): ImageKeyConfigResult {
|
||||
const settings = loadSettings()
|
||||
const context = this.getContext(settings)
|
||||
const stored = this.store.get(context.accountId)
|
||||
return {
|
||||
success: false,
|
||||
configured: false,
|
||||
saved: false,
|
||||
encryptionAvailable: stored.encryptionAvailable,
|
||||
source: 'none',
|
||||
accountId: context.accountId,
|
||||
resourceRoot: context.resourceRoot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeImageXorKey(value: unknown): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return '0x40'
|
||||
const parsed = raw.toLowerCase().startsWith('0x')
|
||||
? Number.parseInt(raw.slice(2), 16)
|
||||
: Number.parseInt(raw, 10)
|
||||
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 255) return raw
|
||||
return `0x${parsed.toString(16).toUpperCase().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function validateImageKeyRequest(
|
||||
request: SaveImageKeyRequest
|
||||
):
|
||||
| { success: true; resourceRoot: string; xorKey: string; aesKey: string }
|
||||
| { success: false; error: string } {
|
||||
const resourceRoot = request.resourceRoot.trim()
|
||||
const xorKey = normalizeImageXorKey(request.xorKey)
|
||||
const aesKey = request.aesKey.trim()
|
||||
if (!resourceRoot) return { success: false, error: '图片资源目录不能为空' }
|
||||
if (!/^0x[0-9A-F]{2}$/.test(xorKey)) return { success: false, error: 'XOR Key 格式不正确' }
|
||||
if (aesKey.length !== 16) return { success: false, error: 'AES Key 必须为 16 个字符' }
|
||||
return { success: true, resourceRoot, xorKey, aesKey }
|
||||
}
|
||||
Reference in New Issue
Block a user