Skip to content

audit F16 [security]: localStorage read-failure silently mints a fresh identity, masking storage-disabled / private-mode / corruption as "first run" #616

@intendednull

Description

@intendednull

File: crates/client/src/lib.rs:993
Severity: security
Obvious? no

load_identity() calls storage::load_identity_bytes() which on wasm32 (crates/client/src/storage.rs:269) does web_sys::window()?.local_storage().ok()??.get_item(...). Every error path — Safari private mode (local_storage() throws), Firefox "block all storage", dom.storage.enabled=false, QuotaExceededError from a previous corrupted/full state, base64 decode failure — collapses into None. The caller treats None indistinguishably from "first run" and immediately generates a brand-new Identity::generate(), overwrites whatever was there with save_identity_bytes, and proceeds.

Threats: (a) a user whose storage transiently fails (extension toggle, profile cleanup, quota pressure from an unrelated app on the same origin) loses their long-term Ed25519 identity and reappears as an unauthenticated stranger to every server they own — including the owner of a server, who now cannot make admin events because the DAG only trusts the original key; (b) tampering attack: an extension or co-tenant page that clears just willow_identity forces fresh generation, then can race the save and inject an attacker-controlled key.

Fix: distinguish Err / quota / disabled-storage from Ok(None). Surface read errors to the UI ("Browser storage unavailable — your identity cannot be loaded; continue ephemeral?") and never overwrite a key slot if the read failed for a reason other than "empty".


Filed by /general-audit @ 88498a5 (2026-05-04). master: #600.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions