feat(opaque): lower KSF values to support old clients (memory intensive)

This commit is contained in:
Edouard Vanbelle
2026-08-05 20:48:55 +02:00
parent c95d108bf8
commit c4bf2568ba
4 changed files with 94 additions and 30 deletions
+36 -5
View File
@@ -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
View File
@@ -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