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 */ class SecureStorage { private storePath: string; private cache: Map = new Map(); private isLoaded: boolean = false; private appReady: boolean = false; constructor() { const userDataPath = app.getPath('userData'); this.storePath = path.join(userDataPath, 'secure-config.json'); // Wait for app to be ready before using safeStorage if (app.isReady()) { this.appReady = true; } else { app.whenReady().then(() => { this.appReady = true; }); } } /** * 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 = fs.readFileSync(this.storePath, 'utf-8'); const parsed = JSON.parse(fileData); // Load all values and store in cache for (const [key, storedValue] of Object.entries(parsed)) { if (typeof storedValue !== 'string' || storedValue.length === 0) { continue; } try { if (storedValue.startsWith('encrypted:')) { // Decrypt encrypted value const encryptedBase64 = storedValue.substring('encrypted:'.length); const buffer = Buffer.from(encryptedBase64, 'base64'); const decrypted = safeStorage.decryptString(buffer); this.cache.set(key, decrypted); } else if (storedValue.startsWith('plain:')) { // Load plain value const plainValue = storedValue.substring('plain:'.length); this.cache.set(key, plainValue); } else { // Legacy format (try to decrypt) try { const buffer = Buffer.from(storedValue, 'base64'); const decrypted = safeStorage.decryptString(buffer); this.cache.set(key, decrypted); } catch { // If decrypt fails, assume it's plain text this.cache.set(key, storedValue); } } } catch (error) { console.error(`[SecureStorage] Failed to load key '${key}':`, error); } } } catch (error) { console.error('[SecureStorage] Failed to load from disk:', error); } } /** * Save encrypted data from memory cache to disk */ private saveToDisk(): void { try { const data: Record = {}; // Check if encryption is available const canEncrypt = safeStorage.isEncryptionAvailable(); for (const [key, value] of this.cache.entries()) { if (canEncrypt && safeStorage.isEncryptionAvailable()) { try { if (value && typeof value === 'string') { const buffer = safeStorage.encryptString(value); if (buffer && buffer.length > 0) { data[key] = `encrypted:${buffer.toString('base64')}`; } else { throw new Error(`Failed to encrypt key '${key}'`); } } else { throw new Error(`Invalid value for key '${key}'`); } } catch (encryptError) { console.error(`[SecureStorage] CRITICAL: Cannot encrypt key '${key}':`, encryptError); throw encryptError; } } else { throw new Error('Encryption not available - cannot save securely'); } } // Ensure directory exists const dir = 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) { console.error('[SecureStorage] Failed to save to disk:', 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 */ get(key: string, defaultValue: T | null = null): T | null { this.ensureLoaded(); const value = this.cache.get(key); if (value === undefined) { return defaultValue; } // Try to parse as JSON for objects/arrays try { return JSON.parse(value) as T; } catch { // Return as-is if not JSON 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 */ set(key: string, value: unknown): void { this.ensureLoaded(); // Convert to string (JSON if object/array) const stringValue = typeof value === 'string' ? value : JSON.stringify(value); this.cache.set(key, stringValue); } /** * Delete a value from secure storage (memory only) * @param key - Storage key */ 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 */ has(key: string): boolean { this.ensureLoaded(); return this.cache.has(key); } /** * Clear all data from secure storage (memory only) */ clear(): void { this.cache.clear(); } /** * Manually save to disk (encrypted with safeStorage) * Call this when you want to persist data */ save(): void { this.saveToDisk(); } /** * Check if encryption is available * @returns True if OS-level encryption is available */ isEncryptionAvailable(): boolean { return safeStorage.isEncryptionAvailable(); } } // Store singleton in global scope to avoid multiple instances with dynamic imports 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(); // Log encryption availability 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;