Tearleads

Cipher suites

The cryptographic primitives and where each one is used.

Published from docs/cipher-suites.md (GitHub)

This document summarizes the cryptographic primitives used by Tearleads and where each is applied. It is a reference map, not a design spec — see keying-design.md and security-guarantees.md for how these fit together, and glossary.md for terminology.

Public-key operations in the application E2EE / keying protocol are post-quantum only (NIST FIPS 203 / 204): no X25519, Ed25519, RSA, ECDH, or NaCl. This scope is the client and API keying path — it does not cover deployment tooling, which uses conventional SSH host keys (Ed25519, and RSA/ECDSA in the Terraform stacks).

Primitives

Primitive Standard / level Role
ML-KEM-1024 (Kyber) FIPS 203, NIST L5 Key encapsulation (wrap keys to recipients)
ML-DSA-87 (Dilithium) FIPS 204, NIST L5 Signatures
AES-256-GCM WebCrypto AEAD Application payload encryption and key wrapping
HKDF-SHA-256 RFC 5869 Per-record and per-purpose key derivation
PBKDF2-SHA-256 Password KDF PIN, passphrase, and backup key derivation
SHA-256 Hash Fingerprints, canonical hashes, integrity
SHA-1 Legacy hash Deterministic v5-style ids only (non-security)
HMAC-SHA-256 MAC Deterministic slot ids, Stripe webhook verification
BIP39 (24-word) 256-bit mnemonic Identity seed phrase
ChaCha20 SQLite3MultipleCiphers At-rest database file encryption (active codec)

Where each is used

ML-KEM-1024 (key encapsulation)

Use Location
KEM envelope primitive: encapsulate, then AES-GCM-wrap a DEK to a recipient key packages/crypto/src/encapsulation/wrapDek.ts, packages/crypto/src/encapsulation/unwrapDek.ts, packages/crypto/src/encapsulation/decryptAsRecipient.ts
Wrap container KEK to a parent derived public key tearleads.container-kek-wrap.ml-kem-1024-aes-256-gcm-parent-kek — packages/crypto/src/keying/containerKekWrapping.ts, packages/client-sdk/src/data/containers/shared/projection.ts
Wrap container KEK to a user or managed principal packages/client-sdk/src/data/containers/shared/projection.ts
Wrap principal (group/org) secret keys to members packages/client-sdk/src/workflows/organizations/principalPolicy/, packages/client-sdk/src/workflows/organizations/groupPolicy/
Identity key-package probe envelope packages/client-sdk/src/client/identity/keyPackage.ts
Identity keypair generation packages/crypto/src/encapsulation/generateKeyPair.ts

Container-KEK-to-principal wrap suite: tearleads.container-kek-wrap.ml-kem-1024-aes-256-gcm. Document and blob content keys are not ML-KEM-wrapped directly to recipients — they wrap to container KEKs with AES-GCM (see below), and only the KEK is ML-KEM-wrapped to users, principals, and parent containers. Parent key pairs are derived from their KEKs using a domain-separated HKDF-SHA-256 seed; their public keys are committed by signed container events and manifests.

ML-DSA-87 (signatures)

Use Location
Sign principal (group/org) policy state packages/crypto/src/principalState.ts
Sign document/blob write headers packages/crypto/src/keying/writeHeader.ts
Sign access events (grant, revoke, link, bind, …) packages/crypto/src/keying/accessEvent.ts
Sign transparency tree heads packages/crypto/src/keying/transparency.ts
Sign auth challenges packages/crypto/src/challenge.ts
Sign / verify primitives packages/crypto/src/signing/sign.ts, packages/crypto/src/signing/verify.ts

AES-256-GCM (symmetric)

Use Suite / location
Document & blob content records (per-record HKDF key) aes-256-gcm-hkdf-sha256-record-key — packages/client-sdk/src/data/documents/shared/crypto.ts, packages/client-sdk/src/data/blobs/attachments/chunkedBlobCrypto.ts
Wrap content key to container KEK tearleads.{document,blob}.content-key-wrap.aes-256-gcm-container-kek — packages/crypto/src/contentKeyEnvelope.ts; AAD binds the suite, document or blob id, content-key epoch, container id and container key epoch id
Wrap predecessor container KEK to successor KEK tearleads.container-kek-wrap.aes-256-gcm-predecessor-kek — packages/crypto/src/keying/containerKekPredecessor.ts; suite and version are persisted per bridge row
Seal container KEK history keyring under current KEK tearleads.container-kek-keyring.aes-256-gcm-current-kek — packages/crypto/src/keying/containerKekKeyring.ts; fixed-width plaintext (8-byte header + 64 bytes per predecessor epoch) makes the sealed length an equality in the epoch number
DEK wrapping under a KEM shared secret packages/crypto/src/encapsulation/wrapDek.ts
Local keyring root-key wrapping (account-root envelope) packages/client-sdk/src/client/localKeyring/aesGcmWrapping.ts
Local identity package at rest packages/app/src/providers/identity/localIdentityPackageCrypto.ts
Local OPFS blob store at rest (authenticated 5 MiB chunks) packages/client-sdk/src/data/blobs/encryptedBlobStore.ts, .../encryptedBlobByteSource.ts

Core helpers: packages/crypto/src/symmetric.ts (32-byte key, 12-byte IV, 16-byte tag).

