fix(privacy): wipe plaintext/PII localStorage caches on logout (SEC-1/2)

Several localStorage caches held decrypted message content or user PII and
survived a normal logout, leaving residue on a shared device (the search
index was already wiped; these were not):

- cinny_scheduled_messages_v1 - decrypted IContent.body of pending sends
- cinny_recent_searches_v1     - search query text
- cinny_recent_forward_targets_v1 - recent forward contact/room graph
- cinny_recent_gifs_v1 / cinny_recent_stickers_v1 - media the user sent
- navToActivePath<userId>       - per-space last-visited room paths
- (plus the translation cache added earlier)

Add a clear function per module and a single auditable clearPlaintextCaches()
aggregator, called from both logout paths (logoutClient + the server-forced
SessionLoggedOut handler) alongside the existing session/search-index wipes.
Unit-tested.

Deliberately NOT cleared (documented in the aggregator): unsent composer
drafts and the presence status message (preserved by product decision N98);
SDK sync/crypto store + io.lotus.* account data (reminders/bookmarks/notes),
already wiped by mx.clearStores(); low-sensitivity UI/metadata residue.

The forward-targets/gifs/stickers/nav-path additions and the accurate
"not covered" documentation address findings from two review passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 16:56:08 -04:00
co-authored by Claude Opus 4.8
parent 7c28ba58b2
commit 726cefb5ab
9 changed files with 187 additions and 7 deletions
+5 -4
View File
@@ -32,7 +32,7 @@ import {
startClient,
} from '../../../client/initMatrix';
import { deleteSearchCacheDatabase } from '../../utils/searchCache';
import { clearTranslationCache } from '../../state/translation';
import { clearPlaintextCaches } from '../../state/plaintextCaches';
import { SplashScreen } from '../../components/splash-screen';
import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
@@ -163,9 +163,10 @@ const useLogoutListener = (mx?: MatrixClient) => {
// change) — the manual logout path already does, but this path didn't, so
// the plaintext survived on disk (and persist() makes it non-evictable).
await deleteSearchCacheDatabase();
// The message-translation cache also holds decrypted plaintext — wipe it
// on server-forced logout too.
clearTranslationCache();
// Other localStorage caches also hold decrypted plaintext / PII
// (translation, scheduled messages, recent searches/forwards/gifs/
// stickers, nav paths) — wipe them on server-forced logout too.
clearPlaintextCaches(mx?.getUserId() ?? undefined);
// Remove only the session credential keys — NOT settings, drafts, and
// other preferences (N98). The SDK's IndexedDB stores are cleared above;
// window.localStorage.clear() is reserved for the explicit reset path.