A historically accurate, zero-dependency Enigma machine in Node.js: rotors I–VIII, reflectors, ring settings, plugboard and the double step. Supports Strings, Buffers, and Streams.
Try it live in your browser → Set the rotors, plug the cables and type, no install needed. · Watch the 49s video
More short package videos on @gykhdev. Sibling packages: @gykh/morse, @gykh/vigenere-cipher, @gykh/caesar-cipher, @gykh/cat-facts. All packages: @gykh on npm.
- 🚀 Zero Dependencies: Pure native Node.js implementation.
- 🎯 Historically accurate: Real rotor wirings I–VIII, reflectors A/B/C, ring settings, plugboard and the middle-rotor double step. Tested against a genuine 1941 German Army message.
- 🔁 Reciprocal: The same settings encrypt and decrypt.
- ⌨️ Key by key: The
Enigmaclass lets you press keys and watch the rotors turn. - 🔄 Multi-Format Support: Strings, Buffers, and Node.js Streams.
- 🌊 Chunk-safe streams: The rotors keep turning across chunks, so streamed output matches encrypting all at once.
- 📦 Dual ESM & CommonJS with full TypeScript types.
pnpm add @gykh/enigma
# or
npm install @gykh/enigma
# or
yarn add @gykh/enigmaRequires Node.js 18 or newer.
Letters A-Z / a-z are enciphered and keep their case. Spaces, digits and punctuation are preserved and do not turn the rotors.
// ESM
import { encryptString, decryptString } from "@gykh/enigma";
// CommonJS
// const { encryptString, decryptString } = require("@gykh/enigma");
const settings = {
rotors: ["II", "IV", "V"],
reflector: "B",
rings: "BUL",
positions: "BLA",
plugboard: "AV BS CG DL FU HZ IN KM OW RX",
};
encryptString("Attack at dawn", settings); // "Evhqzv rr blri"
decryptString("Evhqzv rr blri", settings); // "Attack at dawn"
encryptString("AAAAA"); // "BDZGO" with the default settingsimport { Enigma } from "@gykh/enigma";
const machine = new Enigma({ positions: "ADU" });
machine.press("A"); // "E" rotors now ADV
machine.press("A"); // "Q" rotors now AEW
machine.press("A"); // "I" rotors now BFX: the double step
machine.positions; // "BFX"
machine.reset(); // back to ADU
machine.type("HELLO"); // "IBXXX": types a string, rotors keep turningThis is the first part of an Operation Barbarossa message from 7 July 1941:
const key = { rotors: ["II", "IV", "V"], reflector: "B", rings: [2, 21, 12], plugboard: "AV BS CG DL FU HZ IN KM OW RX" };
// The header carried the message key KCH, enciphered at WXC
decryptString("KCH", { ...key, positions: "WXC" }); // "BLA"
decryptString("EDPUD NRGYS ZRCXN UYTPO MRMBO ...", { ...key, positions: "BLA" });
// "AUFKL XABTE ILUNG XVONX KURTI NOWAX ..." (Aufklärungsabteilung von Kurtinowa)import { encrypt, decrypt } from "@gykh/enigma";
import { readFile } from "fs/promises";
const buffer = await readFile("sample.txt");
const encryptedBuffer = encrypt(buffer, settings);
const decryptedBuffer = decrypt(encryptedBuffer, settings);
console.log(buffer.equals(decryptedBuffer)); // trueFor files or streams exceeding 1000 characters/bytes, use the streaming transform classes:
import { EncryptTransform, DecryptTransform } from "@gykh/enigma";
import fs from "fs";
import { pipeline } from "stream/promises";
await pipeline(
fs.createReadStream("large-input.txt"),
new EncryptTransform(settings),
fs.createWriteStream("large-encrypted.txt")
);
await pipeline(
fs.createReadStream("large-encrypted.txt"),
new DecryptTransform(settings),
fs.createWriteStream("large-decrypted.txt")
);Every field is optional. Omitted fields use the defaults.
| Field | Meaning | Values | Default |
|---|---|---|---|
rotors |
Walzenlage: rotor order, left to right | three different of I–VIII |
["I", "II", "III"] |
reflector |
Umkehrwalze | A, B, C |
"B" |
rings |
Ringstellung: ring settings | "AAA" or [1, 1, 1] (1–26) |
"AAA" |
positions |
Grundstellung: starting rotor positions | "AAA" or [1, 1, 1] |
"AAA" |
plugboard |
Steckerbrett: letter pairs | "AV BS CG" or ["AV", "BS"], each letter once |
none |
str(string, max 1000 chars): Input text.settings(object): See Settings.- Returns:
string. Both functions do the same thing, since Enigma is reciprocal.
buffer(Buffer, max 1000 bytes): Input buffer.- Returns:
Buffer
A stateful machine whose rotors keep turning between calls.
.press(letter): Press one key, returns the lit lamp..type(str): Encipher a string (max 1000 chars) from the current positions..positions: Rotor window letters, e.g."BFX"..reset(): Return to the starting positions.
Node.js stream.Transform subclasses. The rotors keep turning between chunks.
Note
The string and buffer functions enforce an input limit of 1000 characters/bytes. For larger data, pipe through EncryptTransform / DecryptTransform.
- Step. Each key press turns the right rotor one place before the current flows. When a rotor passes its notch, it turns the rotor to its left. The middle rotor also turns itself when it sits on its notch, so it moves on two presses in a row: the famous double step.
- Plugboard. The letter is swapped with its cable partner, if it has one.
- Rotors, right to left. Each rotor is a scrambled wiring, offset by its position minus its ring setting.
- Reflector. It sends the current back through the rotors along a different path.
- Rotors left to right, then the plugboard again. The lamp lights up.
Because the reflector pairs letters up, the machine is reciprocal, and a letter can never encrypt to itself. That flaw helped Allied codebreakers at Bletchley Park break Enigma. Use this package for learning, puzzles and CTFs, not for protecting data.
- Sylvester Das — Website • MiniFyn • Buy Me A Coffee
MIT © 2026 get-your-knowledge-here
