mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-20 13:06:58 +08:00
208 lines
6.7 KiB
TypeScript
208 lines
6.7 KiB
TypeScript
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 }
|
|
}
|