fix(secure-store): preserve original ciphertext on decrypt failure + startup path log

Critical hotfix for the 0.17.0 regression: users upgrading from 0.16.x
were logged out, and their next login wrote a fresh empty secure-store
on top of the original ciphertext — destroying device keys irrecoverably.

Why it happened: loadState used a blanket `catch {}` that conflated
"file doesn't exist (genuine new user)" with "file exists but can't be
decrypted (DPAPI / OSCrypt quirk after the install rename)". Both paths
returned an empty Map; the next scheduledSave then overwrote the
original .bin file with a fresh blob.

Fix:
* Separate ENOENT from decrypt/parse failures. ENOENT → empty Map. Any
  other read error → log, empty Map (no quarantine, matches old
  behaviour for transient lock issues).
* When decrypt/parse fails the original file is renamed to
  <file>.broken-<iso-ts> BEFORE returning empty Map. The next save
  writes to a fresh file; the original ciphertext is preserved on disk
  so a future build (or manual recovery) can still get at the bytes.
* Loud console.error around the failure so future regressions surface
  in main-process logs.

main.ts: move setPath('userData', appData/ChatApp) BEFORE setName so
any productName-derived path caching inside setName can't beat us to
it. Add a startup log of the resolved paths so future debugging has
hard evidence instead of guessing.

Affected users on 0.17.0 should still recover via Settings → Backup
Wiederherstellen (account-level keys are unchanged); this fix prevents
the data destruction for anyone who hasn't upgraded yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-12 21:54:14 +02:00
parent 341f5f227d
commit f9e1d2f073
2 changed files with 84 additions and 18 deletions
+28 -10
View File
@@ -37,23 +37,26 @@ const __dirnameSafe = path.dirname(__filenameSafe);
const DEV_URL = 'http://localhost:1420';
const WINDOW_STATE_FILE = 'window-state.json';
// Pin userData FIRST — before any other Electron call that might cache a
// productName-derived path. The 0.17.0 release saw users get logged out
// after upgrading from 0.16.x: the most likely culprit was an internal
// path resolution kicking off the moment `setName('Netralax')` ran, so
// 0.17.1 swaps the order so the explicit override wins regardless of
// what setName triggers internally. The literal 'ChatApp' here is the
// pre-rename product folder — installed users' SQLite, secrets, sounds,
// IndexedDB all live there and we never want to leave them stranded by
// a future rebrand.
app.setPath('userData', path.join(app.getPath('appData'), 'ChatApp'));
// App branding. productName in package.json drives the packaged exe name
// (Netralax.exe) and electron-builder installer title. setName + the
// AppUserModelId below cover the live process: window title fallback,
// Windows taskbar grouping, notification source attribution.
// AppUserModelId cover the live process: window title fallback, Windows
// taskbar grouping, notification source attribution.
app.setName('Netralax');
if (process.platform === 'win32') {
app.setAppUserModelId('cloud.netralax.desktop');
}
// Pin userData to %APPDATA%\ChatApp regardless of productName so existing
// installs keep their profile, sounds, secrets, SQLite. Electron's default
// is %APPDATA%\<productName>, which after the Netralax rename would point
// at an empty fresh dir — same painful migration as the Tauri → Electron
// cut. Anchored to `appData` (the platform-AppData root) so productName
// changes can't drag it.
app.setPath('userData', path.join(app.getPath('appData'), 'ChatApp'));
// Run dev side-by-side with the installed packaged build by isolating the
// renderer profile / secret-store / SQLite / IndexedDB / localStorage in
// a separate userData dir. Without this both share `%APPDATA%\ChatApp`,
@@ -64,6 +67,21 @@ if (!app.isPackaged) {
app.setPath('userData', app.getPath('userData') + '-Dev');
}
// Startup diagnostics — the 0.17.0 logout regression was hard to debug
// because we had no record of the actual resolved paths. With this log
// any future user can paste their main-process output and we can tell
// at a glance whether userData ended up where we intended.
console.log(
'[main] resolved paths',
JSON.stringify({
appName: app.getName(),
appData: app.getPath('appData'),
userData: app.getPath('userData'),
isPackaged: app.isPackaged,
platform: process.platform,
}),
);
let mainWindow: BrowserWindow | null = null;
function resolvePreloadPath(): string {