encrypt-decrypt
1 密码学基本概念
1.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位
(abcde - cdefg)
,这样bee
单词就变换成了dgg
。同理替换法,移位法也可以采用多表移位的方式,典型的多表案例是“维尼吉亚密码”(又译维热纳尔密码),属于多表密码的一种形式。
古典密码破解方式(频率分析法):
古典密码虽然很简单,但是在密码史上是使用的最久的加密方式,直到“概率论”的数学方法被发现,古典密码就被破解了。
将明文字母的出现频率与密文字母的频率相比较的过程。通过分析每个符号出现的频率而轻易地破译代换式密码。在每种语言中,冗长的文章中的字母表现出一种可对之进行分辨的频率。
分析方法:
英文单词中字母出现的频率是不同的,e以12.702%的百分比占比最高,z 只占到0.074%,感兴趣的可以去百科查字母频率详细统计数据。如果密文数量足够大,仅仅采用频度分析法就可以破解单表的替换法或移位法。
多表的替换法或移位法虽然难度高一些,但如果数据量足够大的话,也是可以破解的。以维尼吉亚密码算法为例,破解方法就是先找出密文中完全相同的字母串,猜测密钥长度,得到密钥长度后再把同组的密文放在一起,使用频率分析法破解。
1.2 近代密码学
古典密码的安全性受到了威胁,外加使用便利性较低,到了工业化时代,近现代密码被广泛应用。
恩尼格玛机是二战时期纳粹德国使用的加密机器,后被英国破译,参与破译的人员有被称为计算机科学之父、人工智能之父的图灵。
恩尼格玛机使用的加密方式本质上还是移位和替代,只不过因为密码表种类极多,破解难度高,同时加密解密机器化,使用便捷,因而在二战时期得以使用。
1.3 现代密码学
-
散列函数
散列函数,也见杂凑函数、摘要函数或哈希函数,可将任意长度的消息经过运算,变成固定长度数值,常见的有
MD5
、SHA-1
、SHA256
,多应用在文件校验,数字签名中。MD5 可以将任意长度的原文生成一个128位(16字节)的哈希值
SHA-1可以将任意长度的原文生成一个160位(20字节)的哈希值
-
对称密码
对称密码应用了相同的加密密钥和解密密钥。
对称密码分为:序列密码(流密码),分组密码(块密码)两种。
流密码是对信息流中的每一个元素(一个字母或一个比特)作为基本的处理单元进行加密,块密码是先对信息流分块,再对每一块分别加密。
例如原文为1234567890,流加密即先对1进行加密,再对2进行加密,再对3进行加密……最后拼接成密文;
块加密先分成不同的块,如1234成块,5678成块,90XX(XX为补位数字)成块,再分别对不同块进行加密,最后拼接成密文。前文提到的古典密码学加密方法,都属于流加密。
-
非对称密码
对称密码的密钥安全极其重要,加密者和解密者需要提前协商密钥,并各自确保密钥的安全性,一但密钥泄露,即使算法是安全的也无法保障原文信息的私密性。
在实际的使用中,远程的提前协商密钥不容易实现,即使协商好,在远程传输过程中也容易被他人获取,因此非对称密钥此时就凸显出了优势。
非对称密码有两支密钥,公钥(publickey)和私钥(privatekey),加密和解密运算使用的密钥不同。
用公钥对原文进行加密后,需要由私钥进行解密;用私钥对原文进行加密后(此时一般称为签名),需要由公钥进行解密(此时一般称为验签)。
公钥可以公开的,大家使用公钥对信息进行加密,再发送给私钥的持有者,私钥持有者使用私钥对信息进行解密,获得信息原文。因为私钥只有单一人持有,因此不用担心被他人解密获取信息原文。
2 凯撒加密
2.1 凯撒加密解密的实现
凯撒密码最早由古罗马军事统帅盖乌斯·尤利乌斯·凯撒在军队中用来传递加密信息,故称凯撒密码。这是一种位移加密方式,只对26个字母进行位移替换加密,规则简单,容易破解。下面是位移1次的对比:
将明文字母表向后移动1位,A变成了B,B变成了C……,Z变成了A。同理,若将明文字母表向后移动3位:
字母表最多可以移动25位。凯撒密码的明文字母表向后或向前移动都是可以的,通常表述为向后移动,如果要向前移动1位,则等同于向后移动25位,位移选择为25即可。
它是一种替换加密的技术,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。
简单来说就是当秘钥为n,其中一个待加密字符ch,加密之后的字符为ch+n,当ch+n超过’z’时,回到’a’计数。
凯撒加密解密工具类:
public class KaiSaUtil {
// 加密
public static String encryptKaiser(String original, Integer key) {
// 1、将输入的字符串转换成字符数组
char[] chars = original.toCharArray();
StringBuilder sb = new StringBuilder();
for (char aChar : chars) {
// 2、获取字符的ascii编码
int asciiCode = aChar;
// 3、偏移数据
asciiCode += key;
// 4、将偏移后的数据转为字符
char result = (char) asciiCode;
// 5、拼接数据
sb.append(result);
}
return sb.toString();
}
// 解密
public static String decryptKaiser(String encryptedData, int key) {
// 1、将密文转换成字符数组
char[] chars = encryptedData.toCharArray();
StringBuilder sb = new StringBuilder();
for (char aChar : chars) {
// 2、获取字符的ascii编码
int asciiCode = aChar;
// 3、偏移数据
asciiCode -= key;
// 4、将偏移后的数据转为字符
char result = (char) asciiCode;
// 5、拼接数据
sb.append(result);
}
return sb.toString();
}
}
凯撒加密代码测试:
public class KaiSaDemo {
public static void main(String[] args) {
String input = "welcome";
Integer key = 3;
System.out.println("加密前:" + input);
String encryptKaiser = KaiSaUtil.encryptKaiser(input, key);
System.out.println("加密后:" + encryptKaiser);
String decryptKaiser = KaiSaUtil.decryptKaiser(encryptKaiser, key);
System.out.println("解密后:" + decryptKaiser);
}
}
测试结果:
加密前:welcome
加密后:zhofrph
解密后:welcome
2.2 使用频度分析法破解凯撒加密
频率分析法:
- 将明文字母的出现频率与密文字母的频率相比较的过程
- 通过分析每个符号出现的频率而轻易地破译代换式密码
- 在每种语言中,冗长的文章中的字母表现出一种可对之进行分辨的频率。
- e是英语中最常用的字母,其出现频率为八分之一
实现步骤:
1、引入FrequencyUtil和FrequencyAnalysis两个类,article.txt
(随便写篇文章) 拷贝到项目文件夹的根目录
2、运行 FrequencyAnalysis.java
用来统计每个字符出现的次数
3、运行 FrequencyAnalysis.java
里面 main 函数里面的 encryptFile
方法 对程序进行加密
public static void main(String[] args) throws Exception {
//测试1,统计字符个数
//printCharCount("article.txt");
//加密文件
int key = 3;
encryptFile("article.txt", "article_en.txt", key);
//读取加密后的文件
// String artile = Util.file2String("article_en.txt");
//解密(会生成多个备选文件)
// decryptCaesarCode(artile, "article_de.txt");
}
4、在根目录会生成一个 article_en.txt
文件,然后我们统计这个文件当中每个字符出现的次数
public static void main(String[] args) throws Exception {
//测试1,统计字符个数
printCharCount("article_en.txt");
//加密文件
int key = 3;
//encryptFile("article.txt", "article_en.txt", key);
//读取加密后的文件
// String artile = Util.file2String("article_en.txt");
//解密(会生成多个备选文件)
// decryptCaesarCode(artile, "article_de.txt");
}
频率分析工具类:
public class FrequencyUtil {
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");
}
}
破解测试类:
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("D:\\IDEA\\encrypt-decrypt\\src\\article.txt");
//加密文件
int key = 3;
// encryptFile("D:\\\\IDEA\\\\encrypt-decrypt\\\\src\\\\article.txt", "D:\\IDEA\\encrypt-decrypt\\src\\article_en.txt", key);
//读取加密后的文件
String artile = FrequencyUtil.file2String("D:\\IDEA\\encrypt-decrypt\\src\\article_en.txt");
//解密(会生成多个备选文件)
decryptCaesarCode(artile, "article_de.txt");
}
public static void printCharCount(String path) throws IOException {
String data = FrequencyUtil.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 = FrequencyUtil.file2String(srcFile);
//加密文件
String encryptData = KaiSaUtil.encryptKaiser(artile, key);
//保存加密后的文件
FrequencyUtil.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 = KaiSaUtil.decryptKaiser(input, key);
String fileName = "de_" + deCount + destPath;
FrequencyUtil.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<Map.Entry<Character, Integer>>(map.entrySet());
//根据字符出现次数排序
Collections.sort(mapList, new Comparator<Entry<Character, Integer>>() {
public int compare(Entry<Character, Integer> o1,
Entry<Character, Integer> o2) {
return o2.getValue().compareTo(o1.getValue());
}
});
return mapList;
}
}
3 常见加密方式
3.1 对称加密
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。
示例 :
我们现在有一个原文3要发送给B
设置密钥为108, 3 * 108 = 324, 将324作为密文发送给B
B拿到密文324后, 使用324/108 = 3 得到原文
常见加密算法:
- DES : Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法,1977年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级政府通信中使用,随后该算法在国际上广泛流传开来。
- AES : Advanced Encryption Standard, 高级加密标准 .在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。
特点:
- 加密速度快, 可以加密大文件
- 密文可逆, 一旦密钥文件泄漏, 就会导致数据暴露
- 加密后编码表找不到对应字符, 出现乱码
- 一般结合Base64使用
3.2 DES加密解密
加密示例代码:
public class DesAesDemo {
public static void main(String[] args) throws Exception {
// 原文
String input = "hello";
// des加密必须是8位
String key = "12345678";
// 算法
String algorithm = "DES";
// 加密类型
String transformation = "DES";
// Cipher:密码,获取加密对象
// transformation:参数表示使用什么类型加密
Cipher cipher = Cipher.getInstance(transformation);
// 指定秘钥规则
// 第一个参数表示:密钥,key的字节数组
// 第二个参数表示:算法
SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
// 对加密进行初始化
// 第一个参数:表示模式,有加密模式和解密模式
// 第二个参数:表示秘钥规则
cipher.init(Cipher.ENCRYPT_MODE, sks);
// 进行加密
byte[] bytes = cipher.doFinal(input.getBytes());
// 打印字节,因为ascii码有负数,解析不出来,所以乱码
for (byte b : bytes) {
System.out.println(b);
}
// 打印密文
System.out.println(new String(bytes));
}
}
测试结果:
-70
22
-58
-96
37
113
37
-81
�Ơ%q%�
注意:
1、DES加密算法规定,密钥key必须是8个字节
2、出现乱码是因为对应的字节出现负数,但负数,没有出现在 ascii 码表里面,所以出现乱码,需要配合base64进行转码
3、base64
导包的时候,需要注意 ,别导错了,需要导入 apache
包
转码代码:
String encode = Base64.encode(bytes);
测试结果:
base64转码前:�i,6�M
base64转码后:2GksNrsVEk0=
DES解密示例代码:
// 解密方法:
public static String decryptDES(String input, String key, String transformation, String algorithm) throws Exception {
// 1,获取Cipher对象
Cipher cipher = Cipher.getInstance(transformation);
// 指定密钥规则
SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
cipher.init(Cipher.DECRYPT_MODE, sks);
// 3. 解密,上面使用的base64编码,下面直接用密文
byte[] bytes = cipher.doFinal(Base64.decode(input));
// 因为是明文,所以直接返回
return new String(bytes);
}
3.3 base64
Base64 算法简介:
Base64是网络上最常见的用于传输8Bit字节码的可读性编码算法之一
可读性编码算法不是为了保护数据的安全性,而是为了可读性
可读性编码不改变信息内容,只改变信息内容的表现形式
所谓Base64,即是说在编码过程中使用了64种字符:大写A到Z、小写a到z、数字0到9、“+”和“/”
Base58是Bitcoin(比特币)中使用的一种编码方式,主要用于产生Bitcoin的钱包地址
相比Base64,Base58不使用数字"0",字母大写"O",字母大写"I",和字母小写"i",以及"+"和"/"符号
Base64 算法原理:
base64 是 3个字节为一组,一个字节 8位,一共 就是24位 ,然后,把3个字节转成4组,每组6位
3 * 8 = 4 * 6 = 24 ,每组6位,缺少的2位,会在高位进行补0 ,这样做的好处在于 ,base取的是后面6位,去掉高2位 ,那么base64的取值就可以控制在0-63位了,所以就叫base64
base64 构成原则:
- 小写 a - z = 26个字母
- 大写 A - Z = 26个字母
- 数字 0 - 9 = 10 个数字
- + / = 2个符号
- 大家可能发现一个问题,咱们的base64有个 = 号,但是在映射表里面没有发现 = 号 , 这个地方需要注意,等号非常特殊,因为base64是三个字节一组 ,如果当我们的位数不够的时候,会使用等号来补齐
base64补等号测试:
public class TestBase64 {
public static void main(String[] args) {
// 1:MQ== 表示一个字节,不够三个字节,所以需要后面通过 == 号补齐
System.out.println(Base64.encode("1".getBytes()));
// System.out.println(Base64.encode("12".getBytes()));
// System.out.println(Base64.encode("123".getBytes()));
// // 你好:中文占6个字节,6 * 8 = 48 ,刚刚好被整除,所以没有等号
// System.out.println(Base64.encode("你好".getBytes()));
}
}
测试结果:
MQ==
MTI=
MTIZ
3.4 AES加密解密
AES
加密解密和 DES
加密解密代码一样,只需要修改加密算法就行,拷贝 ESC
代码
AES加密解密工具类:
public class AESUtil {
// 加密方法
public static String encryptDES(String input, String key, String transformation, String algorithm) throws Exception {
// 获取加密对象
Cipher cipher = Cipher.getInstance(transformation);
// 创建加密规则
// 第一个参数key的字节
// 第二个参数表示加密算法
SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
// ENCRYPT_MODE:加密模式
// DECRYPT_MODE: 解密模式
// 初始化加密模式和算法
cipher.init(Cipher.ENCRYPT_MODE,sks);
// 加密
byte[] bytes = cipher.doFinal(input.getBytes());
// 输出加密后的数据
String encode = Base64.encode(bytes);
return encode;
}
// 解密方法:
public static String decryptDES(String input, String key, String transformation, String algorithm) throws Exception {
// 1,获取Cipher对象
Cipher cipher = Cipher.getInstance(transformation);
// 指定密钥规则
SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
cipher.init(Cipher.DECRYPT_MODE, sks);
// 3. 解密,上面使用的base64编码,下面直接用密文
byte[] bytes = cipher.doFinal(Base64.decode(input));
// 因为是明文,所以直接返回
return new String(bytes);
}
}
测试类:
public class AESDemo {
public static void main(String[] args) throws Exception {
String input = "原文";
// DES加密算法,key的大小必须是8个字节
String key = "1234567812345678";
String transformation = "AES";
// 指定获取密钥的算法
String algorithm = "AES";
String encryptDES = AESUtil.encryptDES(input, key, transformation, algorithm);
System.out.println("加密:" + encryptDES);
String s = AESUtil.decryptDES(encryptDES, key, transformation, algorithm);
System.out.println("解密:" + s);
}
}
测试结果:
加密:TUuxDy0h/Rx1KwhMFXEnog==
解密:原文
注意:
1、AES加密key必须是16个字节
3.5 toString()与new String ()用法区别
str.toString是调用了这个object对象的类的toString方法。一般是返回这么一个String:[class name]@[hashCode]
new String(str)是根据parameter是一个字节数组,使用java虚拟机默认的编码格式,将这个字节数组decode为对应的字符。若虚拟机默认的编码格式是ISO-8859-1,按照ascii编码表即可得到字节对应的字符。
new String()一般使用字符转码的时候,byte[]数组的时候
toString()对象打印的时候使用
4 加密模式
加密模式:https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html
ECB: Electronic codebook, 电子密码本. 需要加密的消息按照块密码的块大小被分为数个块,并对每个块进行独立加密
- 优点 : 可以并行处理数据
- 缺点 : 同样的原文生成同样的密文, 不能很好的保护数据。
- 同时加密,原文是一样的,加密出来的密文也是一样的。
CBC : Cipher-block chaining, 密码块链接. 每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块
- 优点 : 同样的原文生成的密文不一样。
- 缺点 : 串行处理数据。加密比较慢
5 填充模式
当需要按块处理的数据, 数据长度不符合块处理需求时, 按照一定的方法填充满块长的规则
NoPadding:
- 不填充
- 在DES加密算法下, 要求原文长度必须是8byte的整数倍
- 在AES加密算法下, 要求原文长度必须是16byte的整数倍
PKCS5Pading:
数据块的大小为8位, 不够就补足
Tips:
默认情况下, 加密模式和填充模式为 : ECB/PKCS5Padding
如果使用CBC模式, 在初始化Cipher对象时, 需要增加参数, 初始化向量IV : IvParameterSpec iv = new IvParameterSpec(key.getBytes());
加密模式和填充模式:
AES/CBC/NoPadding (128)
AES/CBC/PKCS5Padding (128)
AES/ECB/NoPadding (128)
AES/ECB/PKCS5Padding (128)
DES/CBC/NoPadding (56)
DES/CBC/PKCS5Padding (56)
DES/ECB/NoPadding (56)
DES/ECB/PKCS5Padding (56)
DESede/CBC/NoPadding (168)
DESede/CBC/PKCS5Padding (168)
DESede/ECB/NoPadding (168)
DESede/ECB/PKCS5Padding (168)
RSA/ECB/PKCS1Padding (1024, 2048)
RSA/ECB/OAEPWithSHA-1AndMGF1Padding (1024, 2048)
RSA/ECB/OAEPWithSHA-256AndMGF1Padding (1024, 2048)
6 消息摘要
什么是信息摘要:
- 消息摘要(Message Digest)又称为数字摘要(Digital Digest)
- 是一个唯一对应一个消息或文本的固定长度的值,它由一个单向Hash加密函数对消息进行作用而产生
- 使用数字摘要生成的值是不可以篡改的,为了保证文件或者值的安全
特点:
无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,用SHA-1算法摘要的消息最终有160比特位的输出
只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出
消息摘要是单向、不可逆的
常见算法 :
- MD5
- SHA1
- SHA256
- SHA512
百度搜索 tomcat
,进入官网下载 ,会经常发现有 sha1
,sha512
, 这些都是数字摘要
数字摘要:
bd465ea30ee7e0a66ed67e86d45a53aa5aba0c8d190934e7dfa58294a21ada7b967877d848e1836a19bf01437cab64f275ac827d81b3f3253eb961b60361a045 *apache-tomcat-10.0.2.exe
获取字符串信息摘要:
// 消息摘要算法,为了防止篡改
public class DigestDemo1 {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 原文
String input = "aa";
// 算法
String algorithm = "MD5";
// 获取数字摘要对象
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 获取消息数字摘要的字节数组
byte[] digest = messageDigest.digest(input.getBytes());
System.out.println(new String(digest));
// 结合base64解决转码
System.out.println(Base64.encode(digest));
}
}
测试结果:
A$�
�5�o$� zI
转码后:QSS8CpM1wn8IbyS6IHpJEg==
数字摘要转换成 16 进制
// 4124bc0a9335c27f086f24ba207a4912 md5 在线校验
// QSS8CpM1wn8IbyS6IHpJEg== 消息摘要使用的是16进制
代码转成16进制
// 消息摘要算法,为了防止篡改
public class DigestDemo1 {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 原文
String input = "aa";
// 算法
String algorithm = "MD5";
// 获取数字摘要对象
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 获取消息数字摘要的字节数组
byte[] digest = messageDigest.digest(input.getBytes());
// 创建对象用来拼接
StringBuilder sb = new StringBuilder();
// 对密文进行迭代
for (byte b : digest) {
// 转成 16进制
String s = Integer.toHexString(b & 0xff);
// System.out.println(s);
if (s.length() == 1){
// 如果生成的字符只有一个,前面补0
s = "0"+s;
}
sb.append(s);
}
System.out.println(sb.toString());
}
}
其他数字摘要算法
public class DigestDemo12 {
public static void main(String[] args) throws Exception {
// 原文
String input = "aa";
// 算法
String algorithm = "MD5";
// 获取数字摘要对象
String md5 = getDigest(input, "MD5");
System.out.println(md5);
String sha1 = getDigest(input, "SHA-1");
System.out.println(sha1);
String sha256 = getDigest(input, "SHA-256");
System.out.println(sha256);
String sha512 = getDigest(input, "SHA-512");
System.out.println(sha512);
}
private static String toHex(byte[] digest) throws Exception {
// System.out.println(new String(digest));
// base64编码
// System.out.println(Base64.encode(digest));
// 创建对象用来拼接
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
// 转成 16进制
String s = Integer.toHexString(b & 0xff);
if (s.length() == 1) {
// 如果生成的字符只有一个,前面补0
s = "0" + s;
}
sb.append(s);
}
System.out.println("16进制数据的长度:" + sb.toString().getBytes().length);
return sb.toString();
}
private static String getDigest(String input, String algorithm) throws Exception {
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 消息数字摘要
byte[] digest = messageDigest.digest(input.getBytes());
System.out.println("密文的字节长度:" + digest.length);
return toHex(digest);
}
}
获取文件信息摘要
public class DigestDemo {
public static void main(String[] args) throws Exception {
String input = "aa";
String algorithm = "MD5";
// sha1 可以实现秒传功能
String sha1 = getDigestFile("D:\\apache-tomcat-9.0.41.zip", "SHA-1");
System.out.println(sha1);
String sha512 = getDigestFile("D:\\apache-tomcat-9.0.41.zip", "SHA-512");
System.out.println(sha512);
String md5 = getDigest("aa", "MD5");
System.out.println(md5);
String md51 = getDigest("aa ", "MD5");
System.out.println(md51);
}
private static String getDigestFile(String filePath, String algorithm) throws Exception {
FileInputStream fis = new FileInputStream(filePath);
int len;
byte[] buffer = new byte[1024];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
// 获取消息摘要对象
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 获取消息摘要
byte[] digest = messageDigest.digest(baos.toByteArray());
System.out.println("密文的字节长度:" + digest.length);
return toHex(digest);
}
private static String getDigest(String input, String algorithm) throws Exception {
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
byte[] digest = messageDigest.digest(input.getBytes());
System.out.println("密文的字节长度:" + digest.length);
return toHex(digest);
}
private static String toHex(byte[] digest) {
// System.out.println(new String(digest));
// 消息摘要进行表示的时候,是用16进制进行表示
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
// 转成16进制
String s = Integer.toHexString(b & 0xff);
// 保持数据的完整性,前面不够的用0补齐
if (s.length() == 1) {
s = "0" + s;
}
sb.append(s);
}
System.out.println("16进制数据的长度:" + sb.toString().getBytes().length);
return sb.toString();
}
}
运行程序 ,获取 sha-1
和 sha-512
的值
总结:
- MD5算法 : 摘要结果16个字节, 转16进制后32个字节
- SHA1算法 : 摘要结果20个字节, 转16进制后40个字节
- SHA256算法 : 摘要结果32个字节, 转16进制后64个字节
- SHA512算法 : 摘要结果64个字节, 转16进制后128个字节
7 非对称加密
简介:
- 非对称加密算法又称
现代加密算法
。 - 非对称加密是计算机通信安全的基石,保证了加密数据
不会被破解
。 - 与对称加密算法不同,非对称加密算法需要两个密钥:
公开密钥(publickey)
和私有密(privatekey)
- 公开密钥和私有密钥是
一对
- 如果用
公开密钥
对数据进行加密
,只有用对应的私有密钥
才能解密
。 - 如果用
私有密钥
对数据进行加密
,只有用对应的公开密钥
才能解密
。 - 因为加密和解密使用的是两个
不同
的密钥,所以这种算法叫作非对称加密算法
。
示例:
- 首先生成密钥对, 公钥为(5,14), 私钥为(11,14)
- 现在A希望将原文2发送给B
- A使用公钥加密数据. 2的5次方mod 14 = 4 , 将密文4发送给B
- B使用私钥解密数据. 4的11次方mod14 = 2, 得到原文2
特点:
- 加密和解密使用不同的密钥
- 如果使用私钥加密, 只能使用公钥解密
- 如果使用公钥加密, 只能使用私钥解密
- 处理数据的速度较慢, 因为安全级别高
常见算法:
- RSA
- ECC
7.1 生成公钥和私钥
public class RSAdemo {
public static void main(String[] args) throws Exception {
// 加密算法
String algorithm = "RSA";
// 创建密钥对生成器对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 生成私钥
PrivateKey privateKey = keyPair.getPrivate();
// 生成公钥
PublicKey publicKey = keyPair.getPublic();
// 获取私钥字节数组
byte[] privateKeyEncoded = privateKey.getEncoded();
// 获取公钥字节数组
byte[] publicKeyEncoded = publicKey.getEncoded();
// 对公私钥进行base64编码
String privateKeyString = Base64.encode(privateKeyEncoded);
String publicKeyString = Base64.encode(publicKeyEncoded);
// 打印私钥
System.out.println(privateKeyString);
// 打印公钥
System.out.println(publicKeyString);
}
}
7.2 私钥加密
// 私钥加密
public class RSAdemo {
public static void main(String[] args) throws Exception {
String input = "hello";
// 加密算法
String algorithm = "RSA";
// 创建密钥对生成器对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 生成私钥
PrivateKey privateKey = keyPair.getPrivate();
// 生成公钥
PublicKey publicKey = keyPair.getPublic();
// 获取私钥字节数组
byte[] privateKeyEncoded = privateKey.getEncoded();
// 获取公钥字节数组
byte[] publicKeyEncoded = publicKey.getEncoded();
// 对公私钥进行base64编码
String privateKeyString = Base64.encode(privateKeyEncoded);
String publicKeyString = Base64.encode(publicKeyEncoded);
// 创建加密对象
// 参数表示加密算法
Cipher cipher = Cipher.getInstance(algorithm);
// 初始化加密
// 第一个参数:加密的模式
// 第二个参数:使用私钥进行加密
cipher.init(Cipher.ENCRYPT_MODE,privateKey);
// 私钥加密
byte[] bytes = cipher.doFinal(input.getBytes());
System.out.println(Base64.encode(bytes));
}
}
7.3 私钥加密私钥解密
public class RSAdemo2 {
public static void main(String[] args) throws Exception {
String input = "你好";
// 加密算法
String algorithm = "RSA";
// 创建密钥对生成器对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 生成私钥
PrivateKey privateKey = keyPair.getPrivate();
// 生成公钥
PublicKey publicKey = keyPair.getPublic();
// 获取私钥字节数组
byte[] privateKeyEncoded = privateKey.getEncoded();
// 获取公钥字节数组
byte[] publicKeyEncoded = publicKey.getEncoded();
// 对公私钥进行base64编码
String privateKeyString = Base64.encode(privateKeyEncoded);
String publicKeyString = Base64.encode(publicKeyEncoded);
// 创建加密对象
// 参数表示加密算法
Cipher cipher = Cipher.getInstance(algorithm);
// 初始化加密
// 第一个参数:加密的模式
// 第二个参数:使用私钥进行加密
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 私钥加密
byte[] bytes = cipher.doFinal(input.getBytes());
System.out.println("加密后:"+Base64.encode(bytes));
// 私钥进行解密
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 对密文进行解密,不需要使用base64,因为原文不会乱码
byte[] bytes1 = cipher.doFinal(bytes);
System.out.println("解密后:"+new String(bytes1));
}
}
运行程序 ,因为私钥加密,只能公钥解密:
7.4 私钥加密公钥解密
public class RSAdemo2 {
public static void main(String[] args) throws Exception {
String input = "你好";
// 加密算法
String algorithm = "RSA";
// 创建密钥对生成器对象
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 生成私钥
PrivateKey privateKey = keyPair.getPrivate();
// 生成公钥
PublicKey publicKey = keyPair.getPublic();
// 获取私钥字节数组
byte[] privateKeyEncoded = privateKey.getEncoded();
// 获取公钥字节数组
byte[] publicKeyEncoded = publicKey.getEncoded();
// 对公私钥进行base64编码
String privateKeyString = Base64.encode(privateKeyEncoded);
String publicKeyString = Base64.encode(publicKeyEncoded);
// 创建加密对象
// 参数表示加密算法
Cipher cipher = Cipher.getInstance(algorithm);
// 初始化加密
// 第一个参数:加密的模式
// 第二个参数:使用私钥进行加密
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 私钥加密
byte[] bytes = cipher.doFinal(input.getBytes());
System.out.println("加密后:"+Base64.encode(bytes));
// 公钥进行解密
cipher.init(Cipher.DECRYPT_MODE, publicKey);
// 对密文进行解密,不需要使用base64,因为原文不会乱码
byte[] bytes1 = cipher.doFinal(bytes);
System.out.println("解密后:"+new String(bytes1));
}
}
7.5 保存公钥和私钥
private static void generateKeyToFile(String algorithm, String pubPath, String priPath) throws Exception {
// 获取密钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 获取密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取公钥
PublicKey publicKey = keyPair.getPublic();
// 获取私钥
PrivateKey privateKey = keyPair.getPrivate();
// 获取byte数组
byte[] publicKeyEncoded = publicKey.getEncoded();
byte[] privateKeyEncoded = privateKey.getEncoded();
// 进行Base64编码
String publicKeyString = Base64.encode(publicKeyEncoded);
String privateKeyString = Base64.encode(privateKeyEncoded);
// 保存文件
FileUtils.writeStringToFile(new File(pubPath), publicKeyString, Charset.forName("UTF-8"));
FileUtils.writeStringToFile(new File(priPath), privateKeyString, Charset.forName("UTF-8"));
}
7.6 读取私钥
public static PrivateKey getPrivateKey(String priPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String privateKeyString = FileUtils.readFileToString(new File(priPath), Charset.defaultCharset());
// 输出私钥
System.out.println(privateKeyString);
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
// 生成私钥
return keyFactory.generatePrivate(spec);
}
7.7 读取公钥
public static PublicKey getPublicKey(String pulickPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String publicKeyString = FileUtils.readFileToString(new File(pulickPath), Charset.defaultCharset());
System.out.println(publicKeyString);
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
// 生成公钥
return keyFactory.generatePublic(spec);
}
7.8 RSA工具类
- 导入依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
- 工具类
public class RSAUtil {
private static final String KEY_ALGORITHM = "RSA";
private static KeyPairGenerator keyPairGenerator;
// 为每一个字段生成公私密钥
public static void makeRsaKeys(String pubPath, String priPath, String user) throws Exception {
try {
keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
//随机数生成器
SecureRandom random = new SecureRandom();
//设置KEY_SIZE位长的秘钥
int KEY_SIZE = 1024;
keyPairGenerator.initialize(KEY_SIZE, random);
//开始创建
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();//公钥
PrivateKey privateKey = keyPair.getPrivate();//私钥
//使用Base64进行转码
String publicKeyStr = Base64.encode(publicKey.getEncoded());
String privateKeyStr = Base64.encode(privateKey.getEncoded());
// 保存文件
FileUtils.writeStringToFile(new File(pubPath), publicKeyStr, Charset.forName("UTF-8"));
FileUtils.writeStringToFile(new File(priPath), privateKeyStr, Charset.forName("UTF-8"));
}
// 私钥加密
public static String privateKeyDecode(String data, int mode, String priPath) throws Exception {
// 将文件内容转为字符串
String privateKeyString = FileUtils.readFileToString(new File(priPath), Charset.defaultCharset());
// 输出私钥
// System.out.println(privateKeyString);
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
// 构建密钥规范 进行Base64解码
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
// 生成私钥
PrivateKey privateKey = keyFactory.generatePrivate(spec);
try {
Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
if (mode == Cipher.ENCRYPT_MODE) {//加密
cipher.init(mode, privateKey);
return Base64.encode(cipher.doFinal(data.getBytes()));
} else if (mode == Cipher.DECRYPT_MODE) {//解密
cipher.init(mode, privateKey);
return new String(cipher.doFinal(Base64.decode(data)), StandardCharsets.UTF_8);
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// 公钥解密
public static String publicKeyDecode(String data, String pulickPath, int mode) throws Exception {
// 将文件内容转为字符串
String publicKeyString = FileUtils.readFileToString(new File(pulickPath), Charset.defaultCharset());
// System.out.println(publicKeyString);
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
// 构建密钥规范 进行Base64解码
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
// 生成公钥
PublicKey publicKey = keyFactory.generatePublic(spec);
try {
Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
if (mode == Cipher.ENCRYPT_MODE) {//加密
cipher.init(mode, publicKey);
return Base64.encode(cipher.doFinal(data.getBytes()));
} else if (mode == Cipher.DECRYPT_MODE) {//解密
cipher.init(mode, publicKey);
byte[] bytes = cipher.doFinal(Base64.decode(data));
return new String(bytes, StandardCharsets.UTF_8);
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) throws Exception {
String user = "root";
// 公钥路径
String pubPath = "D:\\IDEA\\encrypt-decrypt\\src\\main\\java\\com\\zwh\\key\\" + user + ".txt";
// 私钥路径
String priPath = "D:\\IDEA\\encrypt-decrypt\\src\\main\\java\\com\\zwh\\key\\" + user + "_en.txt";
makeRsaKeys(pubPath, priPath, user);
String str = "123456";
System.out.println("明文:" + str);
System.out.println("---------私钥加密,公钥解密-----------");
String privateKeyDecode = privateKeyDecode(str, 1, priPath);
System.out.println("私钥加密:" + privateKeyDecode);
String publicKeyDecode = publicKeyDecode(privateKeyDecode, pubPath, 2);
System.out.println("公钥解密:" + publicKeyDecode);
}
}
8 数字签名
基本介绍:
数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证
明。它是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一
个用于签名,另一个用于验证。数字签名是非对称密钥加密技术与数字摘要技术的应用。
简单认识:
相信我们都写过信,在写信的时候落款处总是要留下自己的名字,用来表示写信的人是谁。我们签的这个字就是生活中的签名。
而数字签名呢?其实也是同样的道理,他的含义是:在网络中传输数据时候,给数据添加一个数字签名,表示是谁发的数据,而且还能证明数据没有被篡改。
OK,数字签名的主要作用就是保证了数据的有效性(验证是谁发的)和完整性(证明信息没有被篡改)。下面我们就来好好地看一下他的底层实现原理是什么样子的。
基本原理:
为了理解得清楚,我们通过案例一步一步来讲解。话说张三有俩好哥们A、B。由于工作原因,张三和AB写邮件的时候为了安全都需要加密。于是张三想到了数字签名:
整个思路是这个样子的:
第一步:加密采用非对称加密,张三有三把钥匙,两把公钥,送给朋友。一把私钥留给自己。
第二步:A或者B写邮件给张三:A先用公钥对邮件加密,然后张三收到邮件之后使用私钥解密。
第三步:张三写邮件给A或者B:
(1)张三写完邮件,先用hash函数生成邮件的摘要,附着在文章上面,这就完成了数字签名,然后张三再使用私钥加密。就可以把邮件发出去了。
(2)A或者是B收到邮件之后,先把数字签名取下来,然后使用自己的公钥解密即可。这时候取下来的数字签名中的摘要若和张三的一致,那就认为是张三发来的,再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。
数字证书:
上面提到我们对签名进行验证时,需要用到公钥。如果公钥是伪造的,那我们无法验证数字签名了,也就根本不可能从数字签名确定对方的合法性了。这时候证书就闪亮登场了。我们可能都有考各种证书的经历,比如说普通话证书,四六级证书等等,但是归根结底,到任何场合我们都能拿出我们的证书来证明自己确实已经考过了普通话,考过了四六级。这里的证书也是同样的道理。
如果不理解证书的作用,我们可以举一个例子,比如说我们的毕业证书,任何公司都会承认。为什么会承认?因为那是国家发得,大家都信任国家。也就是说只要是国家的认证机构,我们都信任它是合法的。
此时即使张三的朋友A把公钥弄错了,张三也可以通过这个证书验证。
代码实现:
public class SignatureDemo {
public static void main(String[] args) throws Exception {
String a = "123";
PublicKey publicKey = RSAdemo4.getPublicKey("a.pub", "RSA");
PrivateKey privateKey = RSAdemo4.getPrivateKey("a.pri", "RSA");
String signaturedData = getSignature(a, "sha256withrsa", privateKey);
boolean b = verifySignature(a, "sha256withrsa", publicKey, signaturedData);
System.out.println(b);
System.out.println(signaturedData);
}
/**
* 生成签名
*
* @param input : 原文
* @param algorithm : 算法
* @param privateKey : 私钥
* @return : 签名
* @throws Exception
*/
private static String getSignature(String input, String algorithm, PrivateKey privateKey) throws Exception {
// 获取签名对象
Signature signature = Signature.getInstance(algorithm);
// 初始化签名
signature.initSign(privateKey);
// 传入原文
signature.update(input.getBytes());
// 开始签名
byte[] sign = signature.sign();
// 对签名数据进行Base64编码
return Base64.encode(sign);
}
/**
* 校验签名
*
* @param input : 原文
* @param algorithm : 算法
* @param publicKey : 公钥
* @param signaturedData : 签名
* @return : 数据是否被篡改
* @throws Exception
*/
private static boolean verifySignature(String input, String algorithm, PublicKey publicKey, String signaturedData) throws Exception {
// 获取签名对象
Signature signature = Signature.getInstance(algorithm);
// 初始化签名
signature.initVerify(publicKey);
// 传入原文
signature.update(input.getBytes());
// 校验数据
return signature.verify(Base64.decode(signaturedData));
}
}