feat(security): encrypt persisted provider credentials

Protect every remembered provider credential with Electron safeStorage and restore it only inside the main process. Migrate legacy plaintext settings atomically across the primary config and its backup while keeping credentials memory-only when encryption is unavailable or remembering is disabled.

Initialize credential protection after app readiness but before settings are loaded, add masked renderer projection metadata, and cover encryption, migration, unavailable-storage, and persistence behavior with focused tests.
This commit is contained in:
Sucukdeluxe
2026-08-11 21:38:25 +02:00
parent 2861fd95f7
commit 97ad90ad4f
5 changed files with 349 additions and 80 deletions
+16 -11
View File
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { app, BrowserWindow, clipboard, dialog, ipcMain, IpcMainInvokeEvent, Menu, shell, Tray } from "electron";
import { app, BrowserWindow, clipboard, dialog, ipcMain, IpcMainInvokeEvent, Menu, safeStorage, shell, Tray } from "electron";
import { AddLinksPayload, AppSettings, DebridProvider, EnableRemoteDiagnosticsInput, UpdateInstallProgress } from "../shared/types";
import { AppController } from "./app-controller";
import { IPC_CHANNELS } from "../shared/ipc";
@@ -13,6 +13,7 @@ import { cleanupStaleSubstDrives, shutdownDaemon } from "./extractor";
import { revealHistoryEntry } from "./history-reveal";
import { DEV_SERVER_URL } from "./dev-server-url";
import { resolveAppIconPath } from "./app-icon";
import { configureCredentialProtector } from "./credential-protection";
function validateString(value: unknown, name: string): string {
if (typeof value !== "string") {
@@ -73,7 +74,7 @@ let clipboardTimer: ReturnType<typeof setInterval> | null = null;
let updateQuitTimer: ReturnType<typeof setTimeout> | null = null;
let scheduledStartTimer: ReturnType<typeof setTimeout> | null = null;
let lastClipboardText = "";
const controller = new AppController();
let controller: AppController;
const CLIPBOARD_MAX_TEXT_CHARS = 50_000;
function isDevMode(): boolean {
@@ -859,8 +860,10 @@ app.on("second-instance", () => {
}
});
app.whenReady().then(() => {
cleanupStaleSubstDrives();
app.whenReady().then(() => {
configureCredentialProtector(safeStorage);
controller = new AppController();
cleanupStaleSubstDrives();
registerIpcHandlers();
mainWindow = createWindow();
bindMainWindowLifecycle(mainWindow);
@@ -892,10 +895,12 @@ app.on("before-quit", () => {
if (updateQuitTimer) { clearTimeout(updateQuitTimer); updateQuitTimer = null; }
stopClipboardWatcher();
destroyTray();
shutdownDaemon();
try {
controller.shutdown();
} catch (error) {
logger.error(`Fehler beim Shutdown: ${String(error)}`);
}
});
shutdownDaemon();
if (controller) {
try {
controller.shutdown();
} catch (error) {
logger.error(`Fehler beim Shutdown: ${String(error)}`);
}
}
});