Guide

Encode and decode

The two methods every cipher has, the two options they share, and what happens to case and punctuation on the way through.

Two methods

import { create, resolveCipher } from "@agntn/ciphers";

const caesar = create("caesar");
caesar.encode("Hello, World!", { shift: 7 }).text; // "Olssv, Dvysk!"
caesar.decode("Olssv, Dvysk!", { shift: 7 }).text; // "Hello, World!"

resolveCipher("Rail Fence").encode("WEAREDISCOVEREDRUNATONCE", { rails: 3 }).text;
// "WECRUOERDSOEERNTNEAIVDAC"

create wants the exact registry name. resolveCipher lowercases and turns runs of whitespace into a hyphen, so "Rail Fence" and "rail-fence" are the same thing - and then it also wants an exact match. Both cache one instance per name.

Options every cipher shares

OptionDefaultEffect
preserveCasetrueLowercase in, lowercase out. false uppercases the output.
stripNonAlphafalseDrop everything that is not an ASCII letter before the cipher runs.

With the defaults a Caesar leaves spaces, digits and punctuation exactly where they were and shifts the letters around them. A puzzle that wants one solid block gets stripNonAlpha: true:

create("vigenere").encode("ATTACK AT DAWN", { key: "LEMON", stripNonAlpha: true }).text;
// "LXFOPVEFRNHR"

options on the result echoes what was applied, both flags included when you set them. One log line and you can rerun the call.

Ciphers that reshape the input

Some ciphers can't leave the input alone, so the round trip comes back reshaped:

  • Playfair works on letter pairs. It strips non-letters, folds J into I, splits doubled letters with X and pads an odd length with X.
  • ADFGVX keeps letters and digits and drops the rest.
  • Bifid and Polybius fold J into I, and Bifid drops non-letters.
  • Morse, Bacon and tap code change the alphabet entirely. You get uppercase back and, for Morse, single spaces.

For everything else decode(encode(x)) === x holds for any input. Emoji, Polish letters, whatever - they pass through untouched.

Self-inverse ciphers

ROT-13, ROT-47, Atbash and Enigma are their own inverse. encode and decode do the same thing and info().selfInverse says so. Both methods still exist on every cipher, so you can write decode and mean it.

const rot13 = create("rot13");
rot13.decode(rot13.encode("HELLO").text).text === "HELLO"; // true
rot13.encode("URYYB").text; // "HELLO", same thing

Options per cipher

Each cipher validates its own options and lists them in info().options with a type, whether it is required, a default and a description. The Playground form and ciphers info are built from that list, nothing is typed twice:

create("affine").info().options;
// [
//   { name: "a", type: "number", required: false, default: 5, description: "Multiplier (must be coprime with 26: ...)" },
//   { name: "b", type: "number", required: false, default: 8, description: "Additive shift (0-25)" },
// ]

In your own cipher read one with getOpt(options, "shift", 3). Fallback when the key is absent, the value as typed otherwise.

What a wrong option does

create("caesar").encode("X", { shift: 0 }); // InvalidOptionError, 0 is the identity
create("affine").encode("X", { a: 13 }); // InvalidOptionError, gcd(13, 26) is 13
create("vigenere").encode("X"); // MissingOptionError: key
create("enigma").encode("X", { plugboard: "AB AC" }); // InvalidOptionError, a letter in two pairs

All of them thrown before the first character is touched. A cipher that quietly accepts shift 0 and hands your text back unchanged is worse than one that throws.

@agntn/ciphers·MIT license· Classical ciphers, for lessons and puzzles. Not for protecting anything, ever.