Migrate from electron-store to OS-level secure storage (getSecureStorage)

- Replace `electron-store` with OS-level encrypted storage for secure token, userId, and language management in `LocalSystem` and `keyManager`.
- Add `init-user` IPC handler to initialize user data and manage encryption keys.
- Update login process to handle encrypted storage saving with fallback for macOS issues.
- Add offline warning component to `login/page.tsx` to handle first-time sync requirements.
- Remove `electron-store` and associated dependencies from `package.json` and `package-lock.json`.
This commit is contained in:
natreex
2025-11-19 19:10:12 -05:00
parent 71d13e2b12
commit dde4683c38
11 changed files with 287 additions and 361 deletions

View File

@@ -1,33 +1,26 @@
import type { IpcMainInvokeEvent } from 'electron';
import Store from 'electron-store';
import { getSecureStorage } from '../storage/SecureStorage.js';
// ============================================================
// SESSION MANAGEMENT - Auto-inject userId and lang
// ============================================================
/**
* Electron store instance for session management
* - userId: Set during login via 'login-success' event
* - userLang: Set via 'set-lang' handler
*/
const store = new Store({
encryptionKey: 'eritors-scribe-secure-key'
});
/**
* Get userId from electron-store
* Get userId from secure storage (OS-encrypted)
* Set during login via 'login-success' event
*/
function getUserIdFromSession(): string | null {
return store.get('userId', null) as string | null;
const storage = getSecureStorage();
return storage.get<string>('userId', null);
}
/**
* Get lang from electron-store
* Get lang from secure storage
* Set via 'set-lang' handler, defaults to 'fr'
*/
function getLangFromSession(): 'fr' | 'en' {
return store.get('userLang', 'fr') as 'fr' | 'en';
const storage = getSecureStorage();
return storage.get<'fr' | 'en'>('userLang', 'fr') as 'fr' | 'en';
}
// ============================================================

View File

