一、密码学基本概念
密码在我们的生活中有着重要的作用,那么密码究竟来自何方,为何会产生呢?
密码学是网络安全、信息安全、区块链等产品的基础,常见的非对称加密、对称加密、散列函数等,都属于密码学范畴。
密码学有数千年的历史,从最开始的替换法到如今的非对称加密算法,经历了古典密码学,近代密码学和现代密码学三个阶段。密码学不仅仅是数学家们的智慧,更是如今网络空间安全的重要基础。
二、密码学的发展历史
1、古典密码学
在古代的战争中,多见使用隐藏信息的方式保护重要的通信资料。比如先把需要保护的信息用化学药水写到纸上,药水干后,纸上看不出任何的信息,需要使用另外的化学药水涂抹后才可以阅读纸上的信息。
例:https://www.iqiyi.com/v_19rt6ab1hg.html
这些方法都是在保护重要的信息不被他人获取,但藏信息的方式比较容易被他人识破,例如增加哨兵的排查力度,就会发现其中的猫腻,因而随后发展出了较难破解的古典密码学。
【1】替换法
替换法很好理解,就是用固定的信息将原文替换成无法直接阅读的密文信息。例如将 b 替换成 w ,e 替换成 p ,这样 bee 单词就变换成了 wpp,不知道替换规则的人就无法阅读出原文的含义。
替换法有单表替换和多表替换两种形式。
单表替换即只有一张原文密文对照表单,发送者和接收者用这张表单来加密解密。
在上述例子中,表单即为:a b c d e - s w t r p 。
多表替换即有多张原文密文对照表单,不同字母可以用不同表单的内容替换。
例如约定好表单为:表单 1:abcde-swtrp 、表单2:abcde-chfhk 、表单 3:abcde-jftou。
规定第一个字母用第三张表单,第二个字母用第一张表单,第三个字母用第二张表单,这时 bee单词就变成了
(312)fpk ,破解难度更高,其中 312 又叫做密钥,密钥可以事先约定好,也可以在传输过程中标记出来。
【2】移位法
移位法就是将原文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后得出密文,典型的移位法应用有 "恺撒密码"。
例如约定好向后移动2位(abcde - cdefg),这样 bee 单词就变换成了dgg 。
同理替换法,移位法也可以采用多表移位的方式,典型的多表案例是“维尼吉亚密码”(又译维热纳尔密码),属于多表密码的一种形式。
![](https://files.mdnice.com/user/27110/a262eadd-8635-4466-a1fa-df8e45b74854.png)
【3】古典密码破解方式
古典密码虽然很简单,但是在密码史上是使用的最久的加密方式,直到“概率论”的数学方法被发现,古典密码就被破解了。
![](https://files.mdnice.com/user/27110/3d07abfb-4054-45f2-a9d1-8bc7f32fb1bd.png)
英文单词中字母出现的频率是不同的,e以12.702%的百分比占比最高,z 只占到0.074%,感兴趣的可以去百科查字母频率详细统计数据。如果密文数量足够大,仅仅采用频度分析法就可以破解单表的替换法或移位法。
![](https://files.mdnice.com/user/27110/76b66b09-71de-4d58-83cf-ac92bfba6ca6.png)
多表的替换法或移位法虽然难度高一些,但如果数据量足够大的话,也是可以破解的。以维尼吉亚密码算法为例,破解方法就是先找出密文中完全相同的字母串,猜测密钥长度,得到密钥长度后再把同组的密文放在一起,使用频率分析法破解。
2、近代密码学
古典密码的安全性受到了威胁,外加使用便利性较低,到了工业化时代,近现代密码被广泛应用。
恩尼格玛机
恩尼格玛机是二战时期纳粹德国使用的加密机器,后被英国破译,参与破译的人员有被称为计算机科学之父、人工智能之父的图灵。
![](https://files.mdnice.com/user/27110/91a610b3-9230-4e04-a2dd-f629398332aa.png)
恩尼格玛机
恩尼格玛机使用的加密方式本质上还是移位和替代,只不过因为密码表种类极多,破解难度高,同时加密解密机器化,使用便捷,因而在二战时期得以使用。
3、现代密码学
【1】散列函数
散列函数,也见杂凑函数、摘要函数或哈希函数,可将任意长度的消息经过运算,变成固定长度数值,常见的有MD5、SHA-1、SHA256,多应用在文件校验,数字签名中。
MD5 可以将任意长度的原文生成一个128位(16字节)的哈希值
SHA-1可以将任意长度的原文生成一个160位(20字节)的哈希值
【2】对称密码
对称密码应用了相同的加密密钥和解密密钥。对称密码分为:序列密码(流密码),分组密码(块密码)两种。流密码是对信息流中的每一个元素(一个字母或一个比特)作为基本的处理单元进行加密,块密码是先对信息流分块,再对每一块分别加密。
例如原文为1234567890,流加密即先对1进行加密,再对2进行加密,再对3进行加密……最后拼接成密文;块加密先分成不同的块,如1234成块,5678成块,90XX(XX为补位数字)成块,再分别对不同块进行加密,最后拼接成密文。前文提到的古典密码学加密方法,都属于流加密。
【3】非对称密码
对称密码的密钥安全极其重要,加密者和解密者需要提前协商密钥,并各自确保密钥的安全性,一但密钥泄露,即使算法是安全的也无法保障原文信息的私密性。
在实际的使用中,远程的提前协商密钥不容易实现,即使协商好,在远程传输过程中也容易被他人获取,因此非对称密钥此时就凸显出了优势。
非对称密码有两支密钥,公钥(publickey)和私钥(privatekey),加密和解密运算使用的密钥不同。用公钥对原文进行加密后,需要由私钥进行解密;用私钥对原文进行加密后(此时一般称为签名),需要由公钥进行解密(此时一般称为验签)。公钥可以公开的,大家使用公钥对信息进行加密,再发送给私钥的持有者,私钥持有者使用私钥对信息进行解密,获得信息原文。因为私钥只有单一人持有,因此不用担心被他人解密获取信息原文。
三、如何设置密码才安全
- 密码不要太常见,不要使用类似于123456式的常用密码。
- 各应用软件密码建议不同,避免出现一个应用数据库被脱库,全部应用密码崩塌,
- 可在设置密码时增加注册时间、注册地点、应用特性等方法。例如tianjin123456,表示在天津注册的该应用;zfb123456(支付宝) 使用前缀等方式
四、ASCII编码
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统,并等同于国际标准ISO/IEC 646。
![](https://files.mdnice.com/user/27110/e149a67e-1fa9-4902-b876-d4d1af92fb8e.png)
示例代码:
创建maven项目 encrypt-decrypt
(1)添加pom文件
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
(2)创建类 com.njf.ascii.AsciiDemo,字符转换成ascii码
public class AsciiDemo {
public static void main(String[] args) {
char a = 'A';
int b = a;
// 打印ascii码
System.out.println(b);
}
}
运行程序
![](https://files.mdnice.com/user/27110/26c9eee9-74ec-4aa1-89be-b5a480ef1ad2.png)
(3)字符串转换成ascii码
public class AsciiDemo {
public static void main(String[] args) {
char a = 'A';
int b = a;
// 打印ascii码
System.out.println(b);
System.out.println("---打印字符串的Ascii码---");
String str = "AaZ";
// 获取ascii码,需要把字符串转成字符
char[] chars = str.toCharArray();
for (char c : chars) {
int asciiCode = c;
System.out.println(asciiCode);
}
}
}
打印字符串的ascii码值:
五、恺撒加密
1、中国古代加密
看一个小故事 , 看看古人如何加密和解密:
公元683年,唐中宗即位。随后,武则天废唐中宗,立第四子李旦为皇帝,但朝政大事均由她自己专断。
裴炎、徐敬业和骆宾王等人对此非常不满。徐敬业聚兵十万,在江苏扬州起兵。裴炎做内应,欲以拆字手段为其传递秘密信息。后因有人告密,裴炎被捕,未发出的密信落到武则天手中。这封密信上只有“青鹅”二字,群臣对此大惑不解。
武则天破解了“青鹅”的秘密:“青”字拆开来就是“十二月”,而“鹅”字拆开来就是“我自与”。密信的意思是让徐敬业、骆宾王等率兵于十二月进发,裴炎在内部接应。“青鹅”破译后,裴炎被杀。接着,武则天派兵击败了徐敬业和骆宾王。
![](https://files.mdnice.com/user/27110/520e2429-f306-40e6-93bc-cbd1399a8284.png)
2、外国加密
在密码学中,恺撒密码是一种最简单且最广为人知的加密技术。
凯撒密码最早由古罗马军事统帅盖乌斯·尤利乌斯·凯撒在军队中用来传递加密信息,故称凯撒密码。这是一种位移加密方式,只对26个字母进行位移替换加密,规则简单,容易破解。下面是位移1次的对比:
![](https://files.mdnice.com/user/27110/e1bb8737-aeae-4b16-8f45-129176be9fa5.png)
将明文字母表向后移动1位,A变成了B,B变成了C……,Z变成了A。同理,若将明文字母表向后移动3位:
![](https://files.mdnice.com/user/27110/0b269d44-fe35-4dae-9302-b67780124893.png)
则A变成了D,B变成了E……,Z变成了C。
字母表最多可以移动25位。凯撒密码的明文字母表向后或向前移动都是可以的,通常表述为向后移动,如果要向前移动1位,则等同于向后移动25位,位移选择为25即可。
它是一种替换加密的技术,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。
例如,当偏移量是3的时候,所有的字母A将被替换成D,B变成E,以此类推。
这个加密方法是以恺撒的名字命名的,当年恺撒曾用此方法与其将军们进行联系。
恺撒密码通常被作为其他更复杂的加密方法中的一个步骤。
![](https://files.mdnice.com/user/27110/aca24afc-f46e-4c9a-add0-35c2f569a4e5.png)
简单来说就是当秘钥为n,其中一个待加密字符ch,加密之后的字符为ch+n,当ch+n超过’z’时,回到’a’计数。
3、凯撒位移加密
创建类 KaiserDemo,把 hello world 往右边移动3位
public class KaiserDemo {
public static void main(String[] args) {
String input = "Hello world";
// 往右边移动3位(密钥)
int key = 3;
// 用来拼接
StringBuilder sb = new StringBuilder();
// 字符串转换成字节数组
char[] chars = input.toCharArray();
for (char c : chars) {
int asciiCode = c;
// 使用密钥计算加密后的结果
asciiCode += key;
char newChar = (char) asciiCode;
sb.append(newChar);
}
System.out.println(sb.toString());
}
}
运行结果:
![](https://files.mdnice.com/user/27110/e99c714a-af47-4b97-8c42-8845f39bfc43.png)
4、凯撒加密和解密
public class KaiserDemo {
public static void main(String[] args) {
String input = "Hello world";
// 往右边移动3位(密钥)
int key = 3;
String encryptStr = encryptKaiser(input, key);
System.out.println("密文为:encryptStr = " + encryptStr);
String decryptStr = decryptKaiser(encryptStr, key);
System.out.println("解密后:decryptStr = " + decryptStr);
}
/**
* 用凯撒加密方式解密数据
* @param encryptData 密文
* @param key 密钥
* @return 解密后的数据
*/
public static String decryptKaiser(String encryptData, int key) {
// 用来拼接
StringBuilder sb = new StringBuilder();
//转换为字节数组
char[] chars = encryptData.toCharArray();
for (char c : chars) {
//使用密钥进行解密
int asciiCode = c;
// 偏移数据
asciiCode -= key;
// 将偏移后的数据转为字符
char newChar = (char) asciiCode;
// 拼接数据
sb.append(newChar);
}
return sb.toString();
}
/**
* 用凯撒加密方式加密数据
* @param origin 原文
* @param key 密钥
* @return 加密后的数据
*/
public static String encryptKaiser(String origin, int key) {
// 用来拼接
StringBuilder sb = new StringBuilder();
//转换为字节数组
char[] chars = origin.toCharArray();
//对字节中的每个字符进行加密操作
for (char c : chars) {
int asciiCode = c;
// 使用密钥计算加密后的结果
asciiCode += key;
char newChar = (char) asciiCode;
sb.append(newChar);
}
return sb.toString();
}
}
六、频度分析法破解恺撒加密
1、密码棒
公元前5世纪的时候,斯巴达人利用一根木棒,缠绕上皮革或者羊皮纸,在上面横向写下信息,解下这条皮带。展开来看,这长串字母没有任何意义。
比如这样:
信差可以将这条皮带当成腰带,系在腰上。
比如这样:
![](https://files.mdnice.com/user/27110/6203b80f-c8da-4cbd-952b-20e2e0911e0e.png)
然后收件人将这条皮带缠绕在相同的木棒上,就能恢复信息了。
前404年,一位遍体鳞伤的信差来到斯巴达将领利桑德面前,这趟波斯之旅只有他和四位同伴幸存,利桑德接下腰带,缠绕到他的密码棒上,得知波斯的发那巴祖斯准备侵袭他,多亏密码棒利桑德才能够预先防范,击退敌军。
2、频率分析解密法
密码棒是不是太简单了些?
加密者选择将组成信息的字母替代成别的字母,比如说将a写成1,这样就不能被解密者直接拿到信息了。
这难不倒解密者,以英文字母为例,为了确定每个英文字母的出现频率,分析一篇或者数篇普通的英文文章,英文字母出现频率最高的是e,接下来是t,然后是a……,然后检查要破解的密文,也将每个字母出现的频率整理出来,假设密文中出现频率最高的字母是j,那么就可能是e的替身,如果密码文中出现频率次高的但是P,那么可能是t的替身,以此类推便就能解开加密信息的内容。这就是频率分析法。
- 将明文字母的出现频率与密文字母的频率相比较的过程
- 通过分析每个符号出现的频率而轻易地破译代换式密码
- 在每种语言中,冗长的文章中的字母表现出一种可对之进行分辨的频率
- e是英语中最常用的字母,其出现频率为八分之一
将 article.txt 拷贝到项目文件夹的根目录,并进行加密和解密的测试。
Util.java
public class Util {
public static void print(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
sb.append(bytes[i]).append(" ");
}
System.out.println(sb);
}
public static String file2String(String path) throws IOException {
FileReader reader = new FileReader(new File(path));
char[] buffer = new char[1024];
int len = -1;
StringBuffer sb = new StringBuffer();
while ((len = reader.read(buffer)) != -1) {
sb.append(buffer, 0, len);
}
return sb.toString();
}
public static void string2File(String data, String path) {
FileWriter writer = null;
try {
writer = new FileWriter(new File(path));
writer.write(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static String inputStream2String(InputStream in) throws IOException {
int len = -1;
byte[] buffer = new byte[1024];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = in.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.close();
return baos.toString("UTF-8");
}
}
FrequencyAnalysis.java
public class FrequencyAnalysis {
//英文里出现次数最多的字符
private static final char MAGIC_CHAR = 'e';
//破解生成的最大文件数
private static final int DE_MAX_FILE = 4;
public static void main(String[] args) throws Exception {
//测试1,统计字符个数
printCharCount("article.txt");
//加密文件
int key = 3;
encryptFile("article.txt", "article_en.txt", key);
//统计加密后的字符个数
printCharCount("article_en.txt");
//读取加密后的文件
String artile = Util.file2String("article_en.txt");
//解密(会生成多个备选文件)
decryptCaesarCode(artile, "article_de.txt");
}
public static void printCharCount(String path) throws IOException {
String data = Util.file2String(path);
List<Entry<Character, Integer>> mapList = getMaxCountChar(data);
for (Entry<Character, Integer> entry : mapList) {
//输出前几位的统计信息
System.out.println("字符'" + entry.getKey() + "'出现" + entry.getValue() + "次");
}
}
public static void encryptFile(String srcFile, String destFile, int key) throws IOException {
String artile = Util.file2String(srcFile);
//加密文件
String encryptData = KaiserDemo.encryptKaiser(artile, key);
//保存加密后的文件
Util.string2File(encryptData, destFile);
}
/**
* 破解凯撒密码
*
* @param input 数据源
* @return 返回解密后的数据
*/
public static void decryptCaesarCode(String input, String destPath) {
int deCount = 0;//当前解密生成的备选文件数
//获取出现频率最高的字符信息(出现次数越多越靠前)
List<Entry<Character, Integer>> mapList = getMaxCountChar(input);
for (Entry<Character, Integer> entry : mapList) {
//限制解密文件备选数
if (deCount >= DE_MAX_FILE) {
break;
}
//输出前几位的统计信息
System.out.println("字符'" + entry.getKey() + "'出现" + entry.getValue() + "次");
++deCount;
//出现次数最高的字符跟MAGIC_CHAR的偏移量即为秘钥
int key = entry.getKey() - MAGIC_CHAR;
System.out.println("猜测key = " + key + ", 解密生成第" + deCount + "个备选文件" + "\n");
String decrypt = KaiserDemo.decryptKaiser(input, key);
String fileName = "de_" + deCount + destPath;
Util.string2File(decrypt, fileName);
}
}
//统计String里出现最多的字符
public static List<Entry<Character, Integer>> getMaxCountChar(String data) {
Map<Character, Integer> map = new HashMap<Character, Integer>();
char[] array = data.toCharArray();
for (char c : array) {
if (!map.containsKey(c)) {
map.put(c, 1);
} else {
Integer count = map.get(c);
map.put(c, count + 1);
}
}
//输出统计信息
/*for (Entry<Character, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + "出现" + entry.getValue() + "次");
}*/
//获取获取最大值
int maxCount = 0;
for (Entry<Character, Integer> entry : map.entrySet()) {
//不统计空格
if (/*entry.getKey() != ' ' && */entry.getValue() > maxCount) {
maxCount = entry.getValue();
}
}
//map转换成list便于排序
List<Entry<Character, Integer>> mapList = new ArrayList<Entry<Character, Integer>>(map.entrySet());
//根据字符出现次数排序
Collections.sort(mapList, new Comparator<Entry<Character, Integer>>() {
@Override
public int compare(Entry<Character, Integer> o1,
Entry<Character, Integer> o2) {
return o2.getValue().compareTo(o1.getValue());
}
});
return mapList;
}
}
运行 FrequencyAnalysis.java 用来统计每个字符出现的次数
运行 FrequencyAnalysis.java 里面 main 函数里面的 encryptFile 方法 对程序进行加密
在根目录会生成一个 article_en.txt 文件,然后我们统计这个文件当中每个字符出现的次数
![](https://files.mdnice.com/user/27110/d329cbef-f435-495a-92ec-d848697276ec.png)
我们来看看频度分析法如何工作的
运行程序
![](https://files.mdnice.com/user/27110/a478567c-133b-47ac-99a5-d6cd72d0d780.png)
运行结果 # 出现次数最多, 我们知道在英文当中 e 出现的频率是最高的,我们假设现在 # 号,就是 e ,变形而来的 ,我们可以对照 ascii 编码表 ,我们的凯撒加密当中位移是加了一个 key ,所以我们 猜测 两个值直接相差 -66 ,我们现在就以 -66 进行解密 生成一个文件,我们查看第一个文件发现,根本读不懂,所以解密失败,我们在猜测 h 是 e ,h 和 e 之间相差3 ,所以我们在去看第二个解密文件,发现我们可以读懂,解密成功
![](https://files.mdnice.com/user/27110/75c6782f-bacb-4c64-a52d-0dfc84a7f34e.png)
七、Byte和bit
Byte : 字节. 数据存储的基本单位,比如移动硬盘1T,单位是byte
bit : 比特, 又叫位. 一个位要么是0要么是1. 数据传输的单位 , 比如家里的宽带100MB,下载速度并没有达到100MB,一般都是12-13MB,那么是因为需要使用 100 / 8
关系: 1Byte = 8bit
1、获取字符串byte
public class ByteBit {
public static void main(String[] args) {
String a = "a";
byte[] bytes = a.getBytes();
for (byte b : bytes) {
System.out.println("b = " + b);
int c = b;
// 打印发现byte实际上就是ascii码
System.out.println(c);
}
}
}
运行结果:
![](https://files.mdnice.com/user/27110/07f89213-c399-465b-b599-6b4320d10636.png)
2、byte对应bit
public class ByteBit {
public static void main(String[] args) {
String a = "a";
byte[] bytes = a.getBytes();
for (byte b : bytes) {
System.out.println("b = " + b);
int c = b;
// 打印发现byte实际上就是ascii码
System.out.println(c);
// 我们在来看看每个byte对应的bit,byte获取对应的bit
String bitStr = Integer.toBinaryString(c);
System.out.println(bitStr);
}
}
}
运行程序
打印出来应该是8个bit,但前面是0,没有打印 ,从打印结果可以看出来,一个英文字符 ,占一个字节
其实就是ASCII码对应的二进制数据。
3、中文对应的字节
// 中文在GBK编码下, 占据2个字节
// 中文在UTF-8编码下, 占据3个字节
public class ByteBitDemo {
public static void main(String[] args) throws Exception{
String a = "尚";
byte[] bytes = a.getBytes();
for (byte b : bytes) {
System.out.print(b + " ");
//获取对应的 bit
String s = Integer.toBinaryString(b);
System.out.println(s);
}
}
}
运行程序:我们发现一个中文是有 3 个字节组成
![](https://files.mdnice.com/user/27110/c70bfc53-a6dc-47cb-9d6f-e35b64994d60.png)
我们修改编码格式, 编码格式改成GBK,我们在运行发现变成了 2 个字节
// 中文在GBK编码下, 占据2个字节
// 中文在UTF-8编码下, 占据3个字节
public class ByteBitDemo {
public static void main(String[] args) throws Exception{
String a = "尚";
// 在中文情况下,不同的编码格式,对应不同的字节
//GBK :编码格式占2个字节
// UTF-8:编码格式占3个字节
byte[] bytes = a.getBytes("GBK");
// byte[] bytes = a.getBytes("UTF-8");
//byte[] bytes = a.getBytes(); 默认使用的是 UTF-8
for (byte b : bytes) {
System.out.print(b + " ");
//获取对应的 bit
String s = Integer.toBinaryString(b);
System.out.println(s);
}
}
}
运行程序
![](https://files.mdnice.com/user/27110/fa0dc53a-dfce-4f9c-8f90-2cac8bc5993d.png)
4、英文对应的字节
我们在看看英文,在不同的编码格式占用多少字节
public static void main(String[] args) throws Exception {
String a = "A";
byte[] bytes = a.getBytes();
// 在中文情况下,不同的编码格式,对应不同的字节
//byte[] bytes = a.getBytes("GBK");
for (byte b : bytes) {
System.out.print(b + " ");
String s = Integer.toBinaryString(b);
System.out.println(s);
}
}
运行程序:
![](https://files.mdnice.com/user/27110/1e2c69d4-d926-4be3-9249-5727f90e02f6.png)
可以发现在英文情况下,不同的编码格式都只占用了一个字节。