Auto-lock is a memory hygiene problem
Scheduled9 min read By NT²
Encryption at rest is incomplete if the decrypted key handle outlives the unlocked session. NT² treats auto-lock as memory hygiene: non-extractable CryptoKeys, in-memory only, cleared on idle, refresh, and tab close.
Auto-lock is a memory hygiene problem
Claim: an unlocked vault is a temporary exception
NT² Vault encrypts structured items on the device. That sentence is true and incomplete.
Ciphertext on disk only protects you when the vault is locked. While it is unlocked, the product holds a live cryptographic handle that can unwrap item keys, decrypt payloads, and—when cloud features are enabled—unwrap signing material used to prove vault identity. That handle is more sensitive than any single password field on screen. If it survives a refresh, a closed tab, or five quiet minutes at a café, “local-first encryption” becomes “a sticky secret in the browser process.”
So the claim is sharp: auto-lock is not a convenience timer. It is memory hygiene.
The vault AES key—and the other runtime keys that depend on an unlocked session—must live as non-extractable CryptoKey objects in in-memory application state. They must not be written to localStorage, IndexedDB, service-worker caches, or URL fragments. A hard refresh must leave them null. Closing the tab must end the session. Roughly five minutes without pointer, keyboard, or touch activity must clear the same state. Unlock is a deliberate exception; lock is the default again.
That sounds obvious until you notice how many web apps treat “remember me” and “session restore” as product virtues. For a password vault, those virtues are attack surface.
Constraint: browsers keep secrets badly, and XSS reads memory
Three constraints force the design.
First, Web Crypto gives you handles, not portable key bytes—unless you ask for extractable keys. The Web Cryptography API lets applications call crypto.subtle.deriveKey or importKey and receive a CryptoKey. If extractable is true, later code can call exportKey and copy raw key material into JavaScript strings or typed arrays. If extractable is false, the browser refuses export. Malicious script that reaches the page can still use the key while it is in memory—XSS remains a serious bug—but it cannot casually dump the AES key as bytes for exfiltration or offline reuse. Non-extractable keys shrink the blast radius. They do not forgive third-party scripts, analytics SDKs, or “just this one debug log.”
Second, anything persisted across navigations outlives the user’s intent to be unlocked. Service workers, disk-backed storage, and “restore previous session” paths are excellent for shopping carts and terrible for vault keys. A refresh is not an unlock ceremony. It is a new document load. If the key reappears without the master password (or an equivalent local unlock path), the product has invented silent re-entry. Attackers who can restart the page—or users who walk away thinking lock means lock—inherit that invention.
Third, idle time is physical exposure time. A vault open on a laptop in a shared office, a phone left face-up, or a laptop suspended without locking the OS still has an unlocked browser context. Crypto does not help if the key handle remains available to whoever touches the trackpad next. An inactivity clear is not paranoia theater. It bounds how long “I stepped away” remains “anyone can read the vault.”
These constraints collide with a familiar SPA instinct: keep expensive derived state warm so the UI feels instant. Deriving or reconstructing a vault key is deliberate work. Caching the result across reloads feels like performance. For NT², that cache would be a security bug with a UX story attached.
Zero-knowledge marketing often stops at “the server never sees the password.” That is necessary and insufficient. The password must never leave the browser in the first place, and the derived key must not become a durable browser secret after unlock. Memory hygiene is the client half of the same promise.
Design: non-extractable keys, Svelte memory, explicit clear paths
NT² Vault’s session model is deliberately narrow.
Unlock runs on the device. The master password is entered locally. Key derivation and vault-key reconstruction use Web Crypto in the page. The resulting vault AES key is created or imported with extractable: false. The handle is stored in global Svelte in-memory state for the unlocked session—alongside the active vault identity needed by the UI—not in durable storage. Decrypted item payloads exist only in component memory while you view or edit them. They are not written back as plaintext.
Lock is the inverse operation: drop the CryptoKey references (set them to null), clear related unwrapped session material, and return the UI to a locked surface. The ciphertext on disk remains ciphertext. Unlock again if you need the exception.
Triggers are boring on purpose:
| Event | Session keys |
|---|---|
| Explicit Lock | Cleared to null |
| ~5 minutes without input | Cleared to null |
| Hard refresh / full navigation away | Gone with the document; state starts empty |
Tab close / beforeunload | Cleared so keys do not outlive the tab |
Activity listeners (pointer, keyboard, click, touch) refresh the idle timer while you work. Silence does not. That is the whole policy: use renews the exception; stillness ends it.
flowchart TD
Locked[Locked: CryptoKey is null]
Unlock[Unlock on device]
Mem[In-memory non-extractable CryptoKey]
Idle[Idle ~5 min]
Refresh[Refresh or tab close]
LockBtn[User locks]
Locked --> Unlock
Unlock --> Mem
Mem --> Idle
Mem --> Refresh
Mem --> LockBtn
Idle --> Locked
Refresh --> Locked
LockBtn --> Locked
Two properties matter as much as the diagram.
Non-extractable by construction. Runtime vault keys are derived or imported so the browser will not export them. We do not keep a parallel “convenient” extractable copy for logging, telemetry, or “temporary” debugging hooks that somehow ship. If a developer needs to inspect ciphertext formats, they work with ciphertext—not with exported AES key bytes.
Refresh means null. There is no silent restore of the vault key from disk after a reload. The local vault profile still holds salt, wrapped material, and verifiers needed to unlock again—that is how local-first unlock works without asking a server—but reconstructing a live CryptoKey requires the unlock path. A reload is not a shortcut around the master password.
This pairs with the rest of the stack without pretending the Worker or OPFS somehow “holds the session key.” SQLite and attachment blobs store ciphertext. The UI thread (and the session state that owns unlock) holds the key handle. When the handle is gone, the Worker can still open files; it cannot turn envelopes into plaintext without a key the application no longer has.
Trade-off: friction at the edge of every pause
Memory hygiene costs comfort.
Users who leave a vault open while they context-switch for six minutes will lock. Users who refresh mid-edit will unlock again. Multi-tab workflows still need an unlocked Writer session in the tab that owns writes; closing that tab ends more than a view. Power users who want “keep unlocked until I quit the browser” will find the product stubborn.
We accept that friction.
What we get back is a session that matches how people actually talk about lock: when the vault is locked, the live key is gone. We do not sell a “remember this device for thirty days” cookie that rehydrates AES material. We do not stash key bytes in OPFS next to the database “so unlock is faster next time.” Faster unlock that skips the password is just unlock without consent.
There is an honesty limit we state plainly. Non-extractable keys and idle clear reduce risk; they do not invent a secure enclave inside every browser. XSS while unlocked can still invoke Web Crypto with the live handle. Physical access while unlocked can still read the screen. JavaScript runtimes do not offer cryptographic erasure guarantees the way a hardware module might. Auto-lock shortens the window. It does not delete the need for a locked OS, careful extensions, and a master password that is not reused from a breach dump.
Teams sometimes try a softer middle: extractable keys “only in memory,” or keys in sessionStorage “cleared on tab close.” sessionStorage is still a string-shaped secret store visible to same-origin script. Extractable keys invite export. Both patterns optimize for convenience and fail the hygiene test the first time someone pastes a key into a log or a support bundle.
What we refuse
Architecture is clearer when the refusals are explicit.
We refuse extractable vault AES keys in the unlocked session. If the browser can export the key, so can anything that shares the page’s JavaScript world.
We refuse persisting live key handles across refresh. Durable storage holds ciphertext and unlock ingredients—not a ready-to-use CryptoKey substitute that reopens the vault silently.
We refuse “keep unlocked forever while the PWA is installed.” Installation does not equal continuous trust. A PWA is still a document context with a memory lifetime.
We refuse third-party scripts in the vault origin that would widen XSS blast radius. Non-extractable keys are one mitigation; shipping analytics and widget CDNs into the unlocked vault is how you defeat it.
We refuse treating auto-lock as optional chrome. Idle clear, lock on leave, and null-after-refresh are session policy, not a settings novelty for people who “prefer security.”
These refusals are the client-side twin of refusing password oracles on the edge. The server must not be able to test guesses; the browser must not keep a decrypted key after the user stops using the vault.
Keys should not outlive the session
A local-first vault still needs ordinary product virtues: unlock that feels intentional, lock that feels final, and a page reload that does not quietly re-enter. Those virtues do not come from wishing the browser were a hardware wallet. They come from non-extractable CryptoKey handles, in-memory session state, and clear paths that set those handles back to null.
That is the same boundary story as keeping the KDF salt on the device, separating local unlock from cloud authentication, declining provider password reset, hosting a heavy vault in a PWA instead of a custodial server, and running SQLite off the UI thread so the session UI stays responsive while storage stays honest. Salt placement, unlock ceremony, and memory hygiene are one architecture—not three optional upgrades.
For where unlock material lives, read the KDF salt stays on your device. For why opening the vault is not “logging in to the cloud,” see unlocking a local vault is not logging in to the cloud. For the recovery honesty that follows when we cannot reset your password, see no password reset by design. For the product shape around a client-held session, see why host a heavy server when a PWA can do everything locally?. For how storage work stays off the frame loop while you are unlocked, see SQLite in a Web Worker is not optional.
If that session model fits how you want a private vault to work, you can try NT² Vault or read more at nt2.me.
Last updated 2026-09-19
Related stories
- XSS steals sessions elsewhere; here keys are non-extractable
8 min read
- Wrong password must fail closed
8 min read
- The KDF salt stays on your device
9 min read