问题简述
维吉尼亚密码是使用一系列凯撒密码组成密码字母表的加密算法,属于多表密码的一种简单形式。
在一个凯撒密码中,字母表中的每一字母都会作一定的偏移,例如偏移量为3时,A就转换为了D、B转换为了E……而维吉尼亚密码则是由一些偏移量不同的凯撒密码组成。
为了生成密码,需要使用表格法。这一表格(如图所示)包括了26行字母表,每一行都由前一行向左偏移一位得到。具体使用哪一行字母表进行编译是基于密钥进行的,在过程中会不断地变换。
例如,假设明文为:
ATTACKATDAWN
选择某一关键词并重复而得到密钥,如关键词为LEMON时,密钥为:
LEMONLEMONLE
对于明文的第一个字母A,对应密钥的第一个字母L,于是使用表格中L行字母表进行加密,得到密文第一个字母L。类似地,明文第二个字母为T,在表格中使用对应的E行进行加密,得到密文第二个字母X。以此类推,可以得到:
明文:ATTACKATDAWN密钥:LEMONLEMONLE密文:LXFOPVEFRNHR
解密的过程则与加密相反。例如:根据密钥第一个字母L所对应的L行字母表,发现密文第一个字母L位于A列,因而明文第一个字母为A。密钥第二个字母E对应E行字母表,而密文第二个字母X位于此行T列,因而明文第二个字母为T。以此类推便可得到明文。
用数字0-25代替字母A-Z,维吉尼亚密码的加密文法可以写成同余的形式:
解密方法则能写成:
维吉尼亚由于其加密的复杂性和密钥的不确定性曾经一度被称为“不可破解的密码”。
破解实现
我选择的破解方法是重合指数法。
①我们先编写一个能把密文按照某个步长进行分组的函数makeGroup(),参数beforeText为密文内容,参数step为分组的步长。
1. string* makeGroup(string beforeText, int step) {
2. string* afterText = new string[step];
3. long length = beforeText.length();
4. for (long i = 0; i < length; i++) {
5. afterText[i % step] += beforeText[i];
6. }
7. return afterText;
8. };
②接下来我们就可以开始实质的第一步,那就是先推测密钥的长度,我们按照不同步长对密文进行分组,随后计算出对应的重合指数(公式如下)并打印到控制台。
//求密钥长度,根据不同密钥长度对应的重合指数,正常文本为0.0635左右
int GetKeyLength(string cipherText) {
//尝试1-30的密钥长度
double smallest = 1;
int keyLength = 0;
for (int i = 1; i < 30; i++) {
double index = 0;
string* afterText = makeGroup(cipherText, i);
for (int j = 0; j < i; j++) {
double num = 0;
//ASCII码97-122对应a-z
for (int p = 97; p < 123; p++) {
int count = 0;
for (int q = 0; q < afterText[j].length(); q++) {
if (afterText[j][q] == p) {
count++;
}
}
num += (double(count) / afterText[j].length()) * ((double(count) - 1) / (afterText[j].length() - 1));
}
index += num;
}
cout <<"密钥长度为"<<i<<"时,重合指数="<< index / i << endl;
}
return keyLength;
}
运行该函数即可得到如下:
所以经过分析,密钥的长度应该是8的倍数。
③接下来我们需要将推测的密钥长度代入,按照推测的密钥长度对明文分组,对每组的字母进行频率分析,进而得到该密钥长度对应的密钥。
//频率分析,确定e对应的密钥
string frequencyCount(string cipherText, int step) {
string* afterText = makeGroup(cipherText, step);
string key;
for (int i = 0; i < step; i++) {
//ASCII码97-122对应a-z
int E = 97;
double max = 0;
for (int j = 97; j < 123; j++) {
int count = 0;
for (int k = 0; k < afterText[i].length(); k++) {
if (afterText[i][k] == j) {
count++;
}
}
cout << char(j) << "的频率=" << double(100 * count) / afterText[i].length() << endl;
if ((double(100 * count) / afterText[i].length()) > max) {
max = (double(100 * count) / afterText[i].length());
E = j;
}
}
key += char(E);
}
return key;
}
控制台得到的输出如下,我们找到频率最高的两个或者三个,它们很有可能就是密钥中e对应的字母。
可知e最有可能变成了p或t,通过查表逆推得到对应的密钥字母为L或者P,保险起见两种可能在解密时都要试一试。
当然,我们还可以把查表这个过程变成程序来实现,简单高效,避免查表的繁琐。
//获取密钥
string GetKey(string cipherText, int keyLength) {
string key = frequencyCount(cipherText, keyLength);
for (int i = 0; i < key.length(); i++) {
key[i] = (key[i] - 101 + 26) % 26 + 'a';
}
cout << "根据频率分析法密钥最可能为:" << key << endl;
return key;
}
通过这种方法我们得到了密钥的几种可能的形式,如lagrange、lagrwnve等等,其实现在就可以猜测密钥大概率就是lagrange,因为只有lagrange有实际意义。
④既然得到了密钥,下一步就是解密了,解密的方法就是按照密钥长度对密文进行分组,然后按照对应的密钥字进行查表逆推即可,我们还是把它写成了程序。
//密文解密,参数key为密钥
void decrypt(string cipherText, string key) {
//分组
string* afterText = makeGroup(cipherText, key.length());
//对照密码表倒推替换
for (int i = 0; i < key.length(); i++) {
for (int j = 0; j < afterText[i].length(); j++) {
afterText[i][j] = (afterText[i][j] - key[i] + 26) % 26 + 'a';
}
}
//组装明文内容
string plainText;
for (int j = 0; j < afterText[0].length(); j++) {
for (int i = 0; i < key.length() && j < afterText[i].length(); i++) {
plainText += afterText[i][j];
}
}
cout << plainText << endl;
}