Build log ·

Phase 3 shipped — post-quantum hybrid is the default

Phase 3 made ML-KEM-1024 plus XChaCha20-Poly1305 the default cipher suite for new uploads — one byte changes on disk, no file is rewritten, and the Suite 0x01 dispatch path stays untouched. Rollback is a single env-var flip back to the previous default, not re-encryption.

Written by Cho García

Phase 3 made ML-KEM-1024 plus XChaCha20-Poly1305 the default cipher suite for new uploads as of 2026-05-17. The technical change on disk is a one-byte difference in the v1 header: the suite identifier goes from 0x01 to 0x03. Behind that byte is the post-quantum hybrid construction documented at crypto/spec/format-v1.md. Files encrypted under the previous default (Suite 0x01, AES-256-GCM) stay readable indefinitely. Nothing was re-encrypted; no row on disk was rewritten; no user was migrated.

This is the third build-log entry. The first covered the Phase 1 library extraction; the second covered the Phase 2 v1 wire format that this Phase 3 change sits on. Phase 3 ran as a single combined rollout with one bake window — the Decision D1 framing recorded in reconciliation-2026-05-13.md — rather than as a five-stage rollout in the Phase 2 shape. The rationale was that zero users today means a staged design adds calendar without protecting anyone real, and the decoder-handles- both-suite-bytes invariant from Phase 2 already covers the flag-asymmetry property that matters for rollback.

Why post-quantum hybrid

The threat is harvest-now-decrypt-later. An adversary stores ciphertext today, decrypts it in N years once a cryptographically-relevant quantum computer exists. ML-KEM-1024 (NIST FIPS 203) is the standardized post-quantum KEM at NIST PQC security level 5. Combining it with XChaCha20-Poly1305, a classical AEAD whose primitives have been studied for decades, means a file is at least as strong as the stronger of the two: an adversary has to break both components, independently, to recover the plaintext. The combiner construction and the deterministic per-user ML-KEM keypair derivation live in src/suites/pq-hybrid-v1/ and src/identity/ on the cryptography library, and the threat model is at spec/threat-model.md.

What the observation window verified

