import { safeStorage, app } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; /** * SecureStorage - Replacement for electron-store using Electron's safeStorage API * * Uses OS-level encryption: * - macOS: Keychain * - Windows: DPAPI (Data Protection API) * - Linux: gnome-libsecret/kwallet * * Security notes: * - Protects against physical theft (when PC is off) * - Protects against other users on same machine * - Does NOT protect against malware running under same user * - On Linux, check getStorageBackend() - if 'basic_text', encryption is weak */ type StorageValue = string; type StoredData = Record; class SecureStorage { private readonly storePath: string; private readonly cache: Map = new Map(); private isLoaded: boolean = false; constructor() { const userDataPath: string = app.getPath('userData'); this.storePath = path.join(userDataPath, 'secure-config.json'); } /** * Ensure data is loaded from disk (lazy loading) */ private ensureLoaded(): void { if (!this.isLoaded) { this.loadFromDisk(); this.isLoaded = true; } } /** * Load encrypted data from disk into memory cache */ private loadFromDisk(): void { try { if (!fs.existsSync(this.storePath)) { return; } const fileData: string = fs.readFileSync(this.storePath, 'utf-8'); const parsed: unknown = JSON.parse(fileData); if (typeof parsed !== 'object' || parsed === null) { console.error('[SecureStorage] Invalid data format in storage file'); return; } for (const [key, unknownValue] of Object.entries(parsed)) { if (typeof unknownValue !== 'string' || unknownValue.length === 0) { continue; } const storedValue: string = unknownValue; try { if (storedValue.startsWith('encrypted:')) { const encryptedBase64: string = storedValue.substring('encrypted:'.length); const buffer: Buffer = Buffer.from(encryptedBase64, 'base64'); const decrypted: string = safeStorage.decryptString(buffer); this.cache.set(key, decrypted); } else if (storedValue.startsWith('plain:')) { const plainValue: string = storedValue.substring('plain:'.length); this.cache.set(key, plainValue); } else { try { const buffer: Buffer = Buffer.from(storedValue, 'base64'); const decrypted: string = safeStorage.decryptString(buffer); this.cache.set(key, decrypted); } catch (decryptError: unknown) { this.cache.set(key, storedValue); } } } catch (error: unknown) { const errorMessage: string = error instanceof Error ? error.message : 'Unknown error'; console.error(`[SecureStorage] Failed to load key '${key}': ${errorMessage}`); } } } catch (error: unknown) { const errorMessage: string = error instanceof Error ? error.message : 'Unknown error'; console.error(`[SecureStorage] Failed to load from disk: ${errorMessage}`); } } /** * Save encrypted data from memory cache to disk */ private saveToDisk(): void { if (!safeStorage.isEncryptionAvailable()) { throw new Error('Encryption not available - cannot save securely'); } const data: StoredData = {}; for (const [key, value] of this.cache.entries()) { if (!value) { throw new Error(`Invalid value for key '${key}'`); } const buffer: Buffer = safeStorage.encryptString(value); if (!buffer || buffer.length === 0) { throw new Error(`Failed to encrypt key '${key}'`); } data[key] = `encrypted:${buffer.toString('base64')}`; } try { const dir: string = path.dirname(this.storePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(this.storePath, JSON.stringify(data, null, 2), 'utf-8'); } catch (error: unknown) { const errorMessage: string = error instanceof Error ? error.message : 'Unknown error'; console.error(`[SecureStorage] Failed to save to disk: ${errorMessage}`); throw error; } } /** * Get a value from secure storage * @param key - Storage key * @param defaultValue - Default value if key doesn't exist * @returns Stored value or default */ public get(key: string, defaultValue: T | null = null): T | null { this.ensureLoaded(); const value: StorageValue | undefined = this.cache.get(key); if (value === undefined) { return defaultValue; } try { return JSON.parse(value) as T; } catch { return value as unknown as T; } } /** * Set a value in secure storage (kept in memory only) * @param key - Storage key * @param value - Value to store */ public set(key: string, value: unknown): void { this.ensureLoaded(); const stringValue: string = typeof value === 'string' ? value : JSON.stringify(value); this.cache.set(key, stringValue); } /** * Delete a value from secure storage (memory only) * @param key - Storage key */ public delete(key: string): void { this.ensureLoaded(); this.cache.delete(key); } /** * Check if a key exists in secure storage * @param key - Storage key * @returns True if key exists */ public has(key: string): boolean { this.ensureLoaded(); return this.cache.has(key); } /** * Clear all data from secure storage (memory only) */ public clear(): void { this.cache.clear(); } /** * Manually save to disk (encrypted with safeStorage) * Call this when you want to persist data */ public save(): void { this.saveToDisk(); } /** * Check if encryption is available * @returns True if OS-level encryption is available */ public isEncryptionAvailable(): boolean { return safeStorage.isEncryptionAvailable(); } } declare global { var __secureStorageInstance: SecureStorage | undefined; } /** * Get the SecureStorage singleton instance * @returns SecureStorage instance */ export function getSecureStorage(): SecureStorage { if (!global.__secureStorageInstance) { global.__secureStorageInstance = new SecureStorage(); if (!global.__secureStorageInstance.isEncryptionAvailable()) { console.warn( '[SecureStorage] WARNING: OS-level encryption is not available. ' + 'Data will still be stored but with reduced security.' ); } } return global.__secureStorageInstance; } export default SecureStorage;