Getting Started
Why this exists
A puzzle says "the key is LEMON" and I want the plaintext, not a lecture. An agent gets a ROT-47 string in a challenge and needs to flip it without leaving the process. Each cipher is small, but every one of them has conventions that decide whether your answer matches the author's: does Playfair fold J into I, does Bacon use 24 or 26 letters, which rotors is "Enigma" even. Every online tool picks something different and rarely says what.
So @agntn/ciphers picks once, writes it down, and gives every cipher the same two methods. No HTTP, no keys, no telemetry. It runs in your process and that's it.
Install
pnpm add @agntn/ciphers
First call
import { create } from "@agntn/ciphers";
const vigenere = create("vigenere");
const encoded = vigenere.encode("ATTACK AT DAWN", { key: "LEMON" });
console.log(encoded.text); // LXFOPV EF RNHR
const decoded = vigenere.decode(encoded.text, { key: "LEMON" });
console.log(decoded.text); // ATTACK AT DAWN
The package ships seventy-two ciphers from one list. create(name) gives you one cached instance and wants the exact registry name. resolveCipher("Rail Fence") lowercases, turns spaces into hyphens, and then also wants an exact match. No prefixes, no fuzzy matching, a typo is an UnknownCipherError. Guessing which cipher you meant is how you get the wrong plaintext with a straight face.
The shape
Every encode and decode returns this:
interface CipherResult {
text: string; // the output
cipher: string; // "vigenere"
operation: "encode" | "decode";
options: Record<string, unknown>; // what was actually applied, defaults included
normalizedInput?: string; // optional, for a cipher that reshapes its input; the built-ins leave it out
}
Every classical cipher understands two options. preserveCase (default true) keeps lowercase letters lowercase. stripNonAlpha (default false) drops everything that isn't a letter before the cipher runs. The rest is per cipher: shift, key, secondKey, transposition, square, rotation, omit, orientation, indicator, indicatorUnder, blanks, separator, zero, book, address, pick, start, rails, width, corner, path, a, b, period, letters, positions, rings, plugboard, iv, segment, blockSize, nonce, aad, counter, tagLength, tweak, endian, bytes, digest, algorithm, count, keyLength, iterations, salt. Each cipher's page lists its own, and so does create(name).info().
What ships
| Cipher | Family | Options | Self-inverse |
|---|---|---|---|
| caesar | shift | shift (1 to 25, default 3) | ✗ |
| rot13 | shift | ✓ | |
| rot47 | shift | ✓ | |
| atbash | reflection | ✓ | |
| substitution | mixed alphabet | key | ✗ |
| vigenere | polyalphabetic | key | ✗ |
| gronsfeld | polyalphabetic | key (digits 0 to 9) | ✗ |
| beaufort | polyalphabetic | key | ✓ |
| porta | polyalphabetic | key, rotation (left or right, default left) | ✓ |
| autokey | polyalphabetic | key | ✗ |
| running-key | polyalphabetic | key (a letter per letter of the text) | ✗ |
| trithemius | polyalphabetic | ✗ | |
| alberti | polyalphabetic | key, period | ✗ |
| quagmire-1 | polyalphabetic | key, indicator, indicatorUnder (default A) | ✗ |
| quagmire-2 | polyalphabetic | key, indicator, indicatorUnder (default A) | ✗ |
| quagmire-3 | polyalphabetic | key, indicator, indicatorUnder (default A) | ✗ |
| quagmire-4 | polyalphabetic | key, secondKey, indicator, indicatorUnder (default A) | ✗ |
| chaocipher | polyalphabetic | key (left alphabet), secondKey (right alphabet) | ✗ |
| affine | multiplicative | a (default 5), b (default 8) | ✗ |
| playfair | digraph | key | ✗ |
| four-square | digraph | key, secondKey, omit (j or q, default j) | ✗ |
| two-square | digraph | key, secondKey, orientation (vertical or horizontal, default vertical), omit (j or q, default j) | ✗ |
| hill | polygraphic | key | ✗ |
| polybius | fractionation | key optional | ✗ |
| nihilist | fractionation | key, square optional | ✗ |
| morse | fractionation | ✗ | |
| fractionated-morse | fractionation | key | ✗ |
| bacon | fractionation | letters (24 or 26, default 26) | ✗ |
| tap-code | fractionation | ✗ | |
| a1z26 | fractionation | separator (default -), zero optional | ✗ |
| book | homophonic | book (required), address (default word), pick (default word), start (default 1) | ✗ |
| adfgvx | fractionation | key optional, transposition optional | ✗ |
| bifid | fractionation | key optional, period (default 5) | ✗ |
| straddling-checkerboard | fractionation | key optional (28 cells), blanks (default 26) | ✗ |
| rail-fence | transposition | rails (default 3) | ✗ |
| columnar | transposition | key | ✗ |
| route | transposition | width, corner (default top-left), path (default spiral-clockwise) | ✗ |
| enigma | rotor | positions, rings, plugboard | ✓ |
| aes | substitution-permutation | key (32, 48 or 64 hex digits) | ✗ |
| aes-cbc | substitution-permutation | key (32, 48 or 64 hex digits), iv (32 hex digits) | ✗ |
| aes-cfb | substitution-permutation | key (32, 48 or 64 hex digits), iv (32 hex digits), segment (default 128) | ✗ |
| aes-ofb | substitution-permutation | key (32, 48 or 64 hex digits), iv (32 hex digits) | ✗ |
| aes-ctr | substitution-permutation | key (32, 48 or 64 hex digits), iv (32 hex digits, the first counter block) | ✗ |
| aes-ccm | substitution-permutation | key (32, 48 or 64 hex digits), nonce (14 to 26 hex digits), aad, tagLength (default 128) | ✗ |
| aes-ocb | substitution-permutation | key (32, 48 or 64 hex digits), nonce (2 to 30 hex digits), aad, tagLength (default 128) | ✗ |
| aes-lrw | substitution-permutation | key (64, 80 or 96 hex digits), tweak (default 1) | ✗ |
| aes-xts | substitution-permutation | key (64 or 128 hex digits), tweak (default 0) | ✗ |
| aes-cbc-mac | substitution-permutation | key (32, 48 or 64 hex digits) | ✗ |
| aes-passphrase | substitution-permutation | key (the passphrase, any text), digest (default md5), keyLength (default 256), iterations (default 1), salt (default random) | ✗ |
| rijndael | substitution-permutation | key (32, 40, 48, 56 or 64 hex digits), blockSize (default 128) | ✗ |
| des | feistel | key (16 hex digits) | ✗ |
| desx | feistel | key (48 hex digits) | ✗ |
| triple-des | feistel | key (32 or 48 hex digits) | ✗ |
| triple-des-cbc | feistel | key (32 or 48 hex digits), iv (16 hex digits) | ✗ |
| blowfish | feistel | key (8 to 112 hex digits, an even number) | ✗ |
| idea | lai-massey | key (32 hex digits) | ✗ |
| lucifer | feistel | key (32 hex digits) | ✗ |
| mars | feistel | key (32 to 112 hex digits, in steps of 8) | ✗ |
| serpent | substitution-permutation | key (32, 48 or 64 hex digits) | ✗ |
| cast5 | feistel | key (10 to 32 hex digits, whole bytes) | ✗ |
| camellia | feistel | key (32, 48 or 64 hex digits) | ✗ |
| aria | substitution-permutation | key (32, 48 or 64 hex digits) | ✗ |
| sm4 | feistel | key (32 hex digits) | ✗ |
| openpgp | substitution-permutation | key (the passphrase, any text), algorithm (default aes256), digest (default sha512), count (default 65011712), salt and iv (default random) | ✗ |
| rabbit | arx | key (32 hex digits), iv (16 hex digits, optional), endian (default big) | ✗ |
| rc4 | permutation | key (2 to 512 hex digits, whole bytes) | ✗ |
| xor | polyalphabetic | key (any whole bytes in hex), bytes (default text) | ✗ |
| salsa20 | arx | key (32 or 64 hex digits), nonce (16 hex digits), counter (default 0) | ✗ |
| xsalsa20 | arx | key (64 hex digits), nonce (48 hex digits), counter (default 0) | ✗ |
| chacha20 | arx | key (64 hex digits), nonce (24 hex digits), counter (default 0) | ✗ |
| xchacha20 | arx | key (64 hex digits), nonce (48 hex digits), counter (default 0) | ✗ |
| chacha20-poly1305 | arx | key (64 hex digits), nonce (24 hex digits), aad | ✗ |
Errors
import { CipherError, InvalidOptionError, MissingOptionError, UnknownCipherError } from "@agntn/ciphers";
try {
create("caesar").encode("HELLO", { shift: 26 });
} catch (error) {
if (error instanceof InvalidOptionError) {
error.option; // "shift"
error.reason; // why 26 is not a shift
}
}
No key where a key is required, MissingOptionError. A name nobody registered, UnknownCipherError, and its message lists the names that are, so rot-13 comes back with rot13 in the same line. A value out of range, InvalidOptionError with the option, the value and the reason. Anything else a cipher throws gets wrapped by normalizeError into a CipherError that names the cipher. The CLI prints the message and exits 1. No stack trace. You did nothing wrong, the input did.
Next
- Encode and decode: options, case, what happens to punctuation, the round trip.
- Analysis: brute force a Caesar, read a histogram.
- CLI: the
cipherscommand. - Agents: the tools over MCP, Pi and OMP.
- Custom ciphers: extend
Cipher, register it. - Playground: every cipher, in the page.