From 6965855388d98d66ac8b804060228fb3c8e5b885 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Tue, 4 Aug 2026 07:31:22 +0200 Subject: [PATCH] reactor(opaque): normalize to OXICLOUD_AUTH_OPAQUE_* variables --- Dockerfile | 2 +- docs/config/authentication.md | 14 ++++---- docs/config/env.md | 14 ++++---- example.env | 18 +++++------ .../services/auth_application_service.rs | 4 +-- src/bin/opaque-setup.rs | 8 ++--- src/common/config.rs | 32 +++++++++---------- src/common/di.rs | 12 +++---- .../repositories/pg/opaque_pg_repository.rs | 2 +- src/infrastructure/services/opaque_service.rs | 16 +++++----- tests/api/opaque_substrate.hurl | 4 +-- tests/common/server.env | 10 +++--- tests/e2e/fixtures/oxicloud-stack.ts | 4 +-- tests/e2e/playwright.config.ts | 4 +-- tests/e2e/playwright.coverage.config.ts | 4 +-- 15 files changed, 73 insertions(+), 75 deletions(-) diff --git a/Dockerfile b/Dockerfile index abf1f1ea..aaab1317 100644 --- a/Dockerfile +++ b/Dockerfile @@ -172,7 +172,7 @@ COPY --from=app --chmod=755 /app/release/oxicloud /usr/local/bin/ # ship; it only mutates `storage.files` rows whose name ≠ NFC(name). COPY --from=app --chmod=755 /app/release/migrate-nfc-filenames /usr/local/bin/ # Ship the OPAQUE server-setup generator alongside the server so operators -# can generate their `OXICLOUD_OPAQUE_SERVER_SETUP` value inside the +# can generate their `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` value inside the # container without a separate Rust toolchain: # docker run --rm opaque-setup # prints the base64 value # One-shot, side-effect-free — safe to include; the runtime doesn't diff --git a/docs/config/authentication.md b/docs/config/authentication.md index a53fe84d..c1565ef6 100644 --- a/docs/config/authentication.md +++ b/docs/config/authentication.md @@ -119,20 +119,20 @@ The verification-piggyback flow above deliberately **bypasses the `has_password` OPAQUE (RFC 9807) replaces the traditional "browser sends passphrase, server hashes it" flow with a two-round cryptographic exchange in which the passphrase **never leaves the client**. On registration the client encrypts a random key blob under the passphrase and uploads that opaque envelope. On login the client proves possession of the passphrase without transmitting it — the server can neither read it nor derive it from what it stores. -This is the substrate for planned end-to-end encryption work (see `docs/plan/opaque.md` for the full multi-phase roadmap). This build ships **Phase 0 only** — the primitives, migration column, and configuration substrate. Endpoints are inert until `OXICLOUD_OPAQUE_MODE` is enabled in a future release. +This is the substrate for planned end-to-end encryption work (see `docs/plan/opaque.md` for the full multi-phase roadmap). This build ships **Phase 0 only** — the primitives, migration column, and configuration substrate. Endpoints are inert until `OXICLOUD_AUTH_OPAQUE_MODE` is enabled in a future release. ### When to enable OPAQUE -OPAQUE only touches the password login path. If your deployment doesn't use password auth at all — you've set `OXICLOUD_AUTH_METHODS=oidc`, or `magic_link`, or the OIDC master-rule has locked things down to SSO only — OPAQUE has nothing to shadow and there's no reason to enable it. **Leave every `OXICLOUD_OPAQUE_*` variable at default** (unset). No `OXICLOUD_OPAQUE_SERVER_SETUP` is required in that case; the server won't ask for one. +OPAQUE only touches the password login path. If your deployment doesn't use password auth at all — you've set `OXICLOUD_AUTH_METHODS=oidc`, or `magic_link`, or the OIDC master-rule has locked things down to SSO only — OPAQUE has nothing to shadow and there's no reason to enable it. **Leave every `OXICLOUD_AUTH_OPAQUE_*` variable at default** (unset). No `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` is required in that case; the server won't ask for one. -Even if you accidentally set `OXICLOUD_OPAQUE_MODE=migrate` in an OIDC-only deployment, the boot-time cross-check downgrades the effective mode to `off` and emits an audit-channel INFO explaining why. This is intentional so operators aren't blocked by a setup requirement for a feature they don't use. +Even if you accidentally set `OXICLOUD_AUTH_OPAQUE_MODE=migrate` in an OIDC-only deployment, the boot-time cross-check downgrades the effective mode to `off` and emits an audit-channel INFO explaining why. This is intentional so operators aren't blocked by a setup requirement for a feature they don't use. ### Enabling OPAQUE (when the endpoints ship in Phase 1) Password-using deployments will opt in via three env vars: -1. **`OXICLOUD_OPAQUE_MODE`** — set to `migrate` for the dual-mode phase where both OPAQUE and legacy password login are accepted, then later to `opaque_only` after most users have completed migration. -2. **`OXICLOUD_OPAQUE_SERVER_SETUP`** — generated once and persisted like your JWT secret. Rotating this invalidates every user's registration; treat it as one of the crown jewels. Two ways to generate: +1. **`OXICLOUD_AUTH_OPAQUE_MODE`** — set to `migrate` for the dual-mode phase where both OPAQUE and legacy password login are accepted, then later to `opaque_only` after most users have completed migration. +2. **`OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`** — generated once and persisted like your JWT secret. Rotating this invalidates every user's registration; treat it as one of the crown jewels. Two ways to generate: ```bash # Docker (recommended in production — no toolchain needed): docker run --rm ghcr.io/atalayalabs/oxicloud:latest opaque-setup @@ -141,9 +141,9 @@ Password-using deployments will opt in via three env vars: cargo run --bin opaque-setup ``` Both print the base64 value on stdout (with guidance on stderr, so shell pipelines like `$(docker run ... opaque-setup)` capture cleanly). -3. **`OXICLOUD_OPAQUE_KSF_*`** — client-side Argon2id key-stretching cost. Defaults (256 MiB / 3 iter / 4 lanes) are appropriate for modern desktop / phone hardware. Bumping later is safe (only affects new registrations); lowering is not (still-registered users get a security downgrade the next time they change their passphrase). +3. **`OXICLOUD_AUTH_OPAQUE_KSF_*`** — client-side Argon2id key-stretching cost. Defaults (256 MiB / 3 iter / 4 lanes) are appropriate for modern desktop / phone hardware. Bumping later is safe (only affects new registrations); lowering is not (still-registered users get a security downgrade the next time they change their passphrase). -The `OXICLOUD_HASH_*` variables (server-side legacy Argon2) and `OXICLOUD_OPAQUE_KSF_*` (client-side OPAQUE Argon2) are intentionally separate: the server-side path is RAM-bounded by concurrent-login traffic and needs to stay modest; the client-side path is single-user per attempt and can afford much higher memory. Tuning them together would force a bad compromise in one direction or the other. +The `OXICLOUD_HASH_*` variables (server-side legacy Argon2) and `OXICLOUD_AUTH_OPAQUE_KSF_*` (client-side OPAQUE Argon2) are intentionally separate: the server-side path is RAM-bounded by concurrent-login traffic and needs to stay modest; the client-side path is single-user per attempt and can afford much higher memory. Tuning them together would force a bad compromise in one direction or the other. ### What OPAQUE does NOT touch diff --git a/docs/config/env.md b/docs/config/env.md index 740e6fc9..81200bc9 100644 --- a/docs/config/env.md +++ b/docs/config/env.md @@ -41,7 +41,7 @@ Most runtime variables use the `OXICLOUD_` prefix. A few build-time or allocator | `OXICLOUD_JWT_SECRET` | (auto-generated) | JWT signing secret; auto-persisted to `/.jwt_secret` if unset | | `OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS` | `3600` | Access token lifetime (1 hour) | | `OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS` | `604800` | Refresh token lifetime (7 days); active sessions auto-renew on use | -| `OXICLOUD_HASH_MEMORY_COST` | `65536` | Argon2id memory cost in KiB (64 MiB). **Server-side** — used by the legacy password path (`POST /api/auth/login`) and the app-password Basic-Auth verifier. Distinct from `OXICLOUD_OPAQUE_KSF_*` (client-side). | +| `OXICLOUD_HASH_MEMORY_COST` | `65536` | Argon2id memory cost in KiB (64 MiB). **Server-side** — used by the legacy password path (`POST /api/auth/login`) and the app-password Basic-Auth verifier. Distinct from `OXICLOUD_AUTH_OPAQUE_KSF_*` (client-side). | | `OXICLOUD_HASH_TIME_COST` | `3` | Argon2id iteration count for the server-side legacy path. | | `OXICLOUD_HASH_PARALLELISM` | `2` | Argon2id parallelism lanes for the server-side legacy path. | | `OXICLOUD_DISABLE_REGISTRATION` | false | Disable registration of new user accounts | @@ -52,15 +52,15 @@ Most runtime variables use the `OXICLOUD_` prefix. A few build-time or allocator ### OPAQUE aPAKE (zero-knowledge password login) -OPAQUE (RFC 9807) is a zero-knowledge password-authenticated key exchange: the passphrase never leaves the client, not on registration and not on login. It's shipped in stages (see `docs/plan/opaque.md`); this build carries the **substrate only** — endpoints are inert until `OXICLOUD_OPAQUE_MODE` is set. See `docs/config/authentication.md` for the phase rollout, the migration plan, and admin-facing guidance. +OPAQUE (RFC 9807) is a zero-knowledge password-authenticated key exchange: the passphrase never leaves the client, not on registration and not on login. It's shipped in stages (see `docs/plan/opaque.md`); this build carries the **substrate only** — endpoints are inert until `OXICLOUD_AUTH_OPAQUE_MODE` is set. See `docs/config/authentication.md` for the phase rollout, the migration plan, and admin-facing guidance. | Variable | Default | Description | |---|---|---| -| `OXICLOUD_OPAQUE_MODE` | `off` | Runtime mode. `off` = endpoints 404 (default). `migrate` = endpoints live, legacy `POST /api/auth/login` still accepted. `opaque_only` = endpoints live, legacy refused for users with an envelope. **Effective-mode cross-check**: when `password` is not in `OXICLOUD_AUTH_METHODS`, the mode is auto-downgraded to `off` with an audit-channel INFO line (OPAQUE only replaces the password path — nothing to shadow in an OIDC-only or magic-link-only deployment). So OIDC / magic-link-only operators can safely ignore every `OXICLOUD_OPAQUE_*` variable. | -| `OXICLOUD_OPAQUE_SERVER_SETUP` | — | Base64-encoded `ServerSetup` blob. **Required** when `OXICLOUD_OPAQUE_MODE != off` AND password is enabled — the server refuses to start with a helpful error otherwise. Generate once with the `opaque-setup` CLI subcommand and persist the value like your JWT secret. **Never rotate** — rotating invalidates every user's envelope (they'd all need to reset their passphrase). | -| `OXICLOUD_OPAQUE_KSF_MEMORY_KIB` | `262144` | Client-side Argon2id memory cost in KiB (256 MiB). Runs on the user's device during OPAQUE login/registration, not on the server. Distinct from `OXICLOUD_HASH_MEMORY_COST` (server-side legacy path). Higher values slow brute-force after a hypothetical envelope leak but also slow login on the user's device. | -| `OXICLOUD_OPAQUE_KSF_ITERATIONS` | `3` | Client-side Argon2id iteration count. | -| `OXICLOUD_OPAQUE_KSF_PARALLELISM` | `4` | Client-side Argon2id parallelism lanes. | +| `OXICLOUD_AUTH_OPAQUE_MODE` | `off` | Runtime mode. `off` = endpoints 404 (default). `migrate` = endpoints live, legacy `POST /api/auth/login` still accepted. `opaque_only` = endpoints live, legacy refused for users with an envelope. **Effective-mode cross-check**: when `password` is not in `OXICLOUD_AUTH_METHODS`, the mode is auto-downgraded to `off` with an audit-channel INFO line (OPAQUE only replaces the password path — nothing to shadow in an OIDC-only or magic-link-only deployment). So OIDC / magic-link-only operators can safely ignore every `OXICLOUD_AUTH_OPAQUE_*` variable. | +| `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` | — | Base64-encoded `ServerSetup` blob. **Required** when `OXICLOUD_AUTH_OPAQUE_MODE != off` AND password is enabled — the server refuses to start with a helpful error otherwise. Generate once with the `opaque-setup` CLI subcommand and persist the value like your JWT secret. **Never rotate** — rotating invalidates every user's envelope (they'd all need to reset their passphrase). | +| `OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB` | `262144` | Client-side Argon2id memory cost in KiB (256 MiB). Runs on the user's device during OPAQUE login/registration, not on the server. Distinct from `OXICLOUD_HASH_MEMORY_COST` (server-side legacy path). Higher values slow brute-force after a hypothetical envelope leak but also slow login on the user's device. | +| `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS` | `3` | Client-side Argon2id iteration count. | +| `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM` | `4` | Client-side Argon2id parallelism lanes. | ### Rate Limiting & Account Lockout diff --git a/example.env b/example.env index 47bfba8c..cf98f645 100644 --- a/example.env +++ b/example.env @@ -188,15 +188,13 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/oxicloud # Parallelism lanes (default: 2) #OXICLOUD_HASH_PARALLELISM=2 -# ----------------------------------------------------------------------------- -# OPAQUE aPAKE (zero-knowledge password login, RFC 9807) -# ----------------------------------------------------------------------------- +# --- OPAQUE aPAKE (zero-knowledge password login, RFC 9807) ------------------ # OPAQUE replaces `POST /api/auth/login` with a zero-knowledge exchange: # the passphrase never leaves the client, not on registration and not on # login. This is the substrate for later E2EE work. # # Phase 0 (this build) ships the primitives only — endpoints are inert -# until `OXICLOUD_OPAQUE_MODE` is set. Leave everything commented for a +# until `OXICLOUD_AUTH_OPAQUE_MODE` is set. Leave everything commented for a # no-op install; OIDC-only and magic-link-only deployments never need to # touch OPAQUE at all (see the effective-mode downgrade below). @@ -208,12 +206,12 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/oxicloud # is disabled via OXICLOUD_AUTH_METHODS (OPAQUE has nothing to shadow) — # an audit-channel log line explains why. So enabling this without # password in OXICLOUD_AUTH_METHODS is a no-op, not a boot error. -#OXICLOUD_OPAQUE_MODE=off +#OXICLOUD_AUTH_OPAQUE_MODE=off # Persistent OPAQUE server keypair (base64-encoded ServerSetup blob). # Generated ONCE per deployment; rotating this invalidates every user's # registration (they'd all be forced to re-register on next login). Only -# required when `OXICLOUD_OPAQUE_MODE != off` AND password auth is +# required when `OXICLOUD_AUTH_OPAQUE_MODE != off` AND password auth is # enabled — otherwise the value is ignored. # # Generate on first-time enable: @@ -225,7 +223,7 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/oxicloud # pipelines capture cleanly). Paste the printed line into your env or # secrets manager. NEVER regenerate — treat it like your JWT secret; # losing it forces every user to reset their passphrase. -#OXICLOUD_OPAQUE_SERVER_SETUP= +#OXICLOUD_AUTH_OPAQUE_SERVER_SETUP= # Client-side Argon2id key-stretching parameters (RFC 9807 KSF). # These run on the USER'S DEVICE during OPAQUE login/registration — @@ -238,11 +236,11 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/oxicloud # the user's next password change. # # Memory cost in KiB (default: 262144 = 256 MiB) -#OXICLOUD_OPAQUE_KSF_MEMORY_KIB=262144 +#OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB=262144 # Iterations (default: 3) -#OXICLOUD_OPAQUE_KSF_ITERATIONS=3 +#OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS=3 # Parallelism lanes (default: 4) -#OXICLOUD_OPAQUE_KSF_PARALLELISM=4 +#OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM=4 # ----------------------------------------------------------------------------- # RATE LIMITING & ACCOUNT LOCKOUT diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index daa388ee..394f9a36 100644 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -181,7 +181,7 @@ pub struct AuthApplicationService { /// `AuthConfig::require_verified_email`. require_verified_email: bool, /// OPAQUE envelope repo — populated when the OPAQUE substrate is - /// wired (`OXICLOUD_OPAQUE_MODE != off`). `login()` consults it to + /// wired (`OXICLOUD_AUTH_OPAQUE_MODE != off`). `login()` consults it to /// enforce the Phase 4 gate: once a user has completed at least /// one successful OPAQUE handshake (`opaque_migrated_at IS NOT /// NULL`), legacy `POST /api/auth/login` is refused for that @@ -366,7 +366,7 @@ impl AuthApplicationService { } /// Wire the OPAQUE envelope repo. Called by the DI factory when the - /// OPAQUE substrate is configured (`OXICLOUD_OPAQUE_MODE != off`). + /// OPAQUE substrate is configured (`OXICLOUD_AUTH_OPAQUE_MODE != off`). /// Enables the Phase 4 legacy-login gate — see the field docstring. pub fn with_opaque_repo( mut self, diff --git a/src/bin/opaque-setup.rs b/src/bin/opaque-setup.rs index ade94186..0c4e99e4 100644 --- a/src/bin/opaque-setup.rs +++ b/src/bin/opaque-setup.rs @@ -1,7 +1,7 @@ //! `opaque-setup` — one-shot operator helper that mints a fresh //! [`opaque_ke::ServerSetup`] and prints its base64 encoding to stdout. //! -//! The output goes into `OXICLOUD_OPAQUE_SERVER_SETUP` (env var or secrets +//! The output goes into `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` (env var or secrets //! manager) and MUST be persisted verbatim. Rotating it invalidates every //! user's registration — treat it like the JWT secret, only more so. //! @@ -9,7 +9,7 @@ //! ```text //! cargo run --bin opaque-setup > opaque_setup.b64 //! # or paste directly into your env / .env file: -//! echo "OXICLOUD_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)" >> .env +//! echo "OXICLOUD_AUTH_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)" >> .env //! ``` //! //! The generated value is a small (~64 byte) Ristretto255 keypair @@ -22,13 +22,13 @@ use oxicloud::infrastructure::services::opaque_service::OpaqueService; fn main() { let b64 = OpaqueService::generate_server_setup_b64(); // Print JUST the value — no trailing newline commentary — so shell - // pipelines (`OXICLOUD_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)`) + // pipelines (`OXICLOUD_AUTH_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)`) // capture cleanly without needing `tr -d '\n'` afterwards. println!("{b64}"); // Guidance goes to stderr so it doesn't contaminate the pipeline. eprintln!(); eprintln!("=== OPAQUE server setup generated. ==="); - eprintln!("Persist the line above in OXICLOUD_OPAQUE_SERVER_SETUP."); + eprintln!("Persist the line above in OXICLOUD_AUTH_OPAQUE_SERVER_SETUP."); eprintln!("NEVER rotate: rotating invalidates every user's registration."); eprintln!("Treat this value like your JWT secret."); } diff --git a/src/common/config.rs b/src/common/config.rs index a3cec2b9..9a01fc8f 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -1667,17 +1667,17 @@ pub struct OpaqueConfig { /// [`crate::infrastructure::services::opaque_service::OpaqueMode`] /// for the state-machine and the phase-plan mapping. /// - /// Env: `OXICLOUD_OPAQUE_MODE` (`off` | `migrate` | `opaque_only`). + /// Env: `OXICLOUD_AUTH_OPAQUE_MODE` (`off` | `migrate` | `opaque_only`). /// Default: `off`. pub mode: crate::infrastructure::services::opaque_service::OpaqueMode, /// Base64-encoded [`opaque_ke::ServerSetup`] blob. Generated once /// per deployment and persisted verbatim — rotating this invalidates /// every user's registration. Runbook: on first boot with - /// `OXICLOUD_OPAQUE_MODE != off`, if this is unset, print a fatal + /// `OXICLOUD_AUTH_OPAQUE_MODE != off`, if this is unset, print a fatal /// message with a fresh setup for the operator to paste into their /// env, then exit. /// - /// Env: `OXICLOUD_OPAQUE_SERVER_SETUP`. No default. + /// Env: `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`. No default. pub server_setup_b64: Option, /// Ciphersuite version stamped into `auth.users.opaque_ciphersuite_version` /// on registration. Bumping this without changing the actual @@ -1690,15 +1690,15 @@ pub struct OpaqueConfig { /// the client can construct a matching `argon2::Argon2` before /// running `ClientRegistration::start` / `ClientLogin::start`. /// - /// Env: `OXICLOUD_OPAQUE_KSF_MEMORY_KIB`. Default: `262144` (256 MiB). + /// Env: `OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB`. Default: `262144` (256 MiB). pub ksf_memory_kib: u32, /// Client-side Argon2id iteration count. /// - /// Env: `OXICLOUD_OPAQUE_KSF_ITERATIONS`. Default: `3`. + /// Env: `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS`. Default: `3`. pub ksf_iterations: u32, /// Client-side Argon2id parallelism (lanes). /// - /// Env: `OXICLOUD_OPAQUE_KSF_PARALLELISM`. Default: `4`. + /// Env: `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM`. Default: `4`. pub ksf_parallelism: u32, } @@ -1723,33 +1723,33 @@ impl OpaqueConfig { pub fn from_env() -> Self { use std::env; let mut cfg = Self::default(); - if let Ok(v) = env::var("OXICLOUD_OPAQUE_MODE") { + if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_MODE") { match crate::infrastructure::services::opaque_service::OpaqueMode::parse(&v) { Some(m) => cfg.mode = m, None => { tracing::warn!( target: "oxicloud::config", value = %v, - "OXICLOUD_OPAQUE_MODE has an unrecognised value — keeping default (off). \ + "OXICLOUD_AUTH_OPAQUE_MODE has an unrecognised value — keeping default (off). \ Accepted: off | migrate | opaque_only" ); } } } - if let Ok(v) = env::var("OXICLOUD_OPAQUE_SERVER_SETUP") { + if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_SERVER_SETUP") { cfg.server_setup_b64 = Some(v); } - if let Ok(v) = env::var("OXICLOUD_OPAQUE_KSF_MEMORY_KIB") + if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB") && let Ok(n) = v.parse::() { cfg.ksf_memory_kib = n; } - if let Ok(v) = env::var("OXICLOUD_OPAQUE_KSF_ITERATIONS") + if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS") && let Ok(n) = v.parse::() { cfg.ksf_iterations = n; } - if let Ok(v) = env::var("OXICLOUD_OPAQUE_KSF_PARALLELISM") + if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM") && let Ok(n) = v.parse::() { cfg.ksf_parallelism = n; @@ -1764,7 +1764,7 @@ impl OpaqueConfig { /// OIDC-only or magic-link-only (`OXICLOUD_AUTH_METHODS=oidc` or /// `=magic_link`) has no password path for OPAQUE to shadow; any /// non-`Off` mode would be a no-op that still nagged them for - /// `OXICLOUD_OPAQUE_SERVER_SETUP` at boot. + /// `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` at boot. /// /// This helper resolves the misconfig quietly: if password isn't in /// the allowlist AND OPAQUE mode is non-`Off`, we downgrade to `Off` @@ -1787,7 +1787,7 @@ impl OpaqueConfig { event = "opaque.mode_downgraded", reason = "password_auth_disabled", configured_mode = ?self.mode, - "OXICLOUD_OPAQUE_MODE is configured but password auth is disabled \ + "OXICLOUD_AUTH_OPAQUE_MODE is configured but password auth is disabled \ via OXICLOUD_AUTH_METHODS — treating OPAQUE as off. \ OPAQUE only replaces the password login path; enable password \ in OXICLOUD_AUTH_METHODS to make this setting take effect." @@ -2490,7 +2490,7 @@ pub struct AppConfig { pub auth: AuthConfig, /// OPAQUE (RFC 9807) zero-knowledge password auth configuration. /// Substrate only in Phase 0 — endpoints are inert until - /// `OXICLOUD_OPAQUE_MODE != off`. + /// `OXICLOUD_AUTH_OPAQUE_MODE != off`. pub opaque: OpaqueConfig, /// Feature configuration pub features: FeaturesConfig, @@ -4332,7 +4332,7 @@ mod tests { // // OPAQUE is fundamentally a password mechanism; enabling its mode when // password auth is disabled would be a no-op that still nagged - // operators for `OXICLOUD_OPAQUE_SERVER_SETUP` at boot. The + // operators for `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` at boot. The // `effective_mode` helper resolves that quietly by downgrading to // Off + emitting an audit log, and these tests pin the truth table. diff --git a/src/common/di.rs b/src/common/di.rs index aa5f63c4..f57c805b 100644 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -1936,7 +1936,7 @@ impl AppServiceFactory { // `OXICLOUD_AUTH_METHODS` (password must be enabled for OPAQUE to // have anything to shadow), so OIDC-only / magic-link-only // deployments transparently get `opaque_service = None` even if - // the operator accidentally set `OXICLOUD_OPAQUE_MODE=migrate`. + // the operator accidentally set `OXICLOUD_AUTH_OPAQUE_MODE=migrate`. // // Failing here (missing SERVER_SETUP, malformed base64, ciphersuite // drift) refuses server boot — same fail-closed posture as the @@ -1957,9 +1957,9 @@ impl AppServiceFactory { DomainError::internal_error( "OpaqueInit", format!( - "OXICLOUD_OPAQUE_MODE={:?} but the OPAQUE service failed: {}. \ - Persist a valid OXICLOUD_OPAQUE_SERVER_SETUP or set \ - OXICLOUD_OPAQUE_MODE=off. Refusing to start.", + "OXICLOUD_AUTH_OPAQUE_MODE={:?} but the OPAQUE service failed: {}. \ + Persist a valid OXICLOUD_AUTH_OPAQUE_SERVER_SETUP or set \ + OXICLOUD_AUTH_OPAQUE_MODE=off. Refusing to start.", effective, e ), ) @@ -2850,9 +2850,9 @@ pub struct AppState { pub auth_service: Option, /// OPAQUE aPAKE substrate (RFC 9807). Populated only when /// [`OpaqueConfig::effective_mode`] is not `Off` — that method - /// cross-checks `OXICLOUD_OPAQUE_MODE` against + /// cross-checks `OXICLOUD_AUTH_OPAQUE_MODE` against /// `OXICLOUD_AUTH_METHODS` so an OIDC-only or magic-link-only - /// deployment gets `None` here even if `OXICLOUD_OPAQUE_MODE` was + /// deployment gets `None` here even if `OXICLOUD_AUTH_OPAQUE_MODE` was /// set (with an audit-channel INFO explaining why). `None` also /// means the future OPAQUE endpoints must 404 — a handler that /// unwraps this without a nil check would break the phase gate. diff --git a/src/infrastructure/repositories/pg/opaque_pg_repository.rs b/src/infrastructure/repositories/pg/opaque_pg_repository.rs index ec18b073..724de89c 100644 --- a/src/infrastructure/repositories/pg/opaque_pg_repository.rs +++ b/src/infrastructure/repositories/pg/opaque_pg_repository.rs @@ -387,7 +387,7 @@ mod integration_tests { .await; // Fresh server setup for this test only — mirrors what the - // DI factory would load from OXICLOUD_OPAQUE_SERVER_SETUP. + // DI factory would load from OXICLOUD_AUTH_OPAQUE_SERVER_SETUP. let mut server_rng = OsRng; let server_setup = ServerSetup::::new(&mut server_rng); diff --git a/src/infrastructure/services/opaque_service.rs b/src/infrastructure/services/opaque_service.rs index 8624e7e3..e15f1bf7 100644 --- a/src/infrastructure/services/opaque_service.rs +++ b/src/infrastructure/services/opaque_service.rs @@ -72,10 +72,10 @@ pub struct OpaqueService { impl OpaqueService { /// Build the service from runtime config. Expects the operator to have - /// persisted the server setup already (via `OXICLOUD_OPAQUE_SERVER_SETUP`); + /// persisted the server setup already (via `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`); /// call [`OpaqueService::generate_server_setup_b64`] first-time and print /// the value for the operator to paste into their env before enabling - /// `OXICLOUD_OPAQUE_MODE`. + /// `OXICLOUD_AUTH_OPAQUE_MODE`. /// /// Rejects with `InternalError` if the setup is missing / malformed, or /// with `AccessDenied` if the mode is `off` (guarding against @@ -84,14 +84,14 @@ impl OpaqueService { if config.mode == OpaqueMode::Off { return Err(DomainError::access_denied( "opaque", - "OPAQUE is disabled (OXICLOUD_OPAQUE_MODE=off)", + "OPAQUE is disabled (OXICLOUD_AUTH_OPAQUE_MODE=off)", )); } let setup_b64 = config.server_setup_b64.as_deref().ok_or_else(|| { DomainError::new( ErrorKind::InternalError, "opaque", - "OXICLOUD_OPAQUE_SERVER_SETUP is required when OPAQUE is enabled — \ + "OXICLOUD_AUTH_OPAQUE_SERVER_SETUP is required when OPAQUE is enabled — \ generate one with `oxicloud opaque-setup` and persist it in the env", ) })?; @@ -141,7 +141,7 @@ impl OpaqueService { /// Generate a fresh server setup and return it as base64. Called once /// per deployment; the returned string must be persisted in - /// `OXICLOUD_OPAQUE_SERVER_SETUP` and NEVER rotated (rotating + /// `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` and NEVER rotated (rotating /// invalidates every existing envelope — see /// `docs/plan/opaque.md` §Phase 0). pub fn generate_server_setup_b64() -> String { @@ -160,7 +160,7 @@ fn decode_server_setup(b64: &str) -> Result, DomainEr DomainError::new( ErrorKind::InternalError, "opaque", - format!("OXICLOUD_OPAQUE_SERVER_SETUP is not valid base64: {e}"), + format!("OXICLOUD_AUTH_OPAQUE_SERVER_SETUP is not valid base64: {e}"), ) })?; ServerSetup::::deserialize(&bytes).map_err(|e| { @@ -168,7 +168,7 @@ fn decode_server_setup(b64: &str) -> Result, DomainEr ErrorKind::InternalError, "opaque", format!( - "OXICLOUD_OPAQUE_SERVER_SETUP payload does not match ciphersuite v1: {e}. \ + "OXICLOUD_AUTH_OPAQUE_SERVER_SETUP payload does not match ciphersuite v1: {e}. \ If you rotated the ciphersuite, every user must re-register." ), ) @@ -256,7 +256,7 @@ mod tests { }; let err = OpaqueService::from_config(cfg).expect_err("must reject missing setup"); assert_eq!(err.kind, ErrorKind::InternalError); - assert!(err.to_string().contains("OXICLOUD_OPAQUE_SERVER_SETUP")); + assert!(err.to_string().contains("OXICLOUD_AUTH_OPAQUE_SERVER_SETUP")); } #[test] diff --git a/tests/api/opaque_substrate.hurl b/tests/api/opaque_substrate.hurl index 1d17cd94..3d714958 100644 --- a/tests/api/opaque_substrate.hurl +++ b/tests/api/opaque_substrate.hurl @@ -12,7 +12,7 @@ # 1. The server booted with the OPAQUE substrate loaded — proved # transitively by the fact that this suite reached the # `--test-report` stage at all. `tests/common/server.env` sets -# `OXICLOUD_OPAQUE_MODE=migrate` + a persisted `SERVER_SETUP`; +# `OXICLOUD_AUTH_OPAQUE_MODE=migrate` + a persisted `SERVER_SETUP`; # a boot failure (bad base64, missing setup, ciphersuite drift) # would 500 every request or refuse to bind the port. # @@ -191,7 +191,7 @@ jsonpath "$.error_type" == "InvalidCredentials" # ───────────────────────────────────────────────────────────── # Case 9 — Params publish returns enabled=true under the test -# env (`OXICLOUD_OPAQUE_MODE=migrate`), the current +# env (`OXICLOUD_AUTH_OPAQUE_MODE=migrate`), the current # ciphersuite version (1 — see `docs/config/env.md`), # and the fast test-only KSF params # (memoryKib=8 / iter=1 / lanes=1 from server.env). diff --git a/tests/common/server.env b/tests/common/server.env index f5f8cc7e..3fa08af1 100644 --- a/tests/common/server.env +++ b/tests/common/server.env @@ -136,12 +136,12 @@ OXICLOUD_TRUST_PROXY_CIDR=0.0.0.0/0 # test env — real deployments call `opaque-setup` and paste the output. # Never reuse this value outside CI. Regenerate any time with: # cargo run --bin opaque-setup -OXICLOUD_OPAQUE_MODE=migrate -OXICLOUD_OPAQUE_SERVER_SETUP="ZY4hAGa1MNyE7Ht+8ksLcyMmi/K2iJvxQly+DdfllUxjiH0+CjCt4hG6+9Y68jGet2L213dV0hajCbr4fXnekkWtUxqLr+butVHEksZ9NJRuZTvS6SMC73yf/yku4WUHT1NSRB2yHurAFmYn75D9wdA1VaXTuwgO/u5i1pvcsQs=" +OXICLOUD_AUTH_OPAQUE_MODE=migrate +OXICLOUD_AUTH_OPAQUE_SERVER_SETUP="ZY4hAGa1MNyE7Ht+8ksLcyMmi/K2iJvxQly+DdfllUxjiH0+CjCt4hG6+9Y68jGet2L213dV0hajCbr4fXnekkWtUxqLr+butVHEksZ9NJRuZTvS6SMC73yf/yku4WUHT1NSRB2yHurAFmYn75D9wdA1VaXTuwgO/u5i1pvcsQs=" # Fast Argon2id — CI machines are underpowered vs production (256 MiB # default would drag every test-scaffold future OPAQUE handshake in # Phase 1+). Matches the params used in the round-trip unit test. -OXICLOUD_OPAQUE_KSF_MEMORY_KIB=8 -OXICLOUD_OPAQUE_KSF_ITERATIONS=1 -OXICLOUD_OPAQUE_KSF_PARALLELISM=1 +OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB=8 +OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS=1 +OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM=1 diff --git a/tests/e2e/fixtures/oxicloud-stack.ts b/tests/e2e/fixtures/oxicloud-stack.ts index 70b509d9..9f891520 100644 --- a/tests/e2e/fixtures/oxicloud-stack.ts +++ b/tests/e2e/fixtures/oxicloud-stack.ts @@ -85,8 +85,8 @@ export async function startStack(): Promise { // OPAQUE substrate off for the container E2E — same rationale as the // playwright.config.ts / playwright.coverage.config.ts overrides. // The Hurl suite exercises OPAQUE via server.env directly. - OXICLOUD_OPAQUE_MODE: 'off', - OXICLOUD_OPAQUE_SERVER_SETUP: '', + OXICLOUD_AUTH_OPAQUE_MODE: 'off', + OXICLOUD_AUTH_OPAQUE_SERVER_SETUP: '', }) // /health is a fast liveness probe (no DB hit); 200 means the HTTP server // is up and sqlx migrations have completed. diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 7faa242c..727561f6 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -71,8 +71,8 @@ export default defineConfig({ // need the boot-time init nor the ~200 KiB WASM client. Blanking // the inherited commonEnv values takes the DI factory's // `effective_mode == Off` short-circuit. - OXICLOUD_OPAQUE_MODE: 'off', - OXICLOUD_OPAQUE_SERVER_SETUP: '', + OXICLOUD_AUTH_OPAQUE_MODE: 'off', + OXICLOUD_AUTH_OPAQUE_SERVER_SETUP: '', }, }, }); diff --git a/tests/e2e/playwright.coverage.config.ts b/tests/e2e/playwright.coverage.config.ts index 9e58b10a..f25ffb5c 100644 --- a/tests/e2e/playwright.coverage.config.ts +++ b/tests/e2e/playwright.coverage.config.ts @@ -71,8 +71,8 @@ export default defineConfig({ // WASM bundle nor the boot-time substrate init. Blank the two OPAQUE // env vars inherited from commonEnv so the DI factory takes the // `effective_mode == Off` short-circuit path. - OXICLOUD_OPAQUE_MODE: 'off', - OXICLOUD_OPAQUE_SERVER_SETUP: '', + OXICLOUD_AUTH_OPAQUE_MODE: 'off', + OXICLOUD_AUTH_OPAQUE_SERVER_SETUP: '', }, }, });