Bake ran from 2026-05-14 to 2026-05-17. The window was deliberately short because ShieldFive had zero users at observation start — the seven-day calendar in the original plan was sized for the Phase 2 cadence when there was ambient dogfood traffic; here it would have been theatre. What we actually verified:

  • Cross-deployment compatibility (PR #179). Preview deployment with PHASE3_ENABLED=true encrypted Suite 0x03 files; production deployment with the flag unset decrypted them cleanly. Three cases ran by hand: owner self-encrypt + self-decrypt, share-link encrypt + share-recipient decrypt, Suite 0x01 regression. All green. This mirrors the Phase 2 Stage 3 asymmetry property: decoder lives downstream of the writer flag, so any Suite 0x03 file ever uploaded stays decryptable regardless of the flag's later state.
  • Ambient error-monitoring signal. Two issues fired during the window (SHIELDFIVE-21 and SHIELDFIVE-22), both from a single user on an international tunnel. Root cause was client-side network flakiness — Failed to fetch at the WHATWG layer and a downstream empty-body JSON parse — not Phase 3. Production was serving Suite 0x01 for this user during the bake since the writer flag was unset. Both issues resolved via PR #186, which also added surface-tagged error-monitoring capture plus a post-unlock setUser({ id }) call so future errors are user-attributable without breaking the zero-knowledge posture (id only — no email, no IP).
  • No traffic samples. Encrypt p95 latency, decrypt p95 latency, decrypt success rate, peak worker memory, vault-unlock regression — none of these carry measured numbers from the observation window because there was effectively no traffic to measure. The library's test suite ran the round-trip — the load-bearing 16 MiB multi-chunk test at tests/sfCryptoWorkerPq.test.ts — and confirmed correctness. Performance is back-of-envelope from Node benchmarks on a single core (~170 MB/s encrypt, ~310 MB/s decrypt) and will get real numbers post-launch when users arrive.

Rollback semantics remain as documented in docs/phase3-design.md § 10: if a defect surfaces, revert the default-suite-selector to Suite 0x01 for new uploads via one environment variable (NEXT_PUBLIC_PHASE3_ENABLED = false). Existing Suite 0x03 files keep their on-disk suite byte and decrypt correctly via the suite-byte dispatcher; the decoder is unchanged by the rollback. Forced re-encryption is never the answer — the plaintext lives on the user's machine, not the server.

What this doesn't fix yet

Three real limits worth naming up front. None of them are incident-class; all of them are documented elsewhere and worth restating here so a reader following the build-log doesn't have to re-derive them.

  • Anonymous share-link recipients are classical-only. Suite 0x03 protects the file owner against the harvest-now-decrypt- later threat. Share recipients receive the combined key K wrapped under the share password and decrypt using XChaCha20-Poly1305 directly; they never touch the ML-KEM material. The design and rationale are at docs/phase3-design.md § 5. Direct-to-account sharing, where both parties have ML-KEM keys, is planned but not in v1.
  • No external audit of the application yet. The internal review published on /security is by the founder, not by a third party. The external cryptography-library audit is the next external-trust milestone; it will be commissioned when resources allow. The deferral is a principle, not a constraint.
  • No ML-KEM key-rotation flow. The keypair is deterministically derived from the master secret per docs/phase3-design.md § 5 "Deterministic derivation from master secret"; rotating the ML-KEM keypair requires a master-secret rotation flow, which does not exist today and is post-launch.

Rollback semantics

Worth surfacing in case the question comes up. Rollback does NOT mean re-encryption. If a defect surfaces in Suite 0x03 after the flag flip, the action is to revert the writer-side default to Suite 0x01 by setting PHASE3_ENABLED = false on the production deployment — one environment variable, under sixty seconds. Existing Suite 0x03 files keep their on-disk suite byte and continue to decrypt correctly via the decoder's suite-byte dispatch; the decoder is unchanged by the rollback, only the writer-side default is. The decoder ships before the writer flag flips, and a Stage-3-equivalent cross-deployment test verifies the asymmetry in production — same property the Phase 2 Stage 3 test verified for Suite 0x01. The full rollback discipline is at docs/phase3-design.md § 10.

Forced re-encryption is never the right action under an incident. The plaintext lives on the user's machine, not the server, so system-driven re-encryption is impossible by construction; the only context in which re-encryption applies is an opt-in user- driven "upgrade my old files" flow, and that is a product feature, not an incident response.

What's next

A few threads, post-bake:

  • Direct-to-account sharing UI. The crypto library already provides identity.buildShareForRecipient and identity.openShareAsRecipient; the application UI to drive them is separable product work and not blocked by anything in the wire format.
  • ML-KEM key-rotation flow. Tied to master-secret rotation; no current opt-in path exists. A user-driven "upgrade my old files" flow that re-encrypts opt-in folders against the current default suite is the closest related capability and is also post-launch.
  • External audit of @shieldfive/crypto. Deferred and named on /security with the deferral rationale. The internal review is the baseline an external engagement starts from rather than rediscovers.

Links

Pre-publication checklist

  • Suite 0x03 observation window (2026-05-14 → 2026-05-17) closed. The 7-day calendar in docs/phase3-design.md § 9 was compressed to ambient-watch because the development account was the only traffic source; the body documents the framing.
  • All <!-- pending bake --> markers resolved and removed.
  • Observation window framing in body honestly documents what was verified (cross-deployment compatibility, ambient error-monitoring, library round-trip via tests/sfCryptoWorkerPq.test.ts) and the absence of measured p95 / success-rate / memory numbers (no traffic samples); real numbers will come post-launch.
  • Opening paragraph's "as of the bake-clear date" replaced with the actual flag-flip date (2026-05-17).
  • Post date in app/build-log/posts.ts is "May 2026" — still accurate; no month-boundary flip needed.
  • Step 15 row in reconciliation-2026-05-13.md flipped from "Drafted, finalizes post-bake" to "Complete" (this PR).