Skip to content

Instantly share code, notes, and snippets.

@IGI-111
Created October 11, 2017 12:42
Show Gist options
  • Save IGI-111/d1e26d549ab59526eb1fca34e55dd00b to your computer and use it in GitHub Desktop.
Save IGI-111/d1e26d549ab59526eb1fca34e55dd00b to your computer and use it in GitHub Desktop.
Vigenere cypher solver
const fs = require('fs');
const englishFrequency = [
0.08167,
0.01492,
0.02782,
0.04253,
0.12702,
0.02228,
0.02015,
0.06094,
0.06966,
0.00153,
0.00772,
0.04025,
0.02406,
0.06749,
0.07507,
0.01929,
0.00095,
0.05987,
0.06327,
0.09056,
0.02758,
0.00978,
0.02360,
0.00150,
0.01974,
0.00074
];
function kasikiExamination (ciphertext) { // find probable keyword length
ciphertext = ciphertext.replace(/[^a-z0-9]/gi, '').toUpperCase();
// find intervals repeated occurences of three or more characters
const regex = /(\S{3,})(.*)\1/mig;
let factors = {};
let m;
while ((m = regex.exec(ciphertext)) !== null) {
let spacing = m[1].length +
m[2].length;
for (const factor of computeFactors(spacing)) {
if (factor in factors) {
factors[factor] += 1;
} else {
factors[factor] = 1;
}
}
}
delete factors[1];
let bestLen = 1;
let bestScore = 1;
for (const len in factors) {
if (factors[len] > bestScore) {
bestScore = factors[len];
bestLen = len;
} else if (factors[len] === bestScore && len > bestLen) {
bestLen = len;
}
}
return bestLen;
}
function solveVigenere (keylen, ciphertext) {
ciphertext = ciphertext
.replace(/[^a-z0-9]/gi, '')
.toUpperCase()
.split('')
.map((c) => c.charCodeAt() - 'A'.charCodeAt());
let frequencies = [];
for (let i = 0; i < keylen; ++i) {
frequencies[i] = [];
for (let j = 0; j < 26; ++j) {
frequencies[i][j] = 0;
}
}
// count every letter
for (let i = 0; i < ciphertext.length; ++i) {
frequencies[i % keylen][ciphertext[i]] += 1;
}
// transform into frequencies
for (const frequency of frequencies) {
let total = 0;
for (let i = 0; i < 26; ++i) {
total += frequency[i];
}
for (let i = 0; i < 26; ++i) {
frequency[i] /= total;
}
}
// find minimal error
let key = [];
for (const frequency of frequencies) {
let bestShift = 0;
let bestShiftScore = Infinity;
for (let shift = 0; shift < 26; ++shift) {
let shiftScore = 0;
for (let i = 0; i < 26; ++i) {
shiftScore += Math.abs(englishFrequency[i] - frequency[(i + shift) % 26]);
}
if (shiftScore < bestShiftScore) {
bestShiftScore = shiftScore;
bestShift = shift;
}
}
key.push(bestShift);
}
return String.fromCharCode.apply(null, key.map(c => 'A'.charCodeAt(0) + c));
}
function computeFactors (num) {
let factors = [];
for (let i = 1; i <= Math.floor(Math.sqrt(num)); i += 1) {
if (num % i === 0) {
factors.push(i);
if (num / i !== i) {
factors.push(num / i);
}
}
}
factors.sort((a, b) => a - b);// numeric sort
return factors;
}
function decrypt (ciphertext, key) {
key = key.split('').map((c) => c.charCodeAt() - 'A'.charCodeAt());
let k = 0;
for (let i = 0; i < ciphertext.length; ++i) {
if (/[^a-z0-9]/gi.test(ciphertext[i])) {
continue;
}
const shift = key[k++ % key.length];
let letter = ciphertext[i].toUpperCase().charCodeAt() - 'A'.charCodeAt();
letter = (letter + 26 - shift) % 26;
ciphertext = ciphertext.substr(0, i) +
String.fromCharCode(letter + 'A'.charCodeAt()) + ciphertext.substr(i + 1);
}
return ciphertext;
}
const ciphertext = fs.readFileSync(process.argv[2], 'utf-8');
const keylen = kasikiExamination(ciphertext);
console.log(keylen);
const key = solveVigenere(keylen, ciphertext);
console.log(key);
console.log(decrypt(ciphertext, key));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment