Skip to content
Effect 4.0 is here. One ecosystem. Zero dependencies.

Cipher unstable

Unauthenticated encryption with no integrity: compose with Mac.

AES-CTR and AES-CBC with PKCS#7 padding, for protocols that pair them with a MAC, such as SSH, SRTP, JWE, CMS, and Matrix. Anyone can change the ciphertext without being detected, so authenticate it with Mac (encrypt-then-MAC), or use Aead instead.

The Cipher service generates keys, encrypts, and decrypts. The top-level key, fromCryptoKey, and toBytes functions build and export keys without a service. Both modes run on crypto.subtle.

Keys are opaque. They hold either key bytes or a CryptoKey, display only their algorithm, and compare by reference.

Gotchas

  • Never reuse an IV with the same key. For AES-CTR a repeated counter block reveals the XOR of two plaintexts. For AES-CBC the IV must also be unpredictable.
  • AES-CTR decryption never fails, whatever the ciphertext. AES-CBC decryption fails with one DecryptionFailed reason for both bad padding and a bad length, so callers cannot tell them apart. Verify a MAC before decrypting, or the error itself becomes a padding oracle.
12 exports Since v4.0.0 Source

Algorithms

aes128Cbc

unstable
Since v4.0.0

AES-128-CBC with PKCS#7 padding, with a 16-byte key.

Signature

declare const aes128Cbc: Algorithm

aes128Ctr

unstable
Since v4.0.0

AES-128-CTR, with a 16-byte key.

Details

The 16-byte iv is the initial counter block, and the whole 128-bit block is the counter, as in OpenSSL, Node.js, Go's cipher.NewCTR, and noble.

Signature

declare const aes128Ctr: Algorithm

aes256Cbc

unstable
Since v4.0.0

AES-256-CBC with PKCS#7 padding, with a 32-byte key.

Signature

declare const aes256Cbc: Algorithm

aes256Ctr

unstable
Since v4.0.0

AES-256-CTR, with a 32-byte key.

Details

The 16-byte iv is the initial counter block, and the whole 128-bit block is the counter, as in OpenSSL, Node.js, Go's cipher.NewCTR, and noble.

Signature

declare const aes256Ctr: Algorithm

Constructors

Since v4.0.0

Builds a key from an existing CryptoKey, such as a non-extractable one.

Details

The CryptoKey must be a secret key for the algorithm's mode (AES-CTR or AES-CBC) and 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.

Signature

declare function fromCryptoKey<A extends Algorithm>(algorithm: A, cryptoKey: CryptoKey): Effect<Key<A>, CryptoError>

key

unstable
Since v4.0.0

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

toBytes

unstable
Since v4.0.0

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

isKey

unstable
Since v4.0.0

Checks whether a value is a Key.

Signature

declare function isKey(u: unknown): u is Key<Algorithm>

Layers

Since v4.0.0

Layer that provides Cipher on the WebCrypto reference.

Details

Encryption and decryption run on crypto.subtle. The layer imports a byte-backed key on first use and caches the CryptoKey. AES-CTR uses the whole 128-bit block as the counter (length: 128), which every runtime supports. Generated keys come from SecureRandom, so a seeded SecureRandom gives reproducible keys.

Signature

declare const layerWebCrypto: Layer.Layer<Cipher, never, SecureRandom.SecureRandom>

Models

Algorithm

interface unstable
Since v4.0.0

An unauthenticated cipher algorithm, such as aes256Cbc.

Details

mode is the WebCrypto algorithm name and keyLength is in bytes.

Signature

interface Algorithm {
readonly keyLength: 16 | 32;
readonly mode: "AES-CTR" | "AES-CBC";
readonly name: "aes128Ctr" | "aes256Ctr" | "aes128Cbc" | "aes256Cbc";
}

Key

interface unstable
Since v4.0.0

An opaque unauthenticated cipher 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/Cipher/Key": "~effect/crypto/Cipher/Key";
readonly algorithm: A;
}

Services

Cipher

unstable
Since v4.0.0

Service that generates keys, encrypts, and decrypts without authentication.

Signature

declare class Cipher extends Shape<"effect/crypto/Cipher", {
readonly decrypt: (key: Key, ciphertext: Uint8Array, options: {
readonly iv: Uint8Array;
}) => Effect<Uint8Array<ArrayBufferLike>, CryptoError>;
readonly encrypt: (key: Key, plaintext: Uint8Array, options: {
readonly iv: Uint8Array;
}) => Effect<Uint8Array<ArrayBufferLike>, CryptoError>;
readonly generateKey: <A extends Algorithm>(algorithm: A) => Effect<Key<A>, CryptoError>;
}, this> {
constructor(_: never);
}

Example Encrypting with AES-CBC

import { Effect, Layer } from "effect"
import { Cipher, SecureRandom } from "effect/crypto"
const program = Effect.gen(function*() {
const cipher = yield* Cipher.Cipher
const random = yield* SecureRandom.SecureRandom
const key = yield* cipher.generateKey(Cipher.aes256Cbc)
const iv = yield* random.randomBytes(16)
const ciphertext = yield* cipher.encrypt(key, new TextEncoder().encode("secret"), { iv })
const plaintext = yield* cipher.decrypt(key, ciphertext, { iv })
return [ciphertext.length, new TextDecoder().decode(plaintext)]
})
const layer = Cipher.layerWebCrypto.pipe(Layer.provideMerge(SecureRandom.layerWebCrypto))
await Effect.runPromise(program.pipe(Effect.provide(layer))) // => [16, "secret"]