Skip to content

Instantly share code, notes, and snippets.

@erwinv
Created October 4, 2020 07:19
Show Gist options
  • Save erwinv/ea5afb1ca4102b86b78d8c1915ebf8cf to your computer and use it in GitHub Desktop.
Save erwinv/ea5afb1ca4102b86b78d8c1915ebf8cf to your computer and use it in GitHub Desktop.
Vigenere Cipher
// https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher#Description
class VigenereCipher {
constructor(...keys) {
this.keys = Array.from(keys).map(normalize)
}
get key() { return this._key }
set key(key) {
this._keys = [key]
this._key = normalize(key)
}
get keys() { return this._keys }
set keys(keys) {
// https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher#Variants
function combine(acc, keys) {
if (keys.length === 0) {
return acc
}
const [nextKey, ...rest] = keys
return combine(
VigenereCipher.encrypt(acc, nextKey, true /* symmetric */),
rest
)
}
this._keys = keys
this._key = combine(keys[0], keys.slice(1))
}
_alphabet = memoize(alphabet)
encrypt(plainText, symmetric = false) {
return this.process(normalize(plainText), keyChar => ({
reference: this._alphabet(),
target: this._alphabet(keyChar),
}), symmetric)
}
decrypt(cipherText) {
return this.process(cipherText, keyChar => ({
reference: this._alphabet(keyChar),
target: this._alphabet(),
}))
}
process(text, resolver, symmetric = false) {
return zip(this._key, text, symmetric)
.map(([keyChar, textChar]) => ({
...resolver(keyChar),
point: textChar,
}))
.map(({reference, target, point}) => target.charAt(reference.indexOf(point)))
.join('')
}
static encrypt(key, plainText, symmetric = false) {
return new VigenereCipher(key).encrypt(plainText, symmetric)
}
static decrypt(key, cipherText) {
return new VigenereCipher(key).decrypt(cipherText)
}
}
function normalize(s) {
return `${s}`.split(/\s+/).join('').toUpperCase()
}
function zip(key, text, symmetric = false) {
const zippedLength = symmetric ?
lcm(key.length, text.length)
: text.length
return Array(zippedLength).fill()
.map((_, i) => [key[i % key.length], text[i % text.length]])
}
function alphabet(pivotChar) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
if (!pivotChar) return alphabet
const pivot = alphabet.indexOf(pivotChar)
return alphabet.slice(pivot) + alphabet.slice(0, pivot)
}
function memoize(fn) {
if (fn.length > 1) return fn
let cache = new Map()
return x => {
const key = Symbol.for(x)
if (!cache.has(key)) {
cache.set(key, fn(x))
}
return cache.get(key)
}
}
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b)
}
function lcm(a, b) {
return (a * b) / gcd(a, b)
}
const cipher = new VigenereCipher('lemon')
let cipherText = cipher.encrypt('attack at dawn')
console.log(
'cipher text:', cipherText,
'cipher key:', cipher.key,
'decrypted:', cipher.decrypt(cipherText)
)
cipher.keys = ['go', 'cat']
cipherText = cipher.encrypt('attack at dawn')
console.log(
'cipher text:', cipherText,
'cipher keys:', cipher.keys,
'combined key:', cipher.key,
'decrypted:', cipher.decrypt(cipherText)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment