Skip to content
/ cryptit Public

Cross-platform encryption API & CLI for text and files via node, bun or browser and CLI via docker or compiled binaries.

License

Notifications You must be signed in to change notification settings

mqxym/cryptit

Repository files navigation

@mqxym/cryptit

Base CI

Modern, cross-platform encryption for both files and text.

  • Node 22 / Bun 1 - native argon2 addon + 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

Scheme Support

Currently there are 2 encryption schemes supported:

  • Scheme 0 (default): AES-GCM 256 (native via Crypto API) and Argon2id (single thread parallelism setup using argon2 or argon2-browser*)
  • Scheme 1: XChaCha20Poly1305 (via JavaScript engine @noble/ciphers) and and Argon2id (multi thread parallelism setup using argon2 or argon2-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.


Live Demo


Install

# Bun (recommended)
bun add @mqxym/cryptit

# npm / pnpm
yarn add @mqxym/cryptit           # or npm i / pnpm add

Quick start - Node / Bun

import { 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();

Streaming files

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"));

Browser usage

<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">.


API highlights

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

CLI (cryptit)

# 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 32

Docker CLI

docker pull ghcr.io/mqxym/cryptit-cli:latest

echo "AQVWgYDH/rkR6Ymxv1W9NzFWTsvTTXsnEaLHPx+NlATmuwcqea5RlljX1ly16Px716I2yGX/XsXHt7xG14DmnJ3Czu0A9/TM1sPJayRdHDYPckJ5eGfAGY5n5H8nNjKqhpY=" | docker run --rm -i cryptit:latest decode | jq

Common flags

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.


Versioned format

  • Header: 0x01 | infoByte | salt
  • Decryptors pick the engine by the header’s scheme ⇒ one CLI handles all registered schemes.

Additional Authenticated Data

  • 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.

Compatibility

  • 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.

Build from source

git clone https://github.com/mqxym/cryptit
cd cryptit
bun install && bun run build && bun test

Security

  • 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.


CLI Benchmarks (Bun Engine, MacOS, M3 Pro Chip)

TL;DRScheme 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.


KDF Baseline (avg of 3, encrypt‑text 16 B payload)

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.


License

MIT

About

Cross-platform encryption API & CLI for text and files via node, bun or browser and CLI via docker or compiled binaries.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •