Aead unstable
Authenticated encryption: AES-GCM, ChaCha20-Poly1305, and XChaCha20-Poly1305.
The Aead service generates keys, encrypts, and decrypts. The top-level key, fromCryptoKey, and toBytes functions build and export keys without a service.
Ciphertext is ciphertext‖tag with a 16-byte tag for every algorithm, the
layout WebCrypto produces. A tampered ciphertext, tag, or additional data
fails decryption with a DecryptionFailed reason that carries no detail.
AES-GCM runs on crypto.subtle. ChaCha20-Poly1305 and XChaCha20-Poly1305
run a pure TypeScript implementation vendored from @noble/ciphers 2.4.0,
because WebCrypto does not provide them everywhere. Cure53 audited
@noble/ciphers 1.0.0 in 2024.
Keys are opaque. They hold either key bytes or, for AES-GCM, a CryptoKey,
display only their algorithm, and compare by reference.
Gotchas
Never reuse a nonce with the same key. Random 12-byte nonces risk a collision after about 2^32 messages under one key: use a counter, or XChaCha20-Poly1305, whose 24-byte nonces can be random.
Algorithms
AES-128-GCM, with a 16-byte key and a 12-byte nonce.
Signature
declare const aes128Gcm: AesGcmAES-256-GCM, with a 32-byte key and a 12-byte nonce.
Signature
declare const aes256Gcm: AesGcmchacha20Poly1305
unstableChaCha20-Poly1305 (RFC 8439), with a 32-byte key and a 12-byte nonce.
Details
Runs the pure TypeScript implementation under layerWebCrypto, even where
crypto.subtle provides it, so every runtime takes one code path.
Signature
declare const chacha20Poly1305: Algorithmxchacha20Poly1305
unstableXChaCha20-Poly1305 (draft-irtf-cfrg-xchacha-03), with a 32-byte key and a 24-byte nonce.
When to use
Use when nonces are generated randomly, since 24-byte nonces make
collisions negligible. The output is compatible with libsodium's
crypto_aead_xchacha20poly1305_ietf.
Signature
declare const xchacha20Poly1305: AlgorithmConstructors
fromCryptoKey
unstableBuilds an AES-GCM key from an existing CryptoKey, such as a
non-extractable one.
Details
The CryptoKey must be a secret AES-GCM key of the algorithm's length with
both the encrypt and decrypt usages, since a key reaches both
operations. Otherwise the constructor fails with an InvalidKey reason. It
must come from the Crypto object of the WebCrypto reference.
ChaCha20-Poly1305 and XChaCha20-Poly1305 never run on crypto.subtle, so
passing them is a type error.
Signature
declare function fromCryptoKey<A extends AesGcm>(algorithm: A, cryptoKey: CryptoKey): Effect<Key<A>, CryptoError>Builds a key from raw key bytes.
Details
Copies the bytes, including Node.js Buffer inputs. A length other than
the algorithm's key length fails with an InvalidKey reason. AES-192 is
not supported.
Signature
declare function key<A extends Algorithm>(algorithm: A, bytes: Uint8Array): Effect<Key<A>, CryptoError>Converting
Exports a key's bytes.
Details
Returns a fresh Uint8Array that shares no memory with the key. A
CryptoKey-backed key is exported on the WebCrypto reference, and fails
with a NotExtractable reason when the CryptoKey is not extractable.
Signature
declare function toBytes(key: Key): Effect<Uint8Array<ArrayBufferLike>, CryptoError>Guards
Layers
layerWebCrypto
unstableLayer that provides Aead on the WebCrypto reference.
Details
AES-GCM runs on crypto.subtle. The layer imports a byte-backed key on
first use and caches the CryptoKey. ChaCha20-Poly1305 and
XChaCha20-Poly1305 always run the pure TypeScript implementation, with no
feature detection. Generated keys come from SecureRandom, so a seeded
SecureRandom gives reproducible keys.
Signature
declare const layerWebCrypto: Layer.Layer<Aead, never, SecureRandom.SecureRandom>layerWebCryptoWith
unstableBuilds an Aead layer on the WebCrypto reference that runs the given
implementations for byte-backed keys.
When to use
Use to run an algorithm on a runtime's native code, as NodeCrypto.layerAead
does for ChaCha20-Poly1305 with node:crypto.
Details
implementations maps algorithm names to implementations that replace
crypto.subtle or the algorithm's pure TypeScript code. They follow the
Implementation contract and run only for byte-backed keys, after the
layer has checked the nonce, plaintext, and ciphertext lengths, so errors are
those of layerWebCrypto. CryptoKey-backed keys run on
crypto.subtle, and generated keys come from SecureRandom. Without
implementations, the layer is layerWebCrypto.
Gotchas
An implementation receives the key's own bytes, not a copy, so it must not modify or keep them.
Signature
declare function layerWebCryptoWith(options: { readonly implementations?: Partial<Record<"aes128Gcm" | "aes256Gcm" | "chacha20Poly1305" | "xchacha20Poly1305", Implementation>>;}): Layer<Aead, never, SecureRandom>Example Running ChaCha20-Poly1305 natively
import { Layer } from "effect"import { Aead, SecureRandom } from "effect/crypto"
declare const nativeChacha: Aead.Implementation
const layer = Aead.layerWebCryptoWith({ implementations: { chacha20Poly1305: nativeChacha }}).pipe(Layer.provide(SecureRandom.layerWebCrypto))Models
AES-GCM, from aes128Gcm or aes256Gcm: the algorithms that
run on crypto.subtle, and the only ones fromCryptoKey accepts.
Signature
interface AesGcm extends Algorithm { readonly implementation: undefined; readonly name: "aes128Gcm" | "aes256Gcm"; readonly nonceLength: 12;}An authenticated encryption algorithm, such as aes256Gcm.
Details
keyLength and nonceLength are in bytes. implementation is the pure
TypeScript code for algorithms that WebCrypto does not provide, which a
layer calls when it has no native mapping for name.
Signature
interface Algorithm { readonly implementation: Implementation | undefined; readonly keyLength: 16 | 32; readonly name: "aes128Gcm" | "aes256Gcm" | "chacha20Poly1305" | "xchacha20Poly1305"; readonly nonceLength: 12 | 24;}The synchronous code of an algorithm: the pure TypeScript code of one that WebCrypto does not provide, or a native implementation passed to layerWebCryptoWith.
Details
encrypt returns ciphertext‖tag. decrypt takes ciphertext‖tag and
returns undefined when the tag does not match. Both receive arguments the
layer has already checked.
Signature
interface Implementation { readonly decrypt: (key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array) => Uint8Array<ArrayBufferLike> | undefined; readonly encrypt: (key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array) => Uint8Array;}An opaque authenticated encryption key for the algorithm A.
Details
The key holds either key bytes or a CryptoKey from fromCryptoKey.
toString, toJSON, and inspect show only the algorithm. Keys compare
by reference and do not implement Equal, because comparing key bytes is
not constant time.
Signature
interface Key<out A extends Algorithm = Algorithm> extends Inspectable { readonly "~effect/crypto/Aead/Key": "~effect/crypto/Aead/Key"; readonly algorithm: A;}Services
Service that generates keys, encrypts, and decrypts with authentication.
Signature
declare class Aead extends Shape<"effect/crypto/Aead", { readonly decrypt: (key: Key, ciphertext: Uint8Array, options: { readonly aad?: Uint8Array<ArrayBufferLike>; readonly nonce: Uint8Array; }) => Effect<Uint8Array<ArrayBufferLike>, CryptoError>; readonly encrypt: (key: Key, plaintext: Uint8Array, options: { readonly aad?: Uint8Array<ArrayBufferLike>; readonly nonce: Uint8Array; }) => Effect<Uint8Array<ArrayBufferLike>, CryptoError>; readonly generateKey: <A extends Algorithm>(algorithm: A) => Effect<Key<A>, CryptoError>;}, this> { constructor(_: never);}Example Encrypting with XChaCha20-Poly1305
import { Effect, Layer } from "effect"import { Aead, SecureRandom } from "effect/crypto"
const program = Effect.gen(function*() { const aead = yield* Aead.Aead const random = yield* SecureRandom.SecureRandom const key = yield* aead.generateKey(Aead.xchacha20Poly1305) const nonce = yield* random.randomBytes(24) const sealed = yield* aead.encrypt(key, new TextEncoder().encode("secret"), { nonce }) const opened = yield* aead.decrypt(key, sealed, { nonce }) return [sealed.length, new TextDecoder().decode(opened)]})
const layer = Aead.layerWebCrypto.pipe(Layer.provideMerge(SecureRandom.layerWebCrypto))await Effect.runPromise(program.pipe(Effect.provide(layer))) // => [22, "secret"]