@@ -1,33 +1,35 @@
import Store from 'electron-store';
import { getSecureStorage } from '../storage/SecureStorage.js';
/**
* Key Manager - Manages user encryption keys stored in electron-store
* Key Manager - Manages user encryption keys using OS-level secure storage
* - macOS: Keychain
* - Windows: DPAPI
* - Linux: gnome-libsecret/kwallet
*/
const store = new Store({
encryptionKey: 'eritors-scribe-secure-key'
});
/**
* Get user encryption key from secure store
* Get user encryption key from secure storage
* @param userId - User ID
* @returns User's encryption key or null if not found
* @returns User's encryption key
* @throws Error if encryption key not found
*/
export function getUserEncryptionKey(userId: string): string {
const key: string | undefined = store.get(`encryptionKey-${userId}`) as string | undefined;
if (key === undefined) {
const storage = getSecureStorage();
const key = storage.get<string>(`encryptionKey-${userId}`);
if (key === null || key === undefined) {
throw new Error(`Unknown encryptionKey`);
}
return key;
}
/**
* Set user encryption key in secure store
* Set user encryption key in secure storage (OS-encrypted)
* @param userId - User ID
* @param encryptionKey - Encryption key to store
*/
export function setUserEncryptionKey(userId: string, encryptionKey: string): void {
store.set(`encryptionKey-${userId}`, encryptionKey);
const storage = getSecureStorage();
storage.set(`encryptionKey-${userId}`, encryptionKey);
}
/**
@@ -36,7 +38,8 @@ export function setUserEncryptionKey(userId: string, encryptionKey: string): voi
* @returns True if key exists
*/
export function hasUserEncryptionKey(userId: string): boolean {
return store.has(`encryptionKey-${userId}`);
const storage = getSecureStorage();
return storage.has(`encryptionKey-${userId}`);
}
/**
@@ -44,5 +47,6 @@ export function hasUserEncryptionKey(userId: string): boolean {
* @param userId - User ID
*/
export function deleteUserEncryptionKey(userId: string): void {
store.delete(`encryptionKey-${userId}`);
const storage = getSecureStorage();
storage.delete(`encryptionKey-${userId}`);
}

View File

@@ -1,10 +1,10 @@
import { app, BrowserWindow, ipcMain, nativeImage, protocol } from 'electron';
import { app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage } from 'electron';
import * as path from 'path';
import * as url from 'url';
import { fileURLToPath } from 'url';
import Store from 'electron-store';
import * as fs from 'fs';
import { getDatabaseService } from './database/database.service.js';
import { getSecureStorage } from './storage/SecureStorage.js';
// Import IPC handlers
import './ipc/book.ipc.js';
@@ -47,11 +47,6 @@ const iconPath = isDev
? path.join(process.resourcesPath, 'icon.icns') // macOS utilise .icns
: path.join(process.resourcesPath, 'app.asar/build/icon.png'); // Windows/Linux utilisent .png
// Store sécurisé pour le token
const store = new Store({
encryptionKey: 'eritors-scribe-secure-key' // En production, utiliser une clé générée
});
let mainWindow: BrowserWindow | null = null;
let loginWindow: BrowserWindow | null = null;
@@ -121,53 +116,169 @@ function createMainWindow(): void {
});
}
// IPC Handlers pour la gestion du token
// IPC Handlers pour la gestion du token (OS-encrypted storage)
ipcMain.handle('get-token', () => {
return store.get('authToken', null);
const storage = getSecureStorage();
return storage.get('authToken', null);
});
ipcMain.handle('set-token', (_event, token: string) => {
store.set('authToken', token);
const storage = getSecureStorage();
storage.set('authToken', token);
return true;
});
ipcMain.handle('remove-token', () => {
store.delete('authToken');
const storage = getSecureStorage();
storage.delete('authToken');
return true;
});
// IPC Handlers pour la gestion de la langue
ipcMain.handle('get-lang', () => {
return store.get('userLang', 'fr');
const storage = getSecureStorage();
return storage.get('userLang', 'fr');
});
ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => {
store.set('userLang', lang);
const storage = getSecureStorage();
storage.set('userLang', lang);
return true;
});
ipcMain.on('login-success', (_event, token: string, userId: string) => {
store.set('authToken', token);
store.set('userId', userId);
// IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur
ipcMain.handle('init-user', async (_event, userId: string) => {
console.log('[InitUser] Initializing user:', userId);
const storage = getSecureStorage();
storage.set('userId', userId);
try {
const { getUserEncryptionKey, setUserEncryptionKey, hasUserEncryptionKey } = await import('./database/keyManager.js');
let encryptionKey: string | null = null;
if (!hasUserEncryptionKey(userId)) {
const { generateUserEncryptionKey } = await import('./database/encryption.js');
encryptionKey = generateUserEncryptionKey(userId);
console.log('[InitUser] Generated new encryption key for user');
console.log('[InitUser] Key generated:', encryptionKey ? `${encryptionKey.substring(0, 10)}...` : 'UNDEFINED');
if (!encryptionKey) {
console.error('[InitUser] CRITICAL: Generated key is undefined, blocking operation');
throw new Error('Failed to generate encryption key');
}
setUserEncryptionKey(userId, encryptionKey);
// Verify the key was saved
const savedKey = getUserEncryptionKey(userId);
console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED');
if (!savedKey) {
console.error('[InitUser] CRITICAL: Key was not saved correctly, blocking operation');
throw new Error('Failed to save encryption key');
}
} else {
encryptionKey = getUserEncryptionKey(userId);
console.log('[InitUser] Using existing encryption key:', encryptionKey ? `${encryptionKey.substring(0, 10)}...` : 'UNDEFINED');
if (!encryptionKey) {
console.error('[InitUser] CRITICAL: Existing key is undefined, regenerating');
const { generateUserEncryptionKey } = await import('./database/encryption.js');
encryptionKey = generateUserEncryptionKey(userId);
setUserEncryptionKey(userId, encryptionKey);
}
}
// Save userId to disk now that we have everything
// This is the ONLY additional save after login
if (safeStorage.isEncryptionAvailable()) {
storage.save();
console.log('[InitUser] User ID saved to disk (encrypted)');
} else {
console.error('[InitUser] WARNING: Cannot save user ID - encryption not available');
}
return { success: true, keyCreated: !hasUserEncryptionKey(userId) };
} catch (error) {
console.error('[InitUser] Error managing encryption key:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
});
ipcMain.on('login-success', async (_event, token: string) => {
const storage = getSecureStorage();
storage.set('authToken', token);
// Note: userId will be set later when we get user info from server
if (loginWindow) {
loginWindow.close();
}
createMainWindow();
// Save AFTER mainWindow is created (fixes macOS safeStorage issue)
setTimeout(async () => {
try {
if (safeStorage.isEncryptionAvailable()) {
storage.save();
console.log('[Login] Auth token saved to disk (encrypted)');
} else {
console.error('[Login] Encryption still not available after window creation');
// Try one more time after another delay
setTimeout(() => {
if (safeStorage.isEncryptionAvailable()) {
storage.save();
console.log('[Login] Auth token saved to disk (encrypted) - second attempt');
} else {
console.error('[Login] CRITICAL: Cannot encrypt credentials');
}
}, 1000);
}
} catch (error) {
console.error('[Login] Error saving auth data:', error);
}
}, 500);
});
ipcMain.on('logout', () => {
store.delete('authToken');
store.delete('userId');
store.delete('userLang');
try {
const storage = getSecureStorage();
// Close database connection
const db = getDatabaseService();
db.close();
// Debug: Check what's in storage before deletion
console.log('[Logout] Before deletion - authToken exists:', storage.has('authToken'));
console.log('[Logout] Before deletion - userId exists:', storage.has('userId'));
storage.delete('authToken');
storage.delete('userId');
storage.delete('userLang');
// Debug: Check what's in storage after deletion
console.log('[Logout] After deletion - authToken exists:', storage.has('authToken'));
console.log('[Logout] After deletion - userId exists:', storage.has('userId'));
// IMPORTANT: Save to disk to persist the deletions
storage.save();
console.log('[Logout] Cleared auth data from disk');
} catch (error) {
console.error('[Logout] Error clearing storage:', error);
}
try {
const db = getDatabaseService();
db.close();
} catch (error) {
console.error('[Logout] Error closing database:', error);
}
if (mainWindow) {
mainWindow.close();
mainWindow = null;
}
createLoginWindow();
@@ -240,18 +351,20 @@ ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
});
/**
* Get or generate user encryption key
* Get or generate user encryption key (OS-encrypted storage)
*/
ipcMain.handle('get-user-encryption-key', (_event, userId: string) => {
const key = store.get(`encryptionKey-${userId}`, null);
const storage = getSecureStorage();
const key = storage.get(`encryptionKey-${userId}`, null);
return key;
});
/**
* Store user encryption key
* Store user encryption key (OS-encrypted storage)
*/
ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => {
store.set(`encryptionKey-${userId}`, encryptionKey);
const storage = getSecureStorage();
storage.set(`encryptionKey-${userId}`, encryptionKey);
return true;
});
@@ -324,9 +437,15 @@ app.whenReady().then(() => {
app.dock.setIcon(icon);
}
// Vérifier si un token existe
const token = store.get('authToken');
console.log('Token exists:', !!token);
// Vérifier si un token existe (OS-encrypted storage)
const storage = getSecureStorage();
const token = storage.get('authToken');
const userId = storage.get('userId');
console.log('[Startup] Token exists:', !!token);
console.log('[Startup] UserId exists:', !!userId);
if (token) {
console.log('[Startup] Token value:', token.substring(0, 20) + '...');
}
if (token) {
// Token existe, ouvrir la fenêtre principale
@@ -338,7 +457,8 @@ app.whenReady().then(() => {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
const token = store.get('authToken');
const storage = getSecureStorage();
const token = storage.get('authToken');
if (token) {
createMainWindow();
} else {

View File

@@ -21,9 +21,12 @@ contextBridge.exposeInMainWorld('electron', {
setLang: (lang: 'fr' | 'en') => ipcRenderer.invoke('set-lang', lang),
// Auth events (use send for one-way communication)
loginSuccess: (token: string, userId: string) => ipcRenderer.send('login-success', token, userId),
loginSuccess: (token: string) => ipcRenderer.send('login-success', token),
logout: () => ipcRenderer.send('logout'),
// User initialization (after getting user info from server)
initUser: (userId: string) => ipcRenderer.invoke('init-user', userId),
// Encryption key management (shortcuts for convenience)
generateEncryptionKey: (userId: string) => ipcRenderer.invoke('generate-encryption-key', userId),
getUserEncryptionKey: (userId: string) => ipcRenderer.invoke('get-user-encryption-key', userId),