Phase A1 of the multi-person sharing ADR
(docs/adr/multi-person-sharing.md). Each account now gets an X25519
keypair at registration: the public half stored plaintext, the private
half wrapped under the account DEK and stored as opaque ciphertext. The
keypair is generated client-side; the backend adds no crypto deps and
stores everything verbatim, preserving the zero-knowledge contract.
This change only introduces the keypair and threads it through the auth
flows — it is not consumed yet. It unblocks Phase B (profile sharing)
without touching the data model or the ~11 frontend encrypt call sites,
which is Phase A2 (per-profile DEKs).
Backend:
- User model: identity_public_key, identity_private_key_wrapped{,_iv}
(all Option<String>, backward compatible).
- RegisterRequest/AuthResponse carry the 3 fields; register + login
echo them. No changes to change_password/recover (DEK value is
unchanged across both, so the wrapped private key is too).
- 2 new integration tests: round-trip through register/login, and
optional-fields backward compat.
Frontend:
- crypto/keys.ts: generateIdentityKeyPair, wrapIdentityPrivateKey,
unwrapIdentityPrivateKey + in-memory identity store mirroring the DEK.
- types/api.ts: AuthTokens + RegisterRequest extended (removes an
existing `as any` cast).
- useStore register/login/logout + UnlockPage unwrap the private key
alongside the DEK.
- 3 new crypto tests (X25519 lifecycle, wrong-DEK rejection, distinct
shared secrets); skip gracefully where the runtime lacks X25519.
Backend: cargo test + clippy green. Frontend: npm test + tsc green.
Also gitignore .zcode/ (local tooling artifact).