Core Concepts

Purpose scoping

Every operation takes a purpose string. It’s included in the payload’s AAD (Additional Authenticated Data) and in the HKDF key derivation info. Decrypting with the wrong purpose always fails — even with the correct master key.

await enc.encrypt(cardNumber, 'payment:card');
await enc.encrypt(sessionToken, 'auth:session');
await enc.encrypt(userEmail, 'user:pii:email');
// Each of these uses a completely separate derived key

Treat purpose strings as security boundaries. A value encrypted for auth:session can never be decrypted (or misused) as payment:card. A common convention is domain:subdomain:field.

Zero dependencies

Everything uses the Web Crypto API (globalThis.crypto.subtle), available natively in Node 18+, all modern browsers, and edge runtimes. The only exceptions are ChaCha20-Poly1305 and scrypt, which use Node’s native crypto module.

This means no supply-chain surface, a tiny footprint, and identical behavior across environments.

AEAD everywhere

All symmetric ciphers are authenticated (AEAD): AES-256-GCM, ChaCha20-Poly1305, and AES-SIV all produce an authentication tag that’s verified on decrypt. There is no unauthenticated mode — tampering is always detected and surfaced as a DECRYPTION_FAILED error.

Payload binary layout

Payloads are compact, versioned binary structures (then encoded per your encoding setting):

Symmetric payload:
  [1B version][1B algo_id][2B key_version][12B iv][16B tag][N bytes ciphertext]

Deterministic (AES-SIV):
  [1B version][1B algo_id=0x03][16B SIV][N bytes ciphertext]

Streaming chunks:
  Header: [4B magic "ENCX"][1B version][1B algo][12B stream_nonce][4B chunk_size]
  Chunk:  [4B index][12B iv][16B tag][N bytes ciphertext]

The embedded version and key_version are what enable key rotation with backward-compatible payloads. You can inspect any payload’s header without the key using the inspection utilities.