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=trueencrypted 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 fetchat 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-unlocksetUser({ 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
/securityis 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.buildShareForRecipientandidentity.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/securitywith the deferral rationale. The internal review is the baseline an external engagement starts from rather than rediscovers.
Links
- Spec:
crypto/spec/format-v1.md. - Design:
docs/phase3-design.md. - Posture:
/security. - Source: github.com/shieldfive.
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.tsis "May 2026" — still accurate; no month-boundary flip needed. - Step 15 row in
reconciliation-2026-05-13.mdflipped from "Drafted, finalizes post-bake" to "Complete" (this PR).