Modern, cross-platform encryption for both files and text.
- Node 22 / Bun 1 - native 
argon2addon + WebCrypto - Browser (evergreen) - tiny WASM build of 
argon2-browser - CLI - stream encryption & decryption, zero memory bloat
 - TypeScript-first, tree-shakable, ESM & CJS builds
 - Format-agnostic decryption - one instance reads any registered scheme
 
Currently there are 2 encryption schemes supported:
- Scheme 0 (default): AES-GCM 256 (native via Crypto API) and Argon2id (single thread parallelism setup using 
argon2orargon2-browser*) - Scheme 1: XChaCha20Poly1305 (via JavaScript engine 
@noble/ciphers) and and Argon2id (multi thread parallelism setup usingargon2orargon2-browser*) 
* This means that for the same "difficulty" setting, the KDF will be significantly slower in the browser than in Node.js.
The library can support up to 8 schemes via a header info byte (3 bit allocated).
Warning
Scheme 1 works with an extractable CryptoKey. If unsure use scheme 0.
- Text Encryption / Decryption: https://mqxym.github.io/cryptit/text-encryption.html
 - Text Data Decoding https://mqxym.github.io/cryptit/text-decoding.html
 - File Encryption / Decryption https://mqxym.github.io/cryptit/file-encryption.html
 - File Streaming https://mqxym.github.io/cryptit/streaming.html
 - File Data Decoding https://mqxym.github.io/cryptit/file-decoding.html
 
# Bun (recommended)
bun add @mqxym/cryptit
# npm / pnpm
yarn add @mqxym/cryptit           # or npm i / pnpm addimport { createCryptit } from "@mqxym/cryptit";
const crypt = createCryptit({ scheme: 1 });
const pass  = "correct horse battery staple";
// Encrypt: returns a ConvertibleOutput wrapper
const out = await crypt.encryptText("hello", pass);
// Pick your preferred representation
console.log(out.base64);      // Base64 container
console.log(out.hex);         // Hex container
const bytes = out.uint8array; // Uint8Array container
// Decrypt: accepts Base64, Uint8Array, or ConvertibleInput
const dec = await crypt.decryptText(out.base64, pass);
console.log(dec.text);        // "hello"
// Clean sensitive buffers when done
out.clear();
dec.clear();import { createCryptit } from "@mqxym/cryptit";
import { createReadStream, createWriteStream } from "node:fs";
const crypt = createCryptit();
const pass  = "hunter2";
// encrypt → movie.enc
await createReadStream("movie.mkv")
  .pipeThrough(await crypt.createEncryptionStream(pass))
  .pipeTo(createWriteStream("movie.enc"));
// decrypt back
await createReadStream("movie.enc")
  .pipeThrough(await crypt.createDecryptionStream(pass))
  .pipeTo(createWriteStream("movie.mkv"));<script>
  // This needs to be included before the actual importing of cryptit
  // IMPORTANT: host argon2.wasm where the fetch command points to
  window.loadArgon2WasmBinary = () =>
    fetch("/examples/assets/argon2.wasm")
      .then(r => r.arrayBuffer())
      .then(buf => new Uint8Array(buf));
</script>
<!-- app.ts / app.js -->
<script type="module">
  import { createCryptit } from "@mqxym/cryptit/browser";
  const crypt = createCryptit({ saltStrength: "high", verbose: 2 });
  async function enc() {
    const cipher = await crypt.encryptText("hello", "pw");
    console.log(cipher.base64);  // or .hex / .uint8array
    cipher.clear();
  }
  enc();
</script>Use with a bundler or simply via <script type="module">.
import { createCryptit, Cryptit } from "@mqxym/cryptit";
// Also available: ConvertibleInput / ConvertibleOutput
// import { ConvertibleInput, ConvertibleOutput } from "@mqxym/cryptit";
const c = createCryptit({ verbose: 1 });
// TEXT 
const enc: ConvertibleOutput =
  await c.encryptText(/* string | Uint8Array | ConvertibleInput */ "txt", pass);
