维吉尼亚密码(又译维热纳尔密码)是使用一系列凯撒密码组成密码字母表的加密算法,属于多表密码的一种简单形式。
为了说清楚维吉尼亚密码得从移位替换密码说起,比较典型的就是凯撒密码。
恺撒密码是一种替换加密的技术,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。
例如,当偏移量是3的时候,所有的字母A将被替换成D,B变成E,以此类推。
因为概率论的出现这种简单的移位或替换就容易破解了,其原理很简单,英文中字母出现的频率是不一样的。比如字母e是出现频率最高的,占12.7%;其次是t,9.1%;然后是a,o,i,n等,最少的是z,只占0.1%。
英语中字母频率统计
除了英语,其它语言也有详细统计。
各语言中字母频率统计
只要字符总量足够,全部收集到一起,统计各个字符出现的频率,然后再加上字母前后的关联关系,以及所要加密的语言本身语法搭配就可大幅度降低字母
的排列组合的可能性,这样密码就破解了。
当然维吉尼亚密码也属于古典密码学的范畴,都是对单个字符或符号进行移位或替代
维吉尼亚加密原理
核心:为了掩盖字母使用中暴露的频率特征,解决的办法就是用多套符号代替原来的文字。
比如原来的字母是A,从前只把它替换成F,现在把它替换成F或者G这两个。那什么时候用F什么时候用G呢?可以自行规定,比如说,字母在奇数位时用F代替,字母在偶数位时用G代替。
这样频率分析法暂时失效了。
而且,多套符号加密法并没满足于2~3套,后来典型使用的是26套,这就是第三代密码“维吉尼亚加密法”。
它是一个表格,第一行代表原文的字母,下面每一横行代表原文分别由哪些字母代替,每一竖列代表我们要用第几套字符来替换原文。一共26个字母,一共26套代替法,所以这个表是一个26*26的表。
维吉尼亚密码表
每一行就可以代表一套凯撒密码加密方法。
加密方法
加密公式:C = (P + K)%26
C:密文
P:原文
K:第几套加密方式
使用第几套加密方式是通过约定一个规则来确定的,这个规则就是“密钥”。
这样一个密钥字母代表一套加密方式,比如:t代表第19套加密方式,h代表第7套加密方式,i代表第8套加密方式,s代表第18套加密方式。
这样密钥和原文每个字符一一对应,如果密钥长度不足,那么循环替代。
原文中的两个“l”分别加密成T和D,而且密文中的同样的字符也可能代表不同的原文,比如密文中的L,分别代表了原文中的“e”和“d”。
解密方法
解密公式:P = (C - K)%26
C:密文
P:原文
K:第几套加密方式
如果P<0,P+26取得正序
代码实现
1 import java.util.ArrayList; 2 import java.util.Arrays; 3 import java.util.List; 4 import java.util.Optional; 5 /** 6 * 维吉尼亚加密解密 7 * @author 逆熵 8 * 9 */ 10 public class VirginiaPassword { 11 12 //Virginia Password table 13 private static final String VIRGINIA_PASSWORD_TABLE_ARRAY[] = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}; 14 public static final List<String> VIRGINIA_PASSWORD_TABLE_ARRAY_LIST = Arrays.asList(VIRGINIA_PASSWORD_TABLE_ARRAY); 15 16 /** 17 * 18 * 判断对象是否为空,为空返回false,非空返回true 19 * @param object 20 * @return 21 */ 22 public static boolean isNotNull(Object object) { 23 return Optional.ofNullable(object).isPresent(); 24 } 25 26 /** 27 * 判断字符串非null,并且不为空字符串 28 * @param str 29 * @return 30 */ 31 public static boolean isStringNotEmpty(String str) { 32 return isNotNull(str)&&str.trim()!=""; 33 } 34 35 /** 36 * C = P + K (mod 26). 37 * 维吉尼亚加密 38 * @param original 原文 39 * @param secretKey 密钥 40 * @return 41 */ 42 public static String virginiaEncode(String original,String secretKey) { 43 if(isStringNotEmpty(secretKey)&&isStringNotEmpty(original)){ 44 char[] secretCharArray = secretKey.toCharArray(); 45 char[] originalCharArray = original.toCharArray(); 46 int length = originalCharArray.length; 47 48 List<Integer> list = getSecretKeyList(secretCharArray, length); 49 50 StringBuffer sb = new StringBuffer(); 51 for(int m=0;m<length;m++) { 52 char ch = originalCharArray[m]; 53 int charIndex = VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.indexOf(String.valueOf(ch).toUpperCase()); 54 if(charIndex==-1) { 55 sb.append(String.valueOf(ch)); 56 continue; 57 } 58 59 int size = VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.size(); 60 //C = P + K (mod 26). 获取偏移量索引 61 int tmpIndex = (charIndex + list.get(m))%size; 62 sb.append(VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.get(tmpIndex)); 63 64 } 65 return sb.toString(); 66 } 67 return null; 68 } 69 70 /** 71 * P = C - K (mod 26). 72 * 维吉尼亚解密 73 * @param cipherText 密文 74 * @param secretKey 密钥 75 * @return 76 */ 77 public static String virginiaDecode(String cipherText,String secretKey) { 78 if(isStringNotEmpty(cipherText)&&isStringNotEmpty(secretKey)) { 79 char[] secretCharArray = secretKey.toCharArray(); 80 char[] cipherCharArray = cipherText.toCharArray(); 81 int length = cipherCharArray.length; 82 83 List<Integer> list = getSecretKeyList(secretCharArray, length); 84 StringBuffer sb = new StringBuffer(); 85 for(int m=0;m<length;m++) { 86 char ch = cipherCharArray[m]; 87 int charIndex = VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.indexOf(String.valueOf(ch).toUpperCase()); 88 if(charIndex==-1) { 89 sb.append(String.valueOf(ch)); 90 continue; 91 } 92 93 int size = VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.size(); 94 //P = C - K (mod 26). 模逆运算求索引 95 int len = (charIndex - list.get(m))%size; 96 //索引小于零,加模得正索引 97 int tmpIndex = len<0?len+size:len; 98 sb.append(VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.get(tmpIndex)); 99 100 } 101 102 return sb.toString(); 103 } 104 105 return null; 106 } 107 108 /** 109 * 获取密钥集合 110 * @param secretCharArray 密钥字符数组 111 * @param length 原文或密文的长度 112 * @return 113 */ 114 private static List<Integer> getSecretKeyList(char[] secretCharArray, int length) { 115 List<Integer> list = new ArrayList<Integer>(); 116 for (char c : secretCharArray) { 117 int index = VIRGINIA_PASSWORD_TABLE_ARRAY_LIST.indexOf(String.valueOf(c).toUpperCase()); 118 list.add(index); 119 } 120 121 122 if(list.size()>length) { 123 //截取和目标原文或密文相同长度的集合 124 list = list.subList(0, length); 125 }else { 126 Integer[] keyArray = list.toArray(new Integer[list.size()]); 127 int keySize = list.size(); 128 //整除 129 int count = length/keySize; 130 for(int i=2;i<=count;i++) { 131 for (Integer integer : keyArray) { 132 list.add(integer); 133 } 134 } 135 //求余 136 int mold = length%keySize; 137 if(mold>0) { 138 for(int j=0;j<mold;j++) { 139 list.add(keyArray[j]); 140 } 141 142 } 143 } 144 145 return list; 146 } 147 }
注:以上代码在JDK1.8上编译通过
维吉尼亚加密难以破解的关键是同一种原文对应上亿种密文,相同的密文也一样对应上亿种原文。
维吉尼亚密码的破解方法
以上代码是知道密钥的情况,通过加密的求余逆运算计算出来的,但是真实的情况下,我们只有密文,没有密钥,那么怎么破解维吉尼亚密码呢?
同样的原文“the”被两次加密成“BUK”,这是巧合?
其实不然,密钥KING由4个字母构成,而在密文中代表the的BUK间隔了8个字母,间隔距离刚好是密钥的2倍。
正好在KING这个密钥循环到整数倍的时候,如果也正好赶上出现了同样的原文,那巧合就出现了——原文就会被加密成相同的密文。
而此处即是破解维吉尼亚密码的关键。
在不知道密钥的情况下,分析出密钥的长度,那么密码就被破解了。
怎么分析出密钥的长度呢?
把密文中完全一样的单词挑出来,分析总结其规律。
密文对应的原文虽然理论上数量巨大,但拼写规则一卡,数据量就急剧减少了。
而且密文里的字符串长度越长,对应真实存在的单词数量就越少。
你可以翻翻字典,或者在头脑中找找4个字母的单词和12个字母的单词,看看其数量对比。
步骤:
1.从密文中找出拼写完全相同的字符串
2.数第一次到第二次出现中间间隔的字母数
如果间隔了30个字母
密钥长度为2,那么正好反复出现了15次;
密钥长度为3,那么正好反复出现了10次;
密钥长度为5,那么正好反复出现了6次;
密钥长度为6,那么正好反复出现了5次;
密钥长度为30,那么正好反复出现了1次。
比如还存在B、C、D,只要看同样的因数都出现,那么这个因数就是密钥的长度。
我们通过密钥的长度,就可以使用频率分析方法把原文分析出来。
比如我们知道密钥的长度为6,那么第1、7、13等字母放到一组,因为它们都是通过同一套加密方式加密出来的,然后用频率分析法。
其它组别也按照此方式分析出来。
所以,猜中密钥长度就等于,把维吉尼亚密码化简为N套最基础的移位法。