84 lines
3.6 KiB
Markdown
84 lines
3.6 KiB
Markdown
# ADR-0002: Session memory holds only posting + memo private keys
|
|
|
|
**Status:** Accepted
|
|
**Date:** 2026-04-17
|
|
**Deciders:** project maintainer
|
|
|
|
## Context
|
|
|
|
Blurt accounts have four key roles: `owner`, `active`, `posting`, `memo`.
|
|
|
|
| Role | What it authorizes | Exposure budget |
|
|
|---------|------------------------------------------------------------|-----------------|
|
|
| owner | Account recovery, key rotation | Rare — minutes per year, at most |
|
|
| active | BLURT token `transfer` ops (listing fees paid in BLURT) | Occasional — one transfer per fee-paid listing |
|
|
| posting | `custom_json` / `comment` ops (orders, chat, feedback, profile) | Constant — signs every Morphit action |
|
|
| memo | End-to-end encrypted chat key derivation | Constant — every chat message |
|
|
|
|
If Morphit keeps all four keys in live browser memory, a single XSS or
|
|
supply-chain compromise exposes the user's entire account, including
|
|
recovery. That's a disproportionate blast radius for a frontend bug.
|
|
|
|
## Decision
|
|
|
|
A running Morphit session holds the **posting** and **memo** private keys
|
|
only. `owner` and `active` private keys live exclusively inside the
|
|
Argon2id-encrypted keystore and are reached via callback-style
|
|
just-in-time functions (`useActiveKey`, `useOwnerKey` in
|
|
`$crypto/keystore`) that decrypt, hand the key to one signing operation,
|
|
and zero it in a `finally` block regardless of success or exception.
|
|
|
|
The `LiveIdentity` type enforces this structurally — it has no `active`
|
|
or `owner` *private* field. Code cannot accidentally sign a transfer
|
|
with its posting key because the relevant type is absent.
|
|
|
|
## Alternatives considered
|
|
|
|
- **Keep all four in live memory.** Simplest code path. Rejected —
|
|
disproportionate blast radius as explained above.
|
|
|
|
- **Prompt for password on every action.** Safest possible, but the UX
|
|
is punishing when a user is browsing and chatting. Rejected in favor
|
|
of the tiered policy: common-path operations (posting, chat) need no
|
|
password; rare high-value operations (`transfer`, account creation)
|
|
do.
|
|
|
|
- **Delegate active/owner to a browser extension (WhaleVault, Gravity).**
|
|
Strictly stronger than the JIT-unlock pattern, because the keys
|
|
physically never enter Morphit's origin. Added to carry-forward list
|
|
for Phase 4 (depends on extension interoperability). Phase 2 ships
|
|
the JIT-unlock as the fallback that works without an extension.
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- Compromise of posting key exposes Morphit activity but cannot transfer
|
|
funds, rotate keys, or impersonate the user in any way that survives
|
|
a key rotation.
|
|
- Compromise of memo key exposes chat history but nothing else.
|
|
- Active and owner privates spend only milliseconds in cleartext memory
|
|
per transaction.
|
|
|
|
### Negative
|
|
|
|
- User must enter their password to pay a listing fee in BLURT. UX
|
|
friction, but users pay only when posting orders (not when browsing
|
|
or chatting).
|
|
- JIT unlock is still vulnerable to a compromised page origin observing
|
|
the password at the prompt. Documented in `docs/SECURITY.md` as a
|
|
known v1 limitation; Phase 4's extension integration removes it.
|
|
|
|
### Follow-up work
|
|
|
|
- Phase 4: WhaleVault / Gravity browser-extension signing path (Phase 1
|
|
review item #16).
|
|
- Phase 5: consider secure-enclave signing on platforms that have it
|
|
(WebAuthn / Passkey-backed private keys).
|
|
|
|
## References
|
|
|
|
- `apps/web/src/lib/crypto/keygen.ts` — `FullIdentity` / `LiveIdentity`
|
|
type split.
|
|
- `apps/web/src/lib/crypto/keystore.ts` — `useActiveKey`, `useOwnerKey`.
|
|
- Phase 1 review log entry 2026-04-17 (docs/REVIEW-PHASE1.md).
|