Note: the principal policy payload carries a cipherSuite: "aes-256-gcm" label (PrincipalStatePayloadCipherSuite in packages/crypto/src/principalStateTypes.ts), but the payload itself is base64 JSON the server can read. The organization authority descriptor and the built-in Admins/Members role markers are plaintext; only a custom group’s name is AES-256-GCM-encrypted inside it, under an HKDF-SHA-256 key derived from the organization metadata container KEK (packages/crypto/src/groupMetadata.ts).

Key derivation

KDF Use Location
HKDF-SHA-256 Identity signing/KEM seeds from BIP39 entropy packages/crypto/src/identitySeedPhrase.ts
HKDF-SHA-256 Per-purpose local keys (sqlite, blob-store, identity-persistence) from the random root packages/client-sdk/src/client/localKeyring/rootKey.ts
HKDF-SHA-256 Per-content-record AES-GCM keys packages/client-sdk/src/data/documents/shared/crypto.ts, packages/client-sdk/src/data/blobs/attachments/blobRecordCrypto.ts
PBKDF2-SHA-256 PIN-code wrapping key (310k iters) packages/client-sdk/src/client/localKeyring/localKeyringPinCodeSupport.ts
PBKDF2-SHA-256 Local blob-store key from passphrase (310k iters) packages/client-sdk/src/data/blobs/encryptedBlobStore.ts
PBKDF2-SHA-256 Password-protected local DB backup key (250k iters) packages/app/src/providers/db/localBackupFormat.ts

PIN-code wrap suite: pin-code-pbkdf2-sha256-aes-256-gcm.

Hashing / MAC

Primitive Use Location
SHA-256 Public-key fingerprints, canonical keying hashes packages/crypto/src/fingerprint.ts, packages/crypto/src/keying/canonical.ts
SHA-256 Streamed blob ciphertext integrity packages/crypto/src/incrementalSha256.ts, packages/api/src/utils/sha256.ts
SHA-256 + timingSafeEqual RevenueCat webhook shared-secret compare (constant-time, not HMAC) packages/api/src/routes/billing/revenuecatWebhook.ts
SHA-1 Deterministic v5-style document / child-container ids (non-security) packages/client-sdk/src/data/keying/stableUuid.ts
HMAC-SHA-256 Deterministic container system-slot ids packages/client-sdk/src/workflows/container-contents/systemSlot.ts
HMAC-SHA-256 Stripe webhook signature verification packages/api/src/billing/stripeWebhook.ts

At-rest database

Primitive Use Location
ChaCha20 (active) SQLite file encryption; the app always boots with cipher: "chacha20" packages/app/src/providers/db/bootSQLiteRuntime.ts
SQLCipher / AES-CBC / Ascon-128 (supported, unused) Alternate codecs the lower-level worker API accepts but the app does not select packages/sqlite-worker/src/types.ts, packages/sqlite-worker/src/loadSqlite3.ts

The SQLite cipher key is an HKDF-derived key from the local keyring (sqliteKey).

OPFS local storage

OPFS (the Origin Private File System) is a browser storage backend, not a cipher suite. Tearleads encrypts the data it writes there; OPFS itself does not provide an additional application-managed encryption layer.

OPFS data Protection Location
SQLite database SQLite3MultipleCiphers with the active ChaCha20 codec; its key is the HKDF-derived sqlite local-keyring key packages/app/src/providers/db/bootSQLiteRuntime.ts, packages/client-sdk/src/client/localKeyring/session.ts
Blob files Encrypted-blob-store v2: AES-256-GCM in authenticated 5 MiB chunks, with the namespace, storage key, chunk index, and serialized envelope bound as additional authenticated data packages/client-sdk/src/data/blobs/encryptedBlobStore.ts, packages/client-sdk/src/data/blobs/encryptedBlobEnvelope.ts, packages/client-sdk/src/data/blobs/encryptedBlobByteSource.ts

Each encrypted blob has a fresh 96-bit base IV. Chunk IVs are derived from that base IV and the chunk index, and each ciphertext chunk includes the 16-byte GCM authentication tag. The persisted envelope records the format, version, chunk shape, base IV, and (when a string passphrase is used) a PBKDF2-SHA-256 salt and iteration count. Raw 32-byte/AES-GCM keys, including the production local-keyring-derived blob key, do not use this PBKDF2 envelope.

On supported platforms, SQLite uses the OPFS SyncAccessHandle Pool VFS and blob files are placed under an identity-namespaced tearleads OPFS directory. The app requests durable browser storage before the first persistent database write; this is best effort, so a browser can still deny it. Without OPFS the automatic storage policy selects in-memory SQLite; an explicitly requested persistent SQLite backend hard-fails instead. Encrypted OPFS blob-store factories likewise fail closed rather than silently writing unencrypted files elsewhere.

Libraries

Library Provides
@noble/post-quantum ML-KEM-1024, ML-DSA-87
@noble/hashes SHA-256, HKDF
@scure/bip39 24-word mnemonic seed phrases
WebCrypto (crypto.subtle) AES-GCM, HKDF, PBKDF2, HMAC, SHA-256, SHA-1 (client runtime)
node:crypto SHA-256, HMAC, timingSafeEqual (server / billing only)
SQLite3MultipleCiphers At-rest database encryption codec