Skip to content

Repository files navigation

@gykh/vigenere-cipher

Vigenère Cipher Decoder & Encoder

A fast, zero-dependency Vigenère cipher implementation in Node.js supporting Strings, Buffers, Streams, and key-cracking by frequency analysis.

npm version npm downloads CI Tests node version TypeScript License: MIT Zero Dependencies

Try it live in your browser → Encrypt, decrypt and crack the key, no install needed. · Watch the 30s video

More short package videos on @gykhdev. Sibling packages: @gykh/morse, @gykh/enigma, @gykh/caesar-cipher, @gykh/cat-facts. All packages: @gykh on npm.


Contents

Features

  • 🚀 Zero Dependencies: Pure native Node.js implementation.
  • 🔑 Keyword cipher: Each letter of the key picks a different Caesar shift, so the same letter encrypts differently along the message.
  • 🔄 Multi-Format Support: Encrypt and decrypt Strings, Buffers, and Node.js Streams.
  • 🌊 Chunk-safe streams: The key position carries across chunks, so streamed output matches encrypting all at once.
  • 🕵️ Cracking: crack(str) recovers an unknown key from English ciphertext using the index of coincidence and chi-squared frequency analysis.
  • 📦 Dual ESM & CommonJS with full TypeScript types.

Install

pnpm add @gykh/vigenere-cipher
# or
npm install @gykh/vigenere-cipher
# or
yarn add @gykh/vigenere-cipher

Requires Node.js 18 or newer.


Usage

Letters A-Z / a-z are shifted by the matching key letter (A = 0, B = 1 … Z = 25) and keep their case. Spaces, digits and punctuation are preserved and do not advance the key.

1. Strings (ESM & CommonJS)

// ESM
import { encryptString, decryptString, crack } from "@gykh/vigenere-cipher";

// CommonJS
// const { encryptString, decryptString, crack } = require("@gykh/vigenere-cipher");

encryptString("ATTACKATDAWN", "LEMON"); // "LXFOPVEFRNHR"
decryptString("LXFOPVEFRNHR", "LEMON"); // "ATTACKATDAWN"

encryptString("Attack at dawn! 123", "lemon"); // "Lxfopv ef rnhr! 123"

2. Crack an unknown key

const secret = encryptString(longEnglishText, "CIPHER");

const { key, text } = crack(secret);
console.log(key);  // "CIPHER"
console.log(text); // the original text

Cracking is statistical: it needs English text and works best with 100+ letters of ciphertext. Short messages may return a wrong key.

3. Buffers

import { encrypt, decrypt } from "@gykh/vigenere-cipher";
import { readFile } from "fs/promises";

const buffer = await readFile("sample.txt");

const encryptedBuffer = encrypt(buffer, "LEMON");
const decryptedBuffer = decrypt(encryptedBuffer, "LEMON");

console.log(buffer.equals(decryptedBuffer)); // true

4. Streams (For Large Files)

For files or streams exceeding 1000 characters/bytes, use the streaming transform classes:

import { EncryptTransform, DecryptTransform } from "@gykh/vigenere-cipher";
import fs from "fs";
import { pipeline } from "stream/promises";

await pipeline(
  fs.createReadStream("large-input.txt"),
  new EncryptTransform("LEMON"),
  fs.createWriteStream("large-encrypted.txt")
);

await pipeline(
  fs.createReadStream("large-encrypted.txt"),
  new DecryptTransform("LEMON"),
  fs.createWriteStream("large-decrypted.txt")
);

API Reference

encryptString(str, key)

  • str (string, max 1000 chars): Plaintext.
  • key (string, 1–100 letters A-Z, any case): Keyword.
  • Returns: string

decryptString(str, key)

  • str (string, max 1000 chars): Ciphertext.
  • key (string): Keyword used during encryption.
  • Returns: string

crack(str, options?)

Recovers the key of English ciphertext without knowing it.

  • str (string, max 1000 chars): Ciphertext.
  • options.maxKeyLength (number, 1–100, default 20): Longest key length to try.
  • Returns: { key: string, text: string }, where key is uppercase.

encrypt(buffer, key) / decrypt(buffer, key)

  • buffer (Buffer, max 1000 bytes): Input buffer.
  • key (string): Keyword.
  • Returns: Buffer

new EncryptTransform(key) / new DecryptTransform(key)

Node.js stream.Transform subclasses. The key position carries over between chunks.

Note

The string and buffer functions and crack enforce an input limit of 1000 characters/bytes. For larger data, pipe through EncryptTransform / DecryptTransform.


How the cracker works

  1. Find the key length. For each candidate length n, split the letters into n columns. When n is right, each column is a plain Caesar cipher and its index of coincidence jumps to English levels (≈ 0.066 vs ≈ 0.038 for random text). The shortest length close to the best score wins, since multiples of the key length score equally well.
  2. Solve each column. Try all 26 shifts per column and keep the one whose letter counts best match English (lowest chi-squared).
  3. Decrypt with the recovered key.

The Vigenère cipher is not secure. It resisted attack for three centuries but is broken in milliseconds today. Use it for learning, puzzles and CTFs, not for protecting data.


Developer


License

MIT © 2026 get-your-knowledge-here

Releases

Contributors

Languages