Simple TypeScript library for symmetric encryption.
  • TypeScript 100%
Find a file
Joshue Abance 7baa42a5c8
fix!: add key commitment, associated data and native AES-GCM
Introduce a versioned ciphertext format with a per-message HKDF salt and
key commitment that is checked before decryption. Add an optional aad
input, and use WebCrypto for AES-256-GCM to avoid table-based AES.

AES-256-GCM-SIV stays on @noble/ciphers because neither WebCrypto nor
Node provides it.

BREAKING CHANGE: decrypt rejects 0.1.x ciphertext unless legacy: true is
set. AES-256-GCM now requires crypto.subtle.
2026-11-08 20:43:53 +08:00
src fix!: add key commitment, associated data and native AES-GCM 2026-11-08 20:43:53 +08:00
tests fix!: add key commitment, associated data and native AES-GCM 2026-11-08 20:43:53 +08:00
.gitignore init lib 2026-06-18 00:00:28 +08:00
.npmrc for lib publishing 2026-06-18 00:20:09 +08:00
LICENSE init lib 2026-06-18 00:00:28 +08:00
package.json chore: add package metadata and pre-publish checks 2026-11-08 20:17:47 +08:00
pnpm-lock.yaml chore(deps): upgrade all dependencies and clear audit advisories 2026-11-08 20:17:42 +08:00
README.md fix!: add key commitment, associated data and native AES-GCM 2026-11-08 20:43:53 +08:00
tsconfig.json init lib 2026-06-18 00:00:28 +08:00
tsdown.config.ts chore(deps): upgrade all dependencies and clear audit advisories 2026-11-08 20:17:42 +08:00

minicrypt

Simple TypeScript library for symmetric encryption.

Provides four authenticated encryption ciphers:

  • AES-256-GCM — Standard and widely interoperable
  • AES-256-GCM-SIV — Nonce-misuse-resistant variant of AES-GCM
  • ChaCha20-Poly1305 — Fast software cipher, used in TLS 1.3 and WireGuard
  • XChaCha20-Poly1305 — Extended-nonce variant, safe with random nonces

Warning

This library is designed for encryption/decryption of data, not for hashing or storing passwords. Use a dedicated password-hashing function (Argon2id, bcrypt, scrypt) for password storage.

Installation

pnpm add minicrypt

Important

The key parameter is not an encryption key — it is a secret string that gets derived into a 256-bit key via HKDF-SHA256 (RFC 5869) with domain separation. Use a high-entropy secret (e.g., crypto.randomBytes(32) hex-encoded, or a strong passphrase). Low-entropy secrets like "my-password" are trivially brute-forced — HKDF is a key-derivation function, not a password hasher. If you need to use a user-provided password, hash it through Argon2id / scrypt / bcrypt first, then use the result as the key.

Usage

AES-256-GCM

Standard and widely interoperable.

import { encrypt, decrypt } from "minicrypt/aes-256-gcm";
import crypto from "crypto";

// Generate a high-entropy secret
const key = crypto.randomBytes(32).toString("hex");

const encrypted = await encrypt({ key, data: "Hello, World!" });
const decrypted = await decrypt({ key, data: encrypted });

AES-256-GCM-SIV

Nonce-misuse-resistant: if a nonce is reused, only reveals whether plaintexts are identical.

import { encrypt, decrypt } from "minicrypt/aes-256-gcm-siv";
import crypto from "crypto";

const key = crypto.randomBytes(32).toString("hex");

const encrypted = await encrypt({ key, data: "Hello, World!" });
const decrypted = await decrypt({ key, data: encrypted });

ChaCha20-Poly1305

Standard 12-byte nonce variant used in TLS 1.3, WireGuard, and SSH.

import { encrypt, decrypt } from "minicrypt/chacha20-poly1305";
import crypto from "crypto";

const key = crypto.randomBytes(32).toString("hex");

const encrypted = await encrypt({ key, data: "Hello, World!" });
const decrypted = await decrypt({ key, data: encrypted });

XChaCha20-Poly1305

Extended 24-byte nonce — safe to use with randomly generated nonces.

import { encrypt, decrypt } from "minicrypt/xchacha20-poly1305";
import crypto from "crypto";

const key = crypto.randomBytes(32).toString("hex");

const encrypted = await encrypt({ key, data: "Hello, World!" });
const decrypted = await decrypt({ key, data: encrypted });

API

All modules export the same async interface:

encrypt(options: CryptoOptions): Promise<string>

  • options.key — The secret string used as HKDF-SHA256 input key material
  • options.data — The plaintext string to encrypt (must be non-empty, with no unpaired surrogates)
  • options.info — Optional HKDF info context; must match between encrypt and decrypt (see Security notes)
  • options.aad — Optional associated data, authenticated but not stored in the output
  • Returns — Hex-encoded ciphertext: 0x02 || salt || commitment || nonce || ciphertext || tag

decrypt(options: CryptoOptions): Promise<string>

  • options.key — The secret string (must match the one used for encryption)
  • options.data — The hex-encoded ciphertext to decrypt
  • options.info — Optional HKDF info context; must match between encrypt and decrypt (see Security notes)
  • Returns — The decrypted plaintext string
  • options.aad — Must match the value used for encryption
  • options.legacy — Set to true to read minicrypt 0.1.x ciphertext (migration only)
  • Throws — If decryption fails (wrong key, info or aad, tampered or malformed data)

Choosing a Cipher

Cipher Nonce Size Best For
AES-256-GCM 12 bytes Interoperating with AES-GCM systems
AES-256-GCM-SIV 12 bytes When nonce uniqueness isn't guaranteed
ChaCha20-Poly1305 12 bytes Software-only environments, mobile
XChaCha20-Poly1305 24 bytes Random nonces, high-volume encryption

XChaCha20-Poly1305 is the recommended default unless you need interoperability with a specific cipher.

Security Notes

  • AES-256-GCM uses native AES through WebCrypto. It needs crypto.subtle, which browsers only expose on secure origins.
  • AES-256-GCM-SIV is pure JavaScript. WebCrypto and Node have no GCM-SIV API, so it uses @noble/ciphers, whose AES T-tables can leak cache timings. If an attacker can observe timing on shared hardware, use another cipher.
  • Per-message keys. Each message uses a key derived from a random 256-bit salt, so the 2²³ random-nonce limit on 12-byte-nonce ciphers does not apply.
  • info is combined with the cipher domain. The same key and info derive different keys for different ciphers.
  • Key-committing. Ciphertexts include an HKDF-SHA256 commitment, checked before decryption, so a ciphertext cannot be crafted to decrypt under two secrets. legacy: true skips this, so use it only to migrate.
  • Associated data. Pass aad (such as a record ID) to bind a ciphertext to its context, so it cannot be moved to another record.
  • Text only. Input is UTF-8 encoded, and strings with unpaired surrogates are rejected.

Upgrading from 0.1.x

0.2 uses a new key-committing format, and decrypt rejects 0.1.x ciphertext by default. To migrate, read old data with legacy: true and encrypt it again:

const plaintext = await decrypt({ key, data: oldCiphertext, legacy: true });
const upgraded = await encrypt({ key, data: plaintext });

Pass the same info you used with 0.1.x. Remove legacy: true once your data is migrated, because accepting legacy ciphertext lets an attacker bypass key commitment.

Development

# Install dependencies
pnpm install

# Run tests
pnpm test

# Build library
pnpm build

# Type check
pnpm typecheck

Credits

License

MIT