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:
@@ -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';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
180
electron/main.ts
180
electron/main.ts
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user