From 50bfb5b137a04371b5b546d88630fa76a4363328 Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 17:09:01 +0200 Subject: [PATCH] feat(desktop): wipe local crypto + caches on sign-out --- apps/desktop/src/context/AuthContext.tsx | 5 ++- apps/desktop/src/lib/memoryWipe.ts | 54 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/lib/memoryWipe.ts diff --git a/apps/desktop/src/context/AuthContext.tsx b/apps/desktop/src/context/AuthContext.tsx index d290ece..f7ca700 100644 --- a/apps/desktop/src/context/AuthContext.tsx +++ b/apps/desktop/src/context/AuthContext.tsx @@ -20,6 +20,7 @@ import { import { useTranslation } from 'react-i18next'; import { ensureInstallId } from '../lib/installId'; +import { wipeLocalState } from '../lib/memoryWipe'; import { setSecretStoreUser } from '../lib/secretStore'; import { supabase } from '../lib/supabase'; import { cachedUserKey, ensureLegacyMigrated } from '../lib/userIdentity'; @@ -235,11 +236,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, [session, profile, refreshProfile]); const signOut = useCallback(async () => { + const uid = session?.user.id ?? null; await updateOwnProfile(supabase, { presenceState: 'offline' }).catch((err: unknown) => { console.warn('offline update before sign-out failed', err); }); await supabaseSignOut(supabase); - }, []); + await wipeLocalState(uid); + }, [session]); const value = useMemo( () => ({ diff --git a/apps/desktop/src/lib/memoryWipe.ts b/apps/desktop/src/lib/memoryWipe.ts new file mode 100644 index 0000000..40ffd59 --- /dev/null +++ b/apps/desktop/src/lib/memoryWipe.ts @@ -0,0 +1,54 @@ +import { clearConvKeyCache } from '@chat-app/shared/chat'; + +import { devLocalSecretStore } from './secretStore'; + +// Aggressively scrub local crypto + chat caches on sign-out (and on +// app-close if the user opted in). Preserves things that aren't sensitive +// and would be annoying to lose (theme, locale, install-id). +// +// We can't enumerate IndexedDB names without async + the indexedDB API, +// so we list the ones we know about explicitly. Adding a new local store +// later? Append to LOCAL_DBS. + +const LOCAL_DBS = ['soundboard', 'message-cache', 'chatapp-attachments']; +const PRESERVE_LOCAL_STORAGE = new Set([ + 'chatapp.theme', + 'chatapp.locale', + 'chatapp.installId', + 'i18nextLng', +]); + +export async function wipeLocalState(userId: string | null): Promise { + // 1. Per-conversation key cache (in-memory). + try { clearConvKeyCache(); } catch { /* never throws but be defensive */ } + + // 2. Stronghold / secret-store: drop the user-priv blob for this user. + if (userId) { + try { await devLocalSecretStore.removeSecret('chatapp.userpriv.' + userId); } + catch (err) { console.warn('[wipe] userpriv remove failed', err); } + } + + // 3. localStorage — preserve only the explicit whitelist. + try { + const keysToDrop: string[] = []; + for (let i = 0; i < window.localStorage.length; i++) { + const k = window.localStorage.key(i); + if (k && !PRESERVE_LOCAL_STORAGE.has(k)) keysToDrop.push(k); + } + for (const k of keysToDrop) window.localStorage.removeItem(k); + } catch (err) { console.warn('[wipe] localStorage clear failed', err); } + + // 4. sessionStorage — always full. + try { window.sessionStorage.clear(); } catch { /* ignored */ } + + // 5. IndexedDB — delete known databases. Resolves even when blocked so + // we don't hang sign-out forever. + await Promise.allSettled(LOCAL_DBS.map((name) => new Promise((resolve) => { + try { + const req = window.indexedDB.deleteDatabase(name); + req.onsuccess = () => resolve(); + req.onerror = () => resolve(); + req.onblocked = () => resolve(); + } catch { resolve(); } + }))); +}