225 lines
11 KiB
Markdown
225 lines
11 KiB
Markdown
# WebKit Website Data Isolation Per Nostr Identity
|
|
|
|
**Status: IMPLEMENTED (Phase 0 + Phase A, verified end-to-end).**
|
|
|
|
## Problem
|
|
|
|
When a user logs in, visits a page, logs out, then logs in as a different
|
|
identity and revisits the same page, the second user sees the first user's
|
|
data (session cookies, localStorage, cached responses, service workers).
|
|
|
|
## Root Cause
|
|
|
|
The browser uses the **default** [`WebKitWebContext`](src/main.c:977) and its
|
|
attached [`WebKitWebsiteDataManager`](src/main.c:993) for the entire process
|
|
lifetime. That single data manager holds, process-wide:
|
|
|
|
- HTTP disk + memory cache
|
|
- Cookies (the primary leak — session cookies identify you to web pages)
|
|
- localStorage / sessionStorage / IndexedDB / WebSQL
|
|
- Service worker registrations + offline app cache
|
|
- Favicon database ([`webkit_web_context_set_favicon_database_directory(web_ctx, NULL)`](src/main.c:1003))
|
|
|
|
The logout / identity-switch paths only clear the Nostr signer and do **nothing**
|
|
to WebKit's data stores or to the already-loaded tabs:
|
|
|
|
- [`app_menu_logout_proxy`](src/main.c:186) — frees `g_state.signer`, blanks pubkey, returns.
|
|
- [`agent_logout`](src/agent_login.c:485) — `app_clear_signer()` + `nostr_bridge_set_signer(NULL, ...)`.
|
|
- [`agent_switch_identity`](src/agent_login.c:492) — frees old signer, logs in new, no tab reload.
|
|
|
|
Two compounding problems:
|
|
|
|
1. **Persistent WebKit data** survives the identity switch.
|
|
2. **Live tabs** keep user A's DOM/localStorage in memory; wiping the data
|
|
manager alone does not clear already-loaded pages.
|
|
|
|
The existing [`plans/per-user-profiles.md`](plans/per-user-profiles.md) isolates
|
|
the SQLite layer (bookmarks, history, session, Nostr events) but does **not**
|
|
cover WebKit's own storage — this plan closes that gap.
|
|
|
|
## Solution (chosen: per-user data manager + clear-on-switch safety net)
|
|
|
|
### Part A — Per-user `WebKitWebsiteDataManager`
|
|
|
|
Give each pubkey its own data manager rooted at
|
|
`~/.sovereign_browser/profiles/<pubkey_hex>/webkit/` so cookies, cache, storage,
|
|
service workers, and favicons persist per-user and never cross-contaminate.
|
|
|
|
WebKitGTK constraint: a `WebKitWebContext` is bound to **one**
|
|
`WebKitWebsiteDataManager` for its lifetime — you cannot swap the data manager
|
|
on an existing context. Two viable strategies:
|
|
|
|
**Strategy 1 (preferred): per-user `WebKitWebContext`.**
|
|
Create a fresh `webkit_web_context_new_with_website_data_manager(dm)` for each
|
|
login. All tabs/webviews are created from this context. On logout/switch,
|
|
destroy the old context (which tears down its webviews) and build a new one.
|
|
This is the cleanest isolation and matches how Epiphary/GNOME Web does
|
|
per-profile isolation.
|
|
|
|
**Strategy 2 (fallback): single context, manual clear + per-user dirs.**
|
|
Keep the default context but on each login call
|
|
`webkit_website_data_manager_clear(dm, WEBKIT_WEBSITE_DATA_ALL, 0, NULL, NULL, NULL)`
|
|
then point the cookie/cache/storage dirs at the per-user path. This is fragile
|
|
because the context caches some state internally and the favicon DB directory
|
|
is set once at context init.
|
|
|
|
Recommend **Strategy 1**. It requires reworking how the context is created and
|
|
how `tab_manager` / `nostr_bridge` / `tor_scheme` / `nostr_scheme` / `net_services`
|
|
reference it (they currently call `webkit_web_context_get_default()` or receive
|
|
the context at init), but it gives true isolation.
|
|
|
|
### Part B — Clear-on-switch safety net
|
|
|
|
Even with per-user data managers, the **live tabs** (and the agent chat sidebar
|
|
webview) hold user A's DOM/localStorage in memory. On any identity change:
|
|
|
|
1. Close all tabs: `tab_manager_close_all()` ([`tab_manager.c:3727`](src/tab_manager.c)).
|
|
2. Destroy the agent chat sidebar webview if present.
|
|
3. (Strategy 1) Destroy the old `WebKitWebContext` + data manager.
|
|
4. Build the new per-user context + data manager.
|
|
5. Re-register URI schemes (`sovereign://`, `nostr://`, `tor://`) on the new
|
|
context — they are per-context.
|
|
6. Re-init `nostr_bridge`, `nostr_scheme`, `tor_scheme`, `net_services` against
|
|
the new context.
|
|
7. Re-init `tab_manager` against the new context (or expose a
|
|
`tab_manager_set_context()`).
|
|
8. Restore the new user's session (`session_restore()`) or open the default
|
|
new-tab URL.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Login complete - pubkey known] --> B[profile_ensure_dir pubkey]
|
|
B --> C[Build per-user WebKitWebsiteDataManager<br/>rooted at profiles/pubkey/webkit]
|
|
C --> D[Build per-user WebKitWebContext<br/>new_with_website_data_manager]
|
|
D --> E[Register sovereign/nostr/tor schemes on new ctx]
|
|
E --> F[Re-init nostr_bridge, net_services, tab_manager on new ctx]
|
|
F --> G[session_restore or open new-tab URL]
|
|
|
|
H[Logout / switch_identity] --> I[tab_manager_close_all]
|
|
I --> J[Destroy sidebar webview]
|
|
J --> K[Destroy old WebKitWebContext + data manager]
|
|
K --> L{switch or logout?}
|
|
L -->|switch| A
|
|
L -->|logout| M[Show login dialog / wait for agent login]
|
|
M --> A
|
|
```
|
|
|
|
## Implementation Steps
|
|
|
|
### 1. Profile helpers for WebKit data dir
|
|
In [`src/profile.c`](src/profile.c) / [`src/profile.h`](src/profile.h) add:
|
|
- `profile_get_webkit_dir(const char *pubkey_hex, char *out, size_t out_sz)`
|
|
→ `~/.sovereign_browser/profiles/<pubkey>/webkit/`
|
|
- `profile_ensure_webkit_dir(const char *pubkey_hex)` — mkdir -p.
|
|
|
|
### 2. New module `src/web_context.c` / `src/web_context.h`
|
|
Centralize context + data manager construction so login/logout/switch share
|
|
one code path. Functions:
|
|
- `web_context_build_for_pubkey(const char *pubkey_hex)` → returns a new
|
|
`WebKitWebContext*` with a per-user `WebKitWebsiteDataManager` (cookies,
|
|
cache, local_storage, indexed_db, websql, offline_app_cache, service_worker
|
|
registrations, itp, hsts all pointed at the per-user webkit dir), favicon
|
|
DB enabled, TLS errors ignored, security manager configured
|
|
(sovereign/tor/file secure+local).
|
|
- `web_context_teardown(WebKitWebContext *ctx)` — unrefs context + data
|
|
manager, frees per-user state.
|
|
- Holds the current context pointer (`g_web_ctx`) so the rest of the codebase
|
|
can fetch it without `webkit_web_context_get_default()`.
|
|
|
|
### 3. Refactor scheme/bridge/service init to be re-runnable
|
|
Currently these are one-shot inits tied to the default context. Make them
|
|
accept a `WebKitWebContext*` and be safe to call again on a new context:
|
|
- [`nostr_bridge_register`](src/nostr_bridge.c) — already takes `ctx`; ensure
|
|
it can be called twice (track prior registration, disconnect old handlers).
|
|
- [`nostr_scheme_register`](src/nostr_scheme.c) — make it take `ctx` (currently
|
|
uses default) and idempotent.
|
|
- [`tor_scheme_register`](src/tor_scheme.c) — same.
|
|
- [`net_services_init`](src/net_services.c) — currently calls
|
|
`webkit_web_context_get_default()` internally ([`net_services.c:69`](src/net_services.c));
|
|
pass `ctx` in instead.
|
|
|
|
### 4. Refactor `tab_manager` to support context swap
|
|
[`tab_manager_init`](src/tab_manager.h:46) currently stores the context once.
|
|
Add:
|
|
- `tab_manager_set_context(WebKitWebContext *ctx)` — updates the stored
|
|
context so subsequent `tab_manager_new_tab` calls use the new context.
|
|
Existing tabs are already closed (Part B step 1) before this is called.
|
|
- Or simpler: tear down and re-init `tab_manager` on each switch. Pick whichever
|
|
is less invasive given the static globals in [`tab_manager.c`](src/tab_manager.c).
|
|
|
|
### 5. Wire the login/logout/switch flows
|
|
- [`app_menu_logout_proxy`](src/main.c:186): after clearing the signer, run the
|
|
teardown sequence (close all tabs, destroy sidebar, destroy context) then
|
|
re-show the login dialog. On successful login, run the build sequence.
|
|
- [`agent_logout`](src/agent_login.c:485): same teardown, then leave the
|
|
browser in a logged-out state (no context, or a minimal ephemeral context
|
|
showing a "logged out" page).
|
|
- [`agent_switch_identity`](src/agent_login.c:492): teardown old, build new
|
|
with the new pubkey, restore session.
|
|
- Initial startup in [`main.c`](src/main.c:974): replace
|
|
`webkit_web_context_get_default()` with `web_context_build_for_pubkey()`
|
|
after login completes (this means deferring context creation until after
|
|
the login dialog — reordering the startup sequence).
|
|
|
|
### 6. Favicon database per user
|
|
[`webkit_web_context_set_favicon_database_directory(web_ctx, NULL)`](src/main.c:1003)
|
|
currently uses the default shared location. In `web_context_build_for_pubkey`,
|
|
pass the per-user webkit dir (or a `favicons/` subdir) so favicons don't leak
|
|
between users.
|
|
|
|
### 7. Read-only / no-login mode
|
|
When running with `--no-login` or read-only, there is no pubkey to key the
|
|
data dir on. Use an ephemeral data manager
|
|
(`webkit_website_data_manager_new_ephemeral()`) so no data persists at all,
|
|
or a shared `~/.sovereign_browser/webkit-anon/` dir. Decide and document.
|
|
|
|
### 8. Migration
|
|
Existing users have data in WebKit's default location
|
|
(`~/.cache/sovereign_browser/` or similar). On first login with the new
|
|
system, optionally copy the default WebKit data dir into
|
|
`profiles/<pubkey>/webkit/` so they keep their cookies/cache. Low priority —
|
|
can ship without migration and just start fresh per user.
|
|
|
|
### 9. Tests
|
|
- Manual: log in as user A, visit a site that sets a cookie/localStorage,
|
|
log out, log in as user B, visit the same site, confirm user A's data is
|
|
gone and user B gets a fresh session.
|
|
- Add a test page under [`tests/local-site/`](tests/local-site) that writes
|
|
a visible marker to localStorage + a cookie, so this is reproducible.
|
|
- Verify `sovereign://`, `nostr://`, `tor://` schemes still work after a
|
|
switch (they are re-registered on the new context).
|
|
- Verify the agent chat sidebar works after a switch (its webview is rebuilt
|
|
on the new context).
|
|
|
|
## Files to Modify
|
|
|
|
- New: `src/web_context.c`, `src/web_context.h`
|
|
- [`src/profile.c`](src/profile.c) / [`src/profile.h`](src/profile.h) — webkit dir helpers
|
|
- [`src/main.c`](src/main.c) — startup ordering, logout proxy, use `web_context_*`
|
|
- [`src/agent_login.c`](src/agent_login.c) — `agent_logout`, `agent_switch_identity`
|
|
- [`src/tab_manager.c`](src/tab_manager.c) / [`src/tab_manager.h`](src/tab_manager.h) — context swap support, sidebar teardown
|
|
- [`src/nostr_bridge.c`](src/nostr_bridge.c) — re-runnable registration
|
|
- [`src/nostr_scheme.c`](src/nostr_scheme.c) — accept ctx, idempotent
|
|
- [`src/tor_scheme.c`](src/tor_scheme.c) — accept ctx, idempotent
|
|
- [`src/net_services.c`](src/net_services.c) — accept ctx instead of default
|
|
- [`Makefile`](Makefile) — add `web_context.o`
|
|
- New test page: `tests/local-site/identity-leak-test.html`
|
|
|
|
## Risk Assessment
|
|
|
|
- **High risk** — changes how the `WebKitWebContext` is created and tears down,
|
|
which touches every subsystem that references the context.
|
|
- **Startup reordering** — context creation must move to *after* login, which
|
|
changes the long-standing flow in [`main.c`](src/main.c).
|
|
- **Re-runnable scheme registration** — WebKit may not support unregistering
|
|
scheme handlers cleanly; verify that building a fresh context and
|
|
re-registering works without leaking the old context.
|
|
- **Sidebar webview** — the agent chat sidebar ([`tab_manager_toggle_sidebar`](src/tab_manager.h:198))
|
|
keeps a long-lived webview; must be destroyed on context swap or it will
|
|
hold a ref to the dead context.
|
|
- **Mitigation**: implement Part B (clear-on-switch) first as a standalone
|
|
change — it alone fixes the leak even without per-user dirs. Ship that,
|
|
then layer Part A (per-user data managers) on top.
|