import { join } from 'path' import { app, safeStorage } from 'electron' import Store from 'electron-store' // safeStorage 加密后的前缀标记,用于区分明文和密文 const SAFE_PREFIX = 'safe:' interface ConfigSchema { // 数据库相关 dbPath: string // 数据库根目录 (xwechat_files) decryptKey: string // 解密密钥 myWxid: string // 当前用户 wxid onboardingDone: boolean imageXorKey: number imageAesKey: string wxidConfigs: Record // 缓存相关 cachePath: string lastOpenedDb: string lastSession: string // 界面相关 theme: 'light' | 'dark' | 'system' themeId: string language: string logEnabled: boolean llmModelPath: string whisperModelName: string whisperModelDir: string whisperDownloadSource: string autoTranscribeVoice: boolean transcribeLanguages: string[] exportDefaultConcurrency: number analyticsExcludedUsernames: string[] // 安全相关(通过 safeStorage 加密存储,JSON 中为密文) authEnabled: boolean authPassword: string // SHA-256 hash authUseHello: boolean // 更新相关 ignoredUpdateVersion: string // 通知 notificationEnabled: boolean notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' notificationFilterMode: 'all' | 'whitelist' | 'blacklist' notificationFilterList: string[] wordCloudExcludeWords: string[] } // 需要 safeStorage 加密的字段集合 const ENCRYPTED_STRING_KEYS: Set = new Set(['decryptKey', 'imageAesKey', 'authPassword']) const ENCRYPTED_BOOL_KEYS: Set = new Set(['authEnabled', 'authUseHello']) const ENCRYPTED_NUMBER_KEYS: Set = new Set(['imageXorKey']) export class ConfigService { private static instance: ConfigService private store!: Store static getInstance(): ConfigService { if (!ConfigService.instance) { ConfigService.instance = new ConfigService() } return ConfigService.instance } constructor() { if (ConfigService.instance) { return ConfigService.instance } ConfigService.instance = this this.store = new Store({ name: 'WeFlow-config', defaults: { dbPath: '', decryptKey: '', myWxid: '', onboardingDone: false, imageXorKey: 0, imageAesKey: '', wxidConfigs: {}, cachePath: '', lastOpenedDb: '', lastSession: '', theme: 'system', themeId: 'cloud-dancer', language: 'zh-CN', logEnabled: false, llmModelPath: '', whisperModelName: 'base', whisperModelDir: '', whisperDownloadSource: 'tsinghua', autoTranscribeVoice: false, transcribeLanguages: ['zh'], exportDefaultConcurrency: 2, analyticsExcludedUsernames: [], authEnabled: false, authPassword: '', authUseHello: false, ignoredUpdateVersion: '', notificationEnabled: true, notificationPosition: 'top-right', notificationFilterMode: 'all', notificationFilterList: [], wordCloudExcludeWords: [] } }) // 首次启动时迁移旧版明文安全字段 this.migrateAuthFields() } get(key: K): ConfigSchema[K] { const raw = this.store.get(key) // 布尔型加密字段:存储为加密字符串,读取时解密还原为布尔值 if (ENCRYPTED_BOOL_KEYS.has(key)) { const str = typeof raw === 'string' ? raw : '' if (!str || !str.startsWith(SAFE_PREFIX)) return raw const decrypted = this.safeDecrypt(str) return (decrypted === 'true') as ConfigSchema[K] } // 数字型加密字段:存储为加密字符串,读取时解密还原为数字 if (ENCRYPTED_NUMBER_KEYS.has(key)) { const str = typeof raw === 'string' ? raw : '' if (!str || !str.startsWith(SAFE_PREFIX)) return raw const decrypted = this.safeDecrypt(str) const num = Number(decrypted) return (Number.isFinite(num) ? num : 0) as ConfigSchema[K] } // 字符串型加密字段 if (ENCRYPTED_STRING_KEYS.has(key) && typeof raw === 'string') { return this.safeDecrypt(raw) as ConfigSchema[K] } // wxidConfigs 中嵌套的敏感字段 if (key === 'wxidConfigs' && raw && typeof raw === 'object') { return this.decryptWxidConfigs(raw as any) as ConfigSchema[K] } return raw } set(key: K, value: ConfigSchema[K]): void { let toStore = value // 布尔型加密字段:序列化为字符串后加密 if (ENCRYPTED_BOOL_KEYS.has(key)) { toStore = this.safeEncrypt(String(value)) as ConfigSchema[K] } // 数字型加密字段:序列化为字符串后加密 else if (ENCRYPTED_NUMBER_KEYS.has(key)) { toStore = this.safeEncrypt(String(value)) as ConfigSchema[K] } // 字符串型加密字段 else if (ENCRYPTED_STRING_KEYS.has(key) && typeof value === 'string') { toStore = this.safeEncrypt(value) as ConfigSchema[K] } // wxidConfigs 中嵌套的敏感字段 else if (key === 'wxidConfigs' && value && typeof value === 'object') { toStore = this.encryptWxidConfigs(value as any) as ConfigSchema[K] } this.store.set(key, toStore) } // === safeStorage 加解密 === private safeEncrypt(plaintext: string): string { if (!plaintext) return '' if (plaintext.startsWith(SAFE_PREFIX)) return plaintext if (!safeStorage.isEncryptionAvailable()) return plaintext const encrypted = safeStorage.encryptString(plaintext) return SAFE_PREFIX + encrypted.toString('base64') } private safeDecrypt(stored: string): string { if (!stored) return '' if (!stored.startsWith(SAFE_PREFIX)) { return stored } if (!safeStorage.isEncryptionAvailable()) return '' try { const buf = Buffer.from(stored.slice(SAFE_PREFIX.length), 'base64') return safeStorage.decryptString(buf) } catch { return '' } } // === 旧版本迁移 === // 将旧版明文 auth 字段迁移为 safeStorage 加密格式 private migrateAuthFields(): void { if (!safeStorage.isEncryptionAvailable()) return // 迁移字符串型字段(decryptKey, imageAesKey, authPassword) for (const key of ENCRYPTED_STRING_KEYS) { const raw = this.store.get(key as keyof ConfigSchema) if (typeof raw === 'string' && raw && !raw.startsWith(SAFE_PREFIX)) { this.store.set(key as any, this.safeEncrypt(raw)) } } // 迁移布尔型字段(authEnabled, authUseHello) for (const key of ENCRYPTED_BOOL_KEYS) { const raw = this.store.get(key as keyof ConfigSchema) // 如果是原始布尔值(未加密),转为加密字符串 if (typeof raw === 'boolean') { this.store.set(key as any, this.safeEncrypt(String(raw))) } } // 迁移数字型字段(imageXorKey) for (const key of ENCRYPTED_NUMBER_KEYS) { const raw = this.store.get(key as keyof ConfigSchema) // 如果是原始数字值(未加密),转为加密字符串 if (typeof raw === 'number') { this.store.set(key as any, this.safeEncrypt(String(raw))) } } // 迁移 wxidConfigs 中的嵌套敏感字段 const wxidConfigs = this.store.get('wxidConfigs') if (wxidConfigs && typeof wxidConfigs === 'object') { let needsUpdate = false const updated = { ...wxidConfigs } for (const [wxid, cfg] of Object.entries(updated)) { if (cfg.decryptKey && !cfg.decryptKey.startsWith(SAFE_PREFIX)) { updated[wxid] = { ...cfg, decryptKey: this.safeEncrypt(cfg.decryptKey) } needsUpdate = true } if (cfg.imageAesKey && !cfg.imageAesKey.startsWith(SAFE_PREFIX)) { updated[wxid] = { ...updated[wxid], imageAesKey: this.safeEncrypt(cfg.imageAesKey) } needsUpdate = true } if (cfg.imageXorKey !== undefined && typeof cfg.imageXorKey === 'number') { updated[wxid] = { ...updated[wxid], imageXorKey: this.safeEncrypt(String(cfg.imageXorKey)) as any } needsUpdate = true } } if (needsUpdate) { this.store.set('wxidConfigs', updated) } } // 清理旧版 authSignature 字段(不再需要) this.store.delete('authSignature' as any) } // === wxidConfigs 加解密 === private encryptWxidConfigs(configs: ConfigSchema['wxidConfigs']): ConfigSchema['wxidConfigs'] { const result: ConfigSchema['wxidConfigs'] = {} for (const [wxid, cfg] of Object.entries(configs)) { result[wxid] = { ...cfg } if (cfg.decryptKey) result[wxid].decryptKey = this.safeEncrypt(cfg.decryptKey) if (cfg.imageAesKey) result[wxid].imageAesKey = this.safeEncrypt(cfg.imageAesKey) if (cfg.imageXorKey !== undefined) { (result[wxid] as any).imageXorKey = this.safeEncrypt(String(cfg.imageXorKey)) } } return result } private decryptWxidConfigs(configs: ConfigSchema['wxidConfigs']): ConfigSchema['wxidConfigs'] { const result: ConfigSchema['wxidConfigs'] = {} for (const [wxid, cfg] of Object.entries(configs)) { result[wxid] = { ...cfg } if (cfg.decryptKey) result[wxid].decryptKey = this.safeDecrypt(cfg.decryptKey) if (cfg.imageAesKey) result[wxid].imageAesKey = this.safeDecrypt(cfg.imageAesKey) if (cfg.imageXorKey !== undefined) { const raw = cfg.imageXorKey as any if (typeof raw === 'string' && raw.startsWith(SAFE_PREFIX)) { const decrypted = this.safeDecrypt(raw) const num = Number(decrypted) result[wxid].imageXorKey = Number.isFinite(num) ? num : 0 } } } return result } // === 应用锁验证 === // 验证应用锁状态,防篡改: // - 所有 auth 字段都是 safeStorage 密文,删除/修改密文 → 解密失败 // - 解密失败时,检查 authPassword 密文是否曾经存在(非空非默认值) // 如果存在则说明被篡改,强制锁定 verifyAuthEnabled(): boolean { // 用 as any 绕过泛型推断,因为加密后实际存储的是字符串而非 boolean const rawEnabled: any = this.store.get('authEnabled') const rawPassword: any = this.store.get('authPassword') // 情况1:字段是加密密文,正常解密 if (typeof rawEnabled === 'string' && rawEnabled.startsWith(SAFE_PREFIX)) { const enabled = this.safeDecrypt(rawEnabled) === 'true' const password = typeof rawPassword === 'string' ? this.safeDecrypt(rawPassword) : '' if (!enabled && !password) return false return enabled } // 情况2:字段是原始布尔值(旧版本,尚未迁移) if (typeof rawEnabled === 'boolean') { return rawEnabled } // 情况3:字段被删除(electron-store 返回默认值 false)或被篡改为无法解密的值 // 检查 authPassword 是否有密文残留(说明之前设置过密码) if (typeof rawPassword === 'string' && rawPassword.startsWith(SAFE_PREFIX)) { // 密码密文还在,说明之前启用过应用锁,字段被篡改了 → 强制锁定 return true } return false } // === 其他 === getCacheBasePath(): string { const configured = this.get('cachePath') if (configured && configured.trim().length > 0) { return configured } return join(app.getPath('documents'), 'WeFlow') } getAll(): ConfigSchema { return this.store.store } clear(): void { this.store.clear() } }