// Choose your representation:
enc.base64; enc.hex; enc.uint8array; // and wipe when done:
enc.clear();
const dec: ConvertibleOutput =
  await c.decryptText(/* Base64 string | Uint8Array | ConvertibleInput */ enc.base64, pass);
dec.text;
dec.clear();
// RUNTIME TWEAKS
c.setDifficulty("high");  // Argon2id difficulty preset
c.setScheme(1);           // choose another registered format (scheme 1 = XChaCha20Poly1305)
c.setSaltDifficulty("low");
// HELPERS
Cryptit.isEncrypted(blobOrB64);   // ↦ boolean
Cryptit.decodeHeader(blobOrB64);  // ↦ meta {scheme, salt, …}
Cryptit.decodeData(blobOrB64);  // ↦ {isChunked, ivLength, tagLength, iv, tag, …}Verbose levels:
| Level | Emits | 
|---|---|
| 0 | errors only | 
| 1 | +start/finish notices | 
| 2 | +timings, key-derivation info | 
| 3 | +salt / scheme / KDF details | 
| 4 | wire-level debug | 
# encrypt file → .enc
cryptit encrypt  <in> [-o out] [options]
# decrypt back
cryptit decrypt  <in> [-o out] [options]
# encrypt text
echo "secret" | cryptit encrypt-text  -p pw
cryptit encrypt-text "secret" -d high -S 1 # -> Prompt for password, Argon2id difficulty "high" and Scheme 1
# decrypt text
echo "…b64…" | cryptit decrypt-text -p pw
# inspect header, chunk and text details of Cryptit-encrypted payloads (no decryption)
cryptit decode movie.enc
cat movie.enc | cryptit decode
# output fake data (valid header) in base64 with random 32-byte tail
cryptit fake-data --base64 32docker pull ghcr.io/mqxym/cryptit-cli:latest
echo "AQVWgYDH/rkR6Ymxv1W9NzFWTsvTTXsnEaLHPx+NlATmuwcqea5RlljX1ly16Px716I2yGX/XsXHt7xG14DmnJ3Czu0A9/TM1sPJayRdHDYPckJ5eGfAGY5n5H8nNjKqhpY=" | docker run --rm -i cryptit:latest decode | jq| Flag | Default | Description | 
|---|---|---|
-p, --pass <pw> | 
prompt | passphrase | 
-d, --difficulty <l> | 
middle | Argon2 preset | 
-S, --scheme <0-1>. | 
0. | Scheme preset | 
-s, --salt-strength <l> | 
high | 12 B vs 16 B salt | 
-c, --chunk-size <n> | 
524 288 | plaintext block size | 
-v, --verbose | 
0 … 4 | repeat to increase | 
Exit codes: 0 success · 1 any failure (invalid header, auth, I/O …)
Note
Use the prompt password feature where ever possible, to not leak your password via history.
- Header: 
0x01 | infoByte | salt - Decryptors pick the engine by the header’s scheme ⇒ one CLI handles all registered schemes.
 
- Since version 1.0.0: Header data is authenticated.
 - Since version 2.2.0: 
encryptText()uses 8-bit padding before AEAD, which is also tagged in AAD. 
- 
To decrypt data from versions prior to 1.0.0, there is a temporary solution:
const cryptit = createCryptit({ acceptUnauthenticatedHeader: true });
- This option will be removed in future releases because the header must always be authenticated.
 
 - 
The padding tag for encrypted text in AAD is not required, so encrypted text from versions prior to 2.2.0 can still be decrypted with versions greater than 2.2.0.
- This backward compatibility will also be removed in future releases.
 
 
git clone https://github.com/mqxym/cryptit
cd cryptit
bun install && bun run build && bun test- AES-GCM 256 / 12-byte IV / 128-bit tag
 - XChaCha20Poly1305 / 24-byte IV / 128-bit tag
 - Argon2-id presets (low / middle / high)
 - Salts generated per-ciphertext; never reused
 
