@shieldfive/crypto
Post-quantum-hybrid client-side encryption with a self-describing, cipher-suite-agile on-disk format. The cryptographic core of ShieldFive, released as a standalone library.
Status & honest scope
This library is beta. The wire format is frozen for the v1 release; the public TypeScript API may make minor adjustments before v1.0.0 stable. It has not undergone external cryptographic audit. Until it does, treat this library as a serious work-in-progress rather than a finished product.
The design choices documented in spec/ are deliberate and
reviewable. The implementation is covered by 188 passing tests including
truncation, reordering, splice, and tampering detection across all four
suites. That is enough for internal dogfooding. It is not enough to
claim "most secure crypto library" — that claim requires external review
this library has not yet received.
When this library says it avoids a parallel cryptographic implementation,
that scope is the file-content cipher suites. The keyring/envelope layer
that wraps content keys is built directly on WebCrypto AES-GCM and HMAC
(via crypto.subtle), not on a second hand-rolled implementation.
Design positions
This library makes specific design choices that differ from existing open-source client-side encryption libraries in the encrypted-cloud-storage space. The table below summarizes those positions; each row links to the spec section that documents the choice.
| Property | This library | Proton OpenPGP.js | Internxt SDK | MEGA SDK |
|---|---|---|---|---|
| Post-quantum hybrid by default | ||||
| ML-KEM-1024 (NIST PQC level 5) | (Kyber-512) | |||
| Self-describing on-disk file format | ||||
| Cipher-suite agile (one byte selects) | ||||
| Truncation detection bound to AEAD | ||||
| Cross-file splice prevention (file_id via HKDF salt) | ||||
| Streaming AEAD with random-access chunks | ||||
| WebCrypto-only path (no WASM required) | ||||
| Apache 2.0 (patent grant) | (LGPL) |
= partial or feature-flagged. See spec/threat-model.md
for the comparison methodology and source citations. Proton OpenPGP.js
added optional post-quantum (ML-KEM + ECC hybrid) support via OpenPGP v6
in May 2026 (proton.me/blog/introducing-post-quantum-encryption); it is
opt-in, not default — hence rather than in the table. Comparison
cells reflect publicly available specifications and audit reports as of
2026-05-18 and will be updated as upstreams change.
Quick start
npm install @shieldfive/crypto
# Optional, only if you use suites 0x02 or 0x03:
npm install libsodium-wrappers-sumo
Encrypt a file with the post-quantum hybrid suite (default)
import {
encryptBlob,
decryptBlob,
generateMlKemKeypair,
} from '@shieldfive/crypto/pq-hybrid-v1'
// Recipient generates a long-lived ML-KEM-1024 keypair (typically derived
// from their master secret — see deriveMlKemKeypair).
const { publicKey, secretKey } = generateMlKemKeypair()
// A classical envelope key (e.g. the user's vault key, 32 bytes).
const envelopeKey = crypto.getRandomValues(new Uint8Array(32))
// Encrypt
const file = new File([myData], 'document.pdf')
const result = await encryptBlob({
blob: file,
recipientPublicKey: publicKey,
envelopeKey,
})
// Upload result.blob to your storage backend. It is self-describing.
// Later, decrypt
const plaintext = await decryptBlob({
blob: result.blob,
recipientSecretKey: secretKey,
envelopeKey,
})
Encrypt with the WebCrypto-only suite (no WASM dependency)
import { encryptBlob, decryptBlob } from '@shieldfive/crypto/aes-gcm-v1'
const result = await encryptBlob({ blob: file })
// result.contentKey is your 32-byte fresh per-file key — store it wrapped
// under your envelope key.
const plaintext = await decryptBlob({
blob: result.blob,
contentKey: result.contentKey,
})
Auto-routing decryptor
If you accept files written by any v1 suite and don't know which in advance:
import { autoDecryptBlob } from '@shieldfive/crypto'
const plaintext = await autoDecryptBlob({
blob: encryptedBlob,
contentKey, // for aes-gcm-v1 / xchacha-v1
recipientSecretKey, // for pq-hybrid-v1
envelopeKey, // for pq-hybrid-v1
})
Reading legacy v0 files
The pre-v1 ShieldFive production format is supported read-only:
import { decryptV0 } from '@shieldfive/crypto/legacy-v0'
const plaintext = await decryptV0({
blob: legacyBlob,
contentKey,
noncePrefix, // 4 bytes, from the database
chunkSize, // 5 * 1024 * 1024 typically
})
There is no encryptV0. New writes must use a v1 suite.
Decrypting an exported ShieldFive backup
A ShieldFive user can export their account at any time and
decrypt the resulting bundle offline using this library plus the
small Node script that ships at
shieldfive.com/export. The
recipe walks the key hierarchy (master password → user key →
root key → folder keys → per-file content keys), re-derives the
ML-KEM-1024 secret for Suite 0x03 files deterministically from
the root key (per spec/key-derivation.md), and feeds each blob
into autoDecryptBlob here.
The recipe lives at shieldfive.com/export
and the script ships in this repository as
examples/decrypt-one.mjs, kept in
lock-step with the API surface; this library is the decryption
primitive the script depends on. If
you want to reproduce the path from first principles (with no
ShieldFive code in the loop), spec/format-v1.md and
spec/key-derivation.md together describe everything needed.
File format
The complete v1 wire format is documented in
spec/format-v1.md. The short version:
header := magic(5) = "SF5\x01\x00"
|| suite(1) = 0x01 | 0x02 | 0x03 | 0x04
|| flags(1) = 0x00 (reserved)
|| file_id(16) = random per-file
|| chunk_size(4) = uint32 BE
|| total_chunks(8) = uint64 BE
|| plaintext_size(8) = uint64 BE
|| suite_payload_len(2)
|| suite_payload(variable)
|| header_mac(32) = HMAC-SHA-256, keyed from content key
chunk_i := length_prefix(4) || AEAD-ciphertext-with-tag
Every chunk's AEAD authenticator binds the chunk index, total chunk count,
and final-chunk flag. The file_id is NOT part of the AAD; cross-file
splice resistance is structural, coming from the suite-specific chunk-key
and nonce-prefix derivations that mix file_id in as the HKDF salt/IKM.
Together these make truncation, reordering, and cross-file splicing
detectable by the AEAD rather than by application-layer hashes.
Cipher suites
| ID | Name | Default | Notes |
|---|---|---|---|
| 0x01 | aes-256-gcm-v1 |
WebCrypto-only, hardware-accelerated | |
| 0x02 | xchacha20-poly1305-v1 |
libsodium, 192-bit nonces | |
| 0x03 | pq-hybrid-xchacha-mlkem1024-v1 |
XChaCha20-Poly1305 + ML-KEM-1024 hybrid | |
| 0x04 | aes-256-gcm-v2 |
WebCrypto-only, 8-byte nonce prefix |
Suite 0x04 is the current AES-GCM write path: identical to 0x01 except
the 12-byte GCM IV uses an 8-byte file-derived nonce prefix and a 4-byte
counter (rather than 4 + 8), widening the cross-file IV-collision margin.
0x01 remains defined for reading files written before 0x04 existed.
See spec/format-v1.md for the per-suite derivations.
ML-KEM-1024 is FIPS 203 (NIST PQC standard, security level 5). Files encrypted with suite 0x03 remain confidential against an adversary who breaks either the classical or the post-quantum primitive — only a break of both compromises the file.
Threat model
Documented at length in spec/threat-model.md. The
short summary: this library protects file confidentiality and integrity
against an honest-but-curious server, an active network adversary, and a
"harvest now, decrypt later" quantum adversary. It does not protect
against malicious client delivery, endpoint compromise, side channels in
the host crypto runtime, or metadata leakage at the storage layer. Those
are addressed by the host application, not the crypto layer.
Performance
Benchmarks live in bench/throughput.ts. Run them on your hardware:
npx tsx bench/throughput.ts
Throughput depends heavily on CPU model, AES-NI availability, and Node version. The PQ KEM is a constant per-file cost (~ML-KEM-1024 keygen + encapsulation latency), independent of file size. Streaming chunk throughput tracks the underlying classical AEAD.
We do not publish reference numbers here because numbers without source hardware are misleading; numbers with source hardware get cherry-picked. Run the benchmark on your target hardware and report what you see.
Building from source
npm install
npm test
npm run build # emits dist/
Requires Node 20+.
Status
Beta. The wire format is frozen for the v1 release, but the public TypeScript API may make small adjustments before 1.0.0 stable. Test coverage is comprehensive (188 tests, all four suites, all architectural guarantees verified). A formal third-party security audit is planned for the 1.0.0 stable milestone — until that audit lands, treat this library as a serious work-in-progress rather than a finished product.
License
Apache 2.0. The Apache 2.0 patent grant is important for a crypto library; we chose it over MIT for that reason.
Reporting vulnerabilities
See SECURITY.md. Coordinate with us at
security@shieldfive.com (see SECURITY.md for the encrypted-channel
process). 72-hour acknowledgement,
30-day target patch window for high-severity issues. Researchers acting
in good faith are protected under the safe-harbor clause.
Acknowledgements
- @noble/post-quantum
by Paul Miller — clean, minimal ML-KEM-1024 (FIPS 203)
implementation. ShieldFive pins
^0.6.0, which resolves to the 0.6.1 release. Upstream completed a maintainer self-audit at 0.6.1 in April 2026 (see the "Security" section of the upstream README) and has not been independently audited by an external firm. ShieldFive's PQ-hybrid construction on top of the library — the HKDF-SHA-256 combiner that mixes the classical and ML-KEM shares — has also not received an independent external audit. A third-party audit of the ShieldFive layer is planned for the v1.0.0 stable milestone. - libsodium — XChaCha20-Poly1305 and XSalsa20-Poly1305 secretbox.
- The NIST PQC competition and FIPS 203 authors.
— Cho Garcia, maintainer