Guide

Getting Started

Install, encode one line, decode it back. Same shape from every cipher.
The API and the tool list can still move. Pin exact versions if you build on it now.This code has never had a security audit. Do not use it in production, with real funds or with sensitive data. It is meant for agents, puzzles and local experiments only. It comes as is, without warranty of any kind, and the authors are not liable for any loss, as the MIT license states. Anything that matters wants an audited library.Lessons, riddles, CTFs, yes. Secrets, no. Anyone with a laptop breaks the classical ciphers in seconds. Half the block ciphers run in ECB, which shows every block that repeats, and none of the code was made constant time.

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

shell
pnpm add @agntn/ciphers

First call

tsvigenere.ts
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:

ts
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

CipherFamilyOptionsSelf-inverse
caesarshiftshift (1 to 25, default 3)✗
rot13shift✓
rot47shift✓
atbashreflection✓
substitutionmixed alphabetkey✗
vigenerepolyalphabetickey✗
gronsfeldpolyalphabetickey (digits 0 to 9)✗
beaufortpolyalphabetickey✓
portapolyalphabetickey, rotation (left or right, default left)✓
autokeypolyalphabetickey✗
running-keypolyalphabetickey (a letter per letter of the text)✗
trithemiuspolyalphabetic✗
albertipolyalphabetickey, period✗
quagmire-1polyalphabetickey, indicator, indicatorUnder (default A)✗
quagmire-2polyalphabetickey, indicator, indicatorUnder (default A)✗
quagmire-3polyalphabetickey, indicator, indicatorUnder (default A)✗
quagmire-4polyalphabetickey, secondKey, indicator, indicatorUnder (default A)✗
chaocipherpolyalphabetickey (left alphabet), secondKey (right alphabet)✗
affinemultiplicativea (default 5), b (default 8)✗
playfairdigraphkey✗
four-squaredigraphkey, secondKey, omit (j or q, default j)✗
two-squaredigraphkey, secondKey, orientation (vertical or horizontal, default vertical), omit (j or q, default j)✗
hillpolygraphickey✗
polybiusfractionationkey optional✗
nihilistfractionationkey, square optional✗
morsefractionation✗
fractionated-morsefractionationkey✗
baconfractionationletters (24 or 26, default 26)✗
tap-codefractionation✗
a1z26fractionationseparator (default -), zero optional✗
bookhomophonicbook (required), address (default word), pick (default word), start (default 1)✗
adfgvxfractionationkey optional, transposition optional✗
bifidfractionationkey optional, period (default 5)✗
straddling-checkerboardfractionationkey optional (28 cells), blanks (default 26)✗
rail-fencetranspositionrails (default 3)✗
columnartranspositionkey✗
routetranspositionwidth, corner (default top-left), path (default spiral-clockwise)✗
enigmarotorpositions, rings, plugboard✓
aessubstitution-permutationkey (32, 48 or 64 hex digits)✗
aes-cbcsubstitution-permutationkey (32, 48 or 64 hex digits), iv (32 hex digits)✗
aes-cfbsubstitution-permutationkey (32, 48 or 64 hex digits), iv (32 hex digits), segment (default 128)✗
aes-ofbsubstitution-permutationkey (32, 48 or 64 hex digits), iv (32 hex digits)✗
aes-ctrsubstitution-permutationkey (32, 48 or 64 hex digits), iv (32 hex digits, the first counter block)✗
aes-ccmsubstitution-permutationkey (32, 48 or 64 hex digits), nonce (14 to 26 hex digits), aad, tagLength (default 128)✗
aes-ocbsubstitution-permutationkey (32, 48 or 64 hex digits), nonce (2 to 30 hex digits), aad, tagLength (default 128)✗
aes-lrwsubstitution-permutationkey (64, 80 or 96 hex digits), tweak (default 1)✗
aes-xtssubstitution-permutationkey (64 or 128 hex digits), tweak (default 0)✗
aes-cbc-macsubstitution-permutationkey (32, 48 or 64 hex digits)✗
aes-passphrasesubstitution-permutationkey (the passphrase, any text), digest (default md5), keyLength (default 256), iterations (default 1), salt (default random)✗
rijndaelsubstitution-permutationkey (32, 40, 48, 56 or 64 hex digits), blockSize (default 128)✗
desfeistelkey (16 hex digits)✗
desxfeistelkey (48 hex digits)✗
triple-desfeistelkey (32 or 48 hex digits)✗
triple-des-cbcfeistelkey (32 or 48 hex digits), iv (16 hex digits)✗
blowfishfeistelkey (8 to 112 hex digits, an even number)✗
idealai-masseykey (32 hex digits)✗
luciferfeistelkey (32 hex digits)✗
marsfeistelkey (32 to 112 hex digits, in steps of 8)✗
serpentsubstitution-permutationkey (32, 48 or 64 hex digits)✗
cast5feistelkey (10 to 32 hex digits, whole bytes)✗
camelliafeistelkey (32, 48 or 64 hex digits)✗
ariasubstitution-permutationkey (32, 48 or 64 hex digits)✗
sm4feistelkey (32 hex digits)✗
openpgpsubstitution-permutationkey (the passphrase, any text), algorithm (default aes256), digest (default sha512), count (default 65011712), salt and iv (default random)✗
rabbitarxkey (32 hex digits), iv (16 hex digits, optional), endian (default big)✗
rc4permutationkey (2 to 512 hex digits, whole bytes)✗
xorpolyalphabetickey (any whole bytes in hex), bytes (default text)✗
salsa20arxkey (32 or 64 hex digits), nonce (16 hex digits), counter (default 0)✗
xsalsa20arxkey (64 hex digits), nonce (48 hex digits), counter (default 0)✗
chacha20arxkey (64 hex digits), nonce (24 hex digits), counter (default 0)✗
xchacha20arxkey (64 hex digits), nonce (48 hex digits), counter (default 0)✗
chacha20-poly1305arxkey (64 hex digits), nonce (24 hex digits), aad✗

Errors

ts
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