Important
DISCLAIMER This project was created in collaboration with OpenAI’s language models and me, @mqxym.
TL;DR • Scheme 0 (AES‑GCM/SubtleCrypto + XChaCha20‑Poly1305) is much faster for streaming: peak ~1,810 MiB/s (stdin→stdout, 256 MiB, middle). • Scheme 1 (XChaCha20‑Poly1305) peaks ~170 MiB/s. • KDF cost is now measured separately and not included in “stream‑only” throughput below.
| Difficulty | KDF avg (Scheme 0) | KDF avg (Scheme 1) | 
|---|---|---|
| low | 174.29 ms | 167.05 ms | 
| middle | 540.41 ms | 442.74 ms | 
| high | 1013.10 ms | 825.44 ms | 
Stream‑only Throughput (KDF‑subtracted) — Scheme 0
Higher is better (MiB/s).
| Size | Difficulty | enc f→f | dec f→out | enc in→out | dec in→out | 
|---|---|---|---|---|---|
| 256 MiB | low | 868.33 | 910.05 | 1709.13 | 1519.16 | 
| 256 MiB | middle | 995.48 | 910.46 | 1810.27 | 1585.49 | 
| 256 MiB | high | 899.43 | 866.88 | 1658.29 | 1409.89 | 
Stream‑only Throughput (KDF‑subtracted) — Scheme 1
Higher is better (MiB/s).
| Size | Difficulty | enc f→f | dec f→out | enc in→out | dec in→out | 
|---|---|---|---|---|---|
| 256 MiB | low | 149.33 | 153.01 | 167.36 | 164.99 | 
| 256 MiB | middle | 154.23 | 154.72 | 169.55 | 162.91 | 
| 256 MiB | high | 149.83 | 151.26 | 157.55 | 163.05 | 
Wall‑clock Durations (no subtraction) — Scheme 0
Lower is better (ms / s). Decode columns show latency (ms).
| Size | Difficulty | enc f→f | dec f→out | enc in→out | dec in→out | decode file (ms) | decode stdin (ms) | 
|---|---|---|---|---|---|---|---|
| 256 MiB | low | 469 ms / 0.47 s | 456 ms / 0.46 s | 324 ms / 0.32 s | 343 ms / 0.34 s | 45 | 153 | 
| 256 MiB | middle | 798 ms / 0.80 s | 822 ms / 0.82 s | 682 ms / 0.68 s | 702 ms / 0.70 s | 46 | 190 | 
| 256 MiB | high | 1298 ms / 1.30 s | 1308 ms / 1.31 s | 1167 ms / 1.17 s | 1195 ms / 1.19 s | 45 | 178 | 
Wall‑clock Durations (no subtraction) — Scheme 1
Lower is better (ms / s). Decode columns show latency (ms).
| Size | Difficulty | enc f→f | dec f→out | enc in→out | dec in→out | decode file (ms) | decode stdin (ms) | 
|---|---|---|---|---|---|---|---|
| 256 MiB | low | 1881 ms / 1.88 s | 1840 ms / 1.84 s | 1697 ms / 1.70 s | 1719 ms / 1.72 s | 48 | 133 | 
| 256 MiB | middle | 2103 ms / 2.10 s | 2097 ms / 2.10 s | 1953 ms / 1.95 s | 2014 ms / 2.01 s | 48 | 180 | 
| 256 MiB | high | 2534 ms / 2.53 s | 2518 ms / 2.52 s | 2450 ms / 2.45 s | 2396 ms / 2.40 s | 49 | 190 | 
Legend
enc f→f = encrypt file→file • dec f→out = decrypt file→stdout • enc in→out = encrypt stdin→stdout • dec in→out = decrypt stdin→stdout.
Method notes
• CLI: bun run cli:run  • Difficulties: low/middle/high  • Size: 256 MiB  • Repeats: 1
• KDF repeats = 3, payload = 16 bytes. “Stream‑only” removes the measured KDF baseline for the respective difficulty; wall‑clock shows full end‑to‑end time.
MIT