ShieldFive's cryptography moved from inline production code into a standalone open-source library, with the production deployment re-importing the library via a compatibility bridge. Wire format unchanged. Database unchanged. No user-visible change.
This post explains what shipped, why it shipped this way, and what comes next.
ShieldFive launched in February 2026. Phase 1 is the first build-log entry because it's the first piece of work for which a public engineering record made sense — earlier work was internal, with no surface to record against.
What shipped
Before Phase 1, the cryptography that encrypted files in production
was inline TypeScript inside the web application's worker bundle —
specifically a public/workers/sf-crypto-worker.js that implemented
AES-256-GCM chunked encryption against the v0 wire format
ShieldFive has used since launch. The file was 123 lines, coupled
to the rest of the application via postMessage.
After Phase 1, that file is two lines: an import of
migrationV0.installV0WorkerHandler from @shieldfive/crypto, and
a call to install it. The remaining 121 lines now live in the
open-source library, where they can be read, audited, and verified
by anyone. The library is published to npm as @shieldfive/crypto
(Apache 2.0, 1.0.0-alpha.3 at time of writing) and developed in
the open at
github.com/shieldfive/crypto.
The library's migrationV0 module is a compatibility layer: it
implements exactly the same wire format the inline worker did, so
files encrypted by the inline worker remain decryptable by the
library, and files encrypted by the library are byte-identical to
what the inline worker would have produced. The bridge exists
specifically so production could move to the library without
changing any bytes on disk and without requiring a coordinated
client-server cutover.
Why it shipped this way
Three principles drove the migration's shape.
The library has to be the same code that runs in production. The shortcut would have been to publish a "reference implementation" of the cryptography to GitHub for credibility purposes, while the production deployment kept running the original inline worker — nobody would have noticed for months, and the cleaner separation would have made the library easier to evolve in isolation. The shortcut was rejected because the credibility argument doesn't survive it. The argument is that anyone can read, audit, and verify the cryptography that protects their files; that argument requires the published code to be the production code, byte for byte. A "reference implementation" that production doesn't actually use is marketing.
Migrations to library-backed crypto must not change wire format. A library extraction touches the build process. A wire-format change touches every file ever uploaded. Bundling them into one deploy means a Phase 1 rollback — the kind triggered by a single weird Sentry alert at 11pm — also rolls back the v1 format work that was riding along, and you lose two phases of progress to one problem in either of them. Worse, if a wire-format bug surfaces weeks later, the rollback options narrow to "decrypt every v1 file and re-encrypt as v0 before reverting," which is not a procedure anyone wants to write under pressure. Phase 1 is the build-process change. Phase 2 is the wire-format change. They ship as separate deploys for separate rollback envelopes.
The bake window is real. Production cryptography migrations have a long tail of bugs that surface only under user behavior patterns nobody simulates beforehand — a Safari version on an iPad, a multi-gigabyte upload on a flaky home connection, a share link opened from a corporate VPN. Two days of post-deploy stability tells you almost nothing. Seven days of stability tells you something. The temptation, with a migration this carefully tested, is to compress the window to 48 hours and start the next phase early. The discipline is not to.
How it was verified
Phase 1's correctness gate is byte-identicality: for any input the old inline worker accepted, the new library-backed worker must produce output that's bit-for-bit identical. The migration shipped without incident in production, and that fact is itself the most informative part of the verification — if a byte-identical migration had any meaningful chance of going wrong, the byte-identicality gate would have surfaced it before merge.
Verification ran across file sizes from 1 KiB to 128 MiB, on Chrome and Safari, against the existing test vectors generated from the inline worker's output, and through a Vercel preview deployment that allowed authenticated users to upload a file via the new path and immediately download it via the production path (proving cross-build compatibility before the production cutover).
Phase 2's pre-Stage-0 gate ran shortly after the Phase 1 merge: a
streaming-memory characterization of the library's
createAesGcmV1EncryptStream, verifying that back-pressure works
correctly at the library level and that the streaming worker
design Phase 2 depends on is feasible. Results are documented in
the open Phase 2 design at
docs/phase2-design.md in the web repository.
What's next
Phase 2 introduces the v1 wire format on disk for new uploads. The
v1 format is self-describing (magic bytes plus version), uses
AAD-bound chunks for integrity, and is documented in
spec/format-v1.md
in the library repository. Existing v0 files keep working
indefinitely via the bridge that landed in Phase 1.
Phase 3, sometime after Phase 2 stabilizes, makes the post-quantum hybrid cipher suite (ML-KEM-1024 combined with XChaCha20-Poly1305 via HKDF) the default for new uploads.