fix(crypto): request persistent storage on client init (KE-1 mitigation)
The IndexedDB crypto store is evictable while the localStorage session survives, so the browser can drop it out from under a live login -> the device resurrects with a blank key store and re-uploads a one-time key at an id Synapse already holds -> a permanent '400 M_UNKNOWN: One time key ... already exists' upload storm (and undecryptable to-device/media keys downstream). initClient now calls navigator.storage.persist() before creating the crypto store, so the origin's storage is marked persistent and won't be evicted. Best-effort (granted by engagement/PWA-install, no prompt; denial is non-fatal). Preventive only -- an already-diverged device still needs a clean re-login. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,33 @@ import { deleteSearchCacheDatabase } from '../app/utils/searchCache';
|
|||||||
// This happens after a downgrade (e.g. matrix-js-sdk was briefly upgraded and then reverted).
|
// This happens after a downgrade (e.g. matrix-js-sdk was briefly upgraded and then reverted).
|
||||||
export const IDB_VERSION_CONFLICT = 'IDB_VERSION_CONFLICT';
|
export const IDB_VERSION_CONFLICT = 'IDB_VERSION_CONFLICT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KE-1 mitigation. Ask the browser to make this origin's storage persistent so the
|
||||||
|
* IndexedDB **crypto store** isn't evicted from under a surviving `localStorage`
|
||||||
|
* session. When that happens the device "resurrects" with a blank key store and the
|
||||||
|
* client re-uploads a one-time key at an id Synapse already holds → a permanent
|
||||||
|
* `400 M_UNKNOWN: … already exists` upload-conflict storm (and, downstream,
|
||||||
|
* undecryptable to-device/media keys). `persist()` grants based on engagement / PWA
|
||||||
|
* install and shows no prompt; denial/absence is non-fatal.
|
||||||
|
*/
|
||||||
|
export const requestPersistentStorage = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
if (!navigator.storage?.persist) return false;
|
||||||
|
if (await navigator.storage.persisted()) return true;
|
||||||
|
const granted = await navigator.storage.persist();
|
||||||
|
if (!granted) {
|
||||||
|
console.warn('Persistent storage not granted — the crypto store remains evictable (KE-1).');
|
||||||
|
}
|
||||||
|
return granted;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||||
|
// Protect the crypto store from eviction before we create and write to it.
|
||||||
|
await requestPersistentStorage();
|
||||||
|
|
||||||
const indexedDBStore = new IndexedDBStore({
|
const indexedDBStore = new IndexedDBStore({
|
||||||
indexedDB: globalThis.indexedDB,
|
indexedDB: globalThis.indexedDB,
|
||||||
localStorage: globalThis.localStorage,
|
localStorage: globalThis.localStorage,
|
||||||
|
|||||||
Reference in New Issue
Block a user