feat(auth): bring opaque (RFC 9807) auth
OPAQUE (RFC 9807) implementation (using `opaque-ke` crate)
with opaque authentfication, server will never receive the password (in the auth=password mode)
this is a must have to create trust with users to permit end to end encryption in the future
(we cannot know if user use the same password/passphrase for his asymetric key or his oxicloud auth,
this is why server must never have the password)
pass1: prepare server
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- OPAQUE aPAKE registration + login (Phase 0)
|
||||
-- ════════════════════════════════════════════════════════════════════════════
|
||||
-- OPAQUE (RFC 9807) is a zero-knowledge password-authenticated key exchange:
|
||||
-- the server never sees the user's passphrase, not on registration and not on
|
||||
-- login. What lives server-side is the OPAQUE "envelope" — an opaque blob the
|
||||
-- client uploads at registration time that can only be decrypted with the
|
||||
-- passphrase (via a memory-hard KSF). Login is a two-round exchange that
|
||||
-- proves possession of the passphrase without transmitting it.
|
||||
--
|
||||
-- This migration reserves the storage for the envelope + a small set of
|
||||
-- lifecycle timestamps and the client-visible pubkey. The columns are added
|
||||
-- as NULLABLE so existing password accounts keep working — the OPAQUE
|
||||
-- endpoints ship inert (`OXICLOUD_OPAQUE_MODE=off`) in Phase 0. Later phases
|
||||
-- populate these columns via a silent-migration hook on legacy login.
|
||||
--
|
||||
-- Columns:
|
||||
-- opaque_envelope BYTEA — RegistrationUpload from opaque-ke,
|
||||
-- serialized. Cannot be decrypted by
|
||||
-- the server; only the client with the
|
||||
-- correct passphrase can complete the
|
||||
-- login handshake. This blob also
|
||||
-- embeds the OPAQUE client static
|
||||
-- pubkey used in the 3DH login step —
|
||||
-- extracted at handshake time, not
|
||||
-- denormalised into its own column
|
||||
-- (see docs/plan/opaque.md: the E2EE
|
||||
-- identity pubkey is a separate,
|
||||
-- later concern with its own bridges
|
||||
-- table; conflating the two now would
|
||||
-- bias the E2EE design toward "one
|
||||
-- KEK bridge = OPAQUE" which excludes
|
||||
-- magic-link / OIDC users).
|
||||
-- opaque_ciphersuite_version SMALLINT — matches
|
||||
-- `AppConfig::opaque.ciphersuite_version`.
|
||||
-- Reserved for a future ciphersuite
|
||||
-- migration; current bind is v1 =
|
||||
-- Ristretto255-SHA512-3DH-Argon2id.
|
||||
-- opaque_registered_at TIMESTAMPTZ — first-ever successful registration.
|
||||
-- Set by /register/finish. Presence
|
||||
-- means "this user has an OPAQUE
|
||||
-- envelope on file."
|
||||
-- opaque_migrated_at TIMESTAMPTZ — first successful login VIA OPAQUE.
|
||||
-- Presence means "legacy password
|
||||
-- endpoint is refused for this user
|
||||
-- (Phase 3+)." Distinct from
|
||||
-- registered_at because the transition
|
||||
-- from "envelope exists" to "OPAQUE is
|
||||
-- mandatory" needs a separate signal.
|
||||
--
|
||||
-- force_password_change_at_next_login BOOLEAN — orthogonal but shipped in
|
||||
-- the same migration because the
|
||||
-- admin-set-password flow depends on
|
||||
-- it (see docs/plan/opaque.md §Phase
|
||||
-- 0). Set by the admin reset
|
||||
-- endpoint; cleared by change_password.
|
||||
-- Enables the "admin picks a
|
||||
-- temporary password, user must
|
||||
-- replace it on first use" pattern
|
||||
-- that keeps admin capability intact
|
||||
-- through the OPAQUE cutover.
|
||||
--
|
||||
-- Indexes:
|
||||
-- idx_users_opaque_migrated — partial (WHERE opaque_migrated_at IS NOT
|
||||
-- NULL). Answers "how many users have finished
|
||||
-- migration?" cheaply for the ops dashboard;
|
||||
-- would be sparse if unfiltered.
|
||||
|
||||
ALTER TABLE auth.users
|
||||
ADD COLUMN opaque_envelope BYTEA,
|
||||
ADD COLUMN opaque_ciphersuite_version SMALLINT,
|
||||
ADD COLUMN opaque_registered_at TIMESTAMPTZ,
|
||||
ADD COLUMN opaque_migrated_at TIMESTAMPTZ,
|
||||
ADD COLUMN force_password_change_at_next_login BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE INDEX idx_users_opaque_migrated
|
||||
ON auth.users (opaque_migrated_at)
|
||||
WHERE opaque_migrated_at IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN auth.users.opaque_envelope IS
|
||||
'OPAQUE (RFC 9807) RegistrationUpload blob. Server-opaque; the passphrase
|
||||
is required client-side to complete the login handshake. NULL = user has
|
||||
no OPAQUE registration (Phase 0 default, or account pre-dates the
|
||||
silent-migration rollout in Phase 2). E2EE identity pubkeys live in a
|
||||
separate table added in the E2EE phase — this blob is auth-scoped only.';
|
||||
|
||||
COMMENT ON COLUMN auth.users.opaque_ciphersuite_version IS
|
||||
'Version of the OPAQUE ciphersuite this envelope was minted under. Bound
|
||||
at registration time; changing the server-side ciphersuite invalidates
|
||||
every envelope minted before the flip. Current bind is v1 =
|
||||
Ristretto255-SHA512-3DH with Argon2id KSF.';
|
||||
|
||||
COMMENT ON COLUMN auth.users.opaque_registered_at IS
|
||||
'When this account first minted an OPAQUE envelope. Presence means "the
|
||||
endpoints are usable for this user." Distinct from opaque_migrated_at:
|
||||
you can be registered but still fall back to legacy password auth during
|
||||
the dual-mode window (Phase 2).';
|
||||
|
||||
COMMENT ON COLUMN auth.users.opaque_migrated_at IS
|
||||
'When this account first completed a successful OPAQUE login. Presence
|
||||
means the legacy POST /api/auth/login endpoint is refused for this user
|
||||
(Phase 3+). Once set, the admin-reset flow that goes back through legacy
|
||||
also nulls this column to re-open the fallback path.';
|
||||
|
||||
COMMENT ON COLUMN auth.users.force_password_change_at_next_login IS
|
||||
'When TRUE, the next successful login (legacy or OPAQUE) redirects the
|
||||
user to the change-password flow before any other action. Set by the
|
||||
admin-reset endpoint so admin-picked passwords are always temporary;
|
||||
cleared by change_password on success.';
|
||||
Reference in New Issue
Block a user