feat(opaque): lower KSF values to support old clients (memory intensive)
This commit is contained in:
@@ -135,15 +135,46 @@ Password-using deployments will opt in via three env vars:
|
||||
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
|
||||
docker run --rm ghcr.io/atalayalabs/oxicloud:latest oxicloud-cli opaque setup
|
||||
|
||||
# From a source checkout:
|
||||
cargo run --bin opaque-setup
|
||||
cargo run --bin oxicloud-cli -- 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_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).
|
||||
Both print the base64 value on stdout (with guidance on stderr, so shell pipelines like `$(docker run ... oxicloud-cli opaque setup)` capture cleanly).
|
||||
3. **`OXICLOUD_AUTH_OPAQUE_KSF_*`** — client-side Argon2id key-stretching cost. Defaults (46 MiB / 1 iter / 1 lane) match OWASP's interactive-auth recommendation. See the next section for the rationale + when to bump.
|
||||
|
||||
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.
|
||||
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 be tuned independently. Tuning them together would force a bad compromise in one direction or the other.
|
||||
|
||||
### OPAQUE — KSF parameters
|
||||
|
||||
The **key-stretching function** (KSF) is Argon2id, applied to the user's passphrase before OPAQUE's OPRF step. It runs **client-side, inside a synchronous WASM call on the main thread, twice per login** (once each in OPAQUE's `start` and `finish`). Interactive login latency is roughly `2 × Argon2(memory, iterations)`. There is no server-side cost — the KSF exists solely to raise the price an attacker would pay to brute-force a passphrase from a hypothetically-stolen envelope.
|
||||
|
||||
**Defaults chosen: 46 MiB / 1 iteration / 1 lane.** This is OWASP's [Argon2 for interactive authentication](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) recommendation. We picked it over the library's higher suggestions because:
|
||||
|
||||
1. **Client-side execution means device-compatibility trumps peak resistance.** The KSF must fit inside the browser's WASM heap AND finish in a UX-tolerable window on the WORST device your users have — not just the fastest one. A 256 MiB memory cost is fine on an M1 desktop (~4 s per login) but on a 2015-era budget phone or a 4 GB Chromebook it either takes tens of seconds OR fails to allocate the WASM heap outright, locking those users out of the app entirely. 46 MiB stays well below iOS Safari's WASM caps and finishes in <300 ms even on old laptops.
|
||||
|
||||
2. **OPAQUE's whole design shifts the threat away from the KSF.** Unlike server-side password hashing where a database dump plus a fast KSF plus a common-password wordlist is a real threat, OPAQUE's envelope is *useless* without both the passphrase AND the server's static secret AND running the full aPAKE handshake. The KSF here isn't the primary defense — it's defense-in-depth for an attacker who somehow gets both the envelope AND the server's `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`. That's a compromised-server scenario where 46 MiB vs 256 MiB isn't the deciding factor.
|
||||
|
||||
3. **Modern passphrase entropy already outpaces KSF cost.** A random 12-character passphrase carries ~72 bits of entropy. Even at 46 MiB / 1 iter (~150 ms per Argon2 run on modern silicon), 2^72 guesses cost 2^72 × 150 ms ≈ 10^13 CPU-years. 5× more Argon2 doesn't change that being computationally infeasible.
|
||||
|
||||
Per-device login latency at the defaults:
|
||||
|
||||
| Device | ~Time per login |
|
||||
|---|---|
|
||||
| Apple M-series desktop | ~250 ms |
|
||||
| Modern Intel/AMD desktop | ~300 ms |
|
||||
| 2015-era Intel i5 laptop | ~1 s |
|
||||
| Modern mid-range Android/iOS phone | ~700 ms |
|
||||
| 2015-era budget Android / old iPad | ~2-3 s |
|
||||
| Chromebook (low-end, 4 GB RAM) | ~1-2 s |
|
||||
|
||||
**When to bump the defaults higher:**
|
||||
- You run OPAQUE against a threat model where a full server compromise (envelope + `SERVER_SETUP` both leaked) is a realistic scenario, AND your users' passphrases are weak (short, common-word, reused), AND your user base is on modern hardware only. Then a 4× memory bump multiplies attacker cost by 4× per guess.
|
||||
- Rule of thumb: `65536` KiB (64 MiB) is a reasonable middle ground for a modern-only user base; `262144` (256 MiB) is paranoid-tier and will lock out older devices.
|
||||
|
||||
**When to LOWER further:** don't — 46 MiB is already OWASP's floor for interactive auth. Below that, offline brute-force starts to become genuinely fast on GPU.
|
||||
|
||||
**Changing these values does NOT invalidate existing envelopes.** KSF params are baked into the envelope at register time and the SPA fetches them via `GET /api/auth/opaque/params` on each login. If you bump the config, existing users keep logging in with their old (cheaper) KSF; only *new* registrations use the new value. Silent-migration re-mints envelopes under the current KSF whenever a user changes their password. So you can dial up or down without disrupting live users — the change propagates organically over the next password rotation cycle.
|
||||
|
||||
### What OPAQUE does NOT touch
|
||||
|
||||
|
||||
+4
-4
@@ -57,10 +57,10 @@ OPAQUE (RFC 9807) is a zero-knowledge password-authenticated key exchange: the p
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `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. |
|
||||
| `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 `oxicloud-cli opaque setup` 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` | `47104` | Client-side Argon2id memory cost in KiB (46 MiB — matches OWASP interactive-auth recommendation). Runs on the user's device during OPAQUE login/registration, TWICE per login. Distinct from `OXICLOUD_HASH_MEMORY_COST` (server-side legacy path). Bumping raises brute-force cost after a hypothetical envelope leak but also raises login latency and risks WASM heap OOM on low-memory devices — see `authentication.md § OPAQUE — KSF parameters` for the full rationale + per-device latency table. |
|
||||
| `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS` | `1` | Client-side Argon2id iteration count (OWASP interactive-auth recommendation). |
|
||||
| `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM` | `1` | Client-side Argon2id parallelism lanes (OWASP recommendation). Higher only helps on multi-core hardware and can hurt single-core / older mobile devices. |
|
||||
|
||||
### Rate Limiting & Account Lockout
|
||||
|
||||
|
||||
+23
-15
@@ -216,9 +216,9 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/oxicloud
|
||||
#
|
||||
# Generate on first-time enable:
|
||||
# # Docker (recommended for production):
|
||||
# docker run --rm ghcr.io/atalayalabs/oxicloud:latest opaque-setup
|
||||
# docker run --rm ghcr.io/atalayalabs/oxicloud:latest oxicloud-cli opaque setup
|
||||
# # Or from a source checkout:
|
||||
# cargo run --bin opaque-setup
|
||||
# cargo run --bin oxicloud-cli -- opaque setup
|
||||
# Both print the base64 value on stdout (guidance on stderr, so shell
|
||||
# pipelines capture cleanly). Paste the printed line into your env or
|
||||
# secrets manager. NEVER regenerate — treat it like your JWT secret;
|
||||
@@ -226,21 +226,29 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/oxicloud
|
||||
#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 —
|
||||
# distinct from OXICLOUD_HASH_* which runs on the server for the legacy
|
||||
# password path. Client-side execution means we can afford higher memory
|
||||
# than the server would (each user pays once for themselves rather than
|
||||
# the server paying for every concurrent login).
|
||||
# These run on the USER'S DEVICE during OPAQUE login/registration,
|
||||
# TWICE per login (once each in OPAQUE's `start` and `finish` steps),
|
||||
# on the main thread inside a synchronous WASM call. Interactive
|
||||
# login latency is roughly `2 × Argon2(memory, iterations)`.
|
||||
#
|
||||
# Bumping these does NOT affect existing envelopes; they'd re-mint on
|
||||
# the user's next password change.
|
||||
# Defaults match OWASP's Argon2id-for-interactive-auth guidance
|
||||
# (46 MiB / 1 iter / 1 lane) — chosen for compatibility with older
|
||||
# and lower-end devices where a heavier memory budget either takes
|
||||
# tens of seconds OR fails to allocate WASM heap outright. See
|
||||
# `docs/config/authentication.md § OPAQUE — KSF parameters` for the
|
||||
# full rationale and per-device latency table.
|
||||
#
|
||||
# Memory cost in KiB (default: 262144 = 256 MiB)
|
||||
#OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB=262144
|
||||
# Iterations (default: 3)
|
||||
#OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS=3
|
||||
# Parallelism lanes (default: 4)
|
||||
#OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM=4
|
||||
# Changing these does NOT invalidate existing envelopes — the KSF
|
||||
# params are baked in per-envelope at register time; silent-migration
|
||||
# re-mints under new params on the user's next password change.
|
||||
#
|
||||
# Memory cost in KiB (default: 47104 = 46 MiB, OWASP interactive)
|
||||
#OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB=47104
|
||||
# Iterations (default: 1, OWASP interactive)
|
||||
#OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS=1
|
||||
# Parallelism lanes (default: 1 — OWASP recommendation; higher only
|
||||
# helps on multi-core devices and hurts single-core / older mobile)
|
||||
#OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM=1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# RATE LIMITING & ACCOUNT LOCKOUT
|
||||
|
||||
+31
-6
@@ -1690,15 +1690,27 @@ pub struct OpaqueConfig {
|
||||
/// the client can construct a matching `argon2::Argon2` before
|
||||
/// running `ClientRegistration::start` / `ClientLogin::start`.
|
||||
///
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB`. Default: `262144` (256 MiB).
|
||||
/// **The KSF runs client-side, twice per login** (once each in
|
||||
/// OPAQUE's `start` and `finish` steps), inside a synchronous WASM
|
||||
/// call on the main thread. So the interactive login latency the
|
||||
/// user perceives is roughly `2 × Argon2(memory, iterations)`.
|
||||
///
|
||||
/// Default: `47104` KiB (46 MiB) — matches the OWASP recommendation
|
||||
/// for interactive password-based KDF. Rationale in
|
||||
/// `docs/config/authentication.md § OPAQUE — KSF parameters`.
|
||||
///
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB`.
|
||||
pub ksf_memory_kib: u32,
|
||||
/// Client-side Argon2id iteration count.
|
||||
///
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS`. Default: `3`.
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS`. Default: `1`
|
||||
/// (OWASP recommendation for interactive auth).
|
||||
pub ksf_iterations: u32,
|
||||
/// Client-side Argon2id parallelism (lanes).
|
||||
///
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM`. Default: `4`.
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM`. Default: `1`
|
||||
/// (OWASP recommendation). Higher values only help on multi-core
|
||||
/// hardware and hurt single-core / older mobile devices.
|
||||
pub ksf_parallelism: u32,
|
||||
}
|
||||
|
||||
@@ -1708,9 +1720,22 @@ impl Default for OpaqueConfig {
|
||||
mode: crate::infrastructure::services::opaque_service::OpaqueMode::Off,
|
||||
server_setup_b64: None,
|
||||
ciphersuite_version: 1,
|
||||
ksf_memory_kib: 262_144,
|
||||
ksf_iterations: 3,
|
||||
ksf_parallelism: 4,
|
||||
// OWASP recommended interactive-auth Argon2id parameters
|
||||
// (2024 password-storage cheat sheet): 46 MiB / 1 iter /
|
||||
// 1 lane. Keeps interactive login usable on older /
|
||||
// low-end / mobile devices where a heavier memory budget
|
||||
// either takes tens of seconds OR fails to allocate WASM
|
||||
// heap outright (iOS Safari + old Android WebView cap).
|
||||
// Full rationale in docs/config/authentication.md.
|
||||
//
|
||||
// Changing these values does NOT invalidate existing
|
||||
// envelopes — the KSF params are effectively baked into
|
||||
// the envelope at register time. Silent-migration
|
||||
// re-mints under the current params on the user's next
|
||||
// password change.
|
||||
ksf_memory_kib: 47_104,
|
||||
ksf_iterations: 1,
|
||||
ksf_parallelism: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user