一、现代密码学

1、散列函数

散列函数,也见杂凑函数、摘要函数或哈希函数,可将任意长度的消息经过运算,变成固定长度数值,常见的有MD5SHA-1SHA256,多应用在文件校验,数字签名中。

MD5 可以将任意长度的原文生成一个 128 位(16 字节)的哈希值

SHA-1 可以将任意长度的原文生成一个 160 位(20 字节)的哈希值

2、对称密码

对称密码应用了相同的加密密钥和解密密钥。对称密码分为:序列密码(流密码),分组密码(块密码)两种。流密码是对信息流中的每一个元素(一个字母或一个比特)作为基本的处理单元进行加密,块密码是先对信息流分块,再对每一块分别加密。

例如原文为 1234567890,流加密即先对 1 进行加密,再对 2 进行加密,再对 3 进行加密……最后拼接成密文;块加密先分成不同的块,如 1234 成块,5678 成块,90XX(XX 为补位数字)成块,再分别对不同块进行加密,最后拼接成密文。前文提到的古典密码学加密方法,都属于流加密。

3、非对称密码

对称密码的密钥安全极其重要,加密者和解密者需要提前协商密钥,并各自确保密钥的安全性,一但密钥泄露,即使算法是安全的也无法保障原文信息的私密性。

在实际的使用中,远程的提前协商密钥不容易实现,即使协商好,在远程传输过程中也容易被他人获取,因此非对称密钥此时就凸显出了优势。

非对称密码有两支密钥,公钥(publickey)和私钥(privatekey),加密和解密运算使用的密钥不同。用公钥对原文进行加密后,需要由私钥进行解密;用私钥对原文进行加密后(此时一般称为签名),需要由公钥进行解密(此时一般称为验签)。公钥可以公开的,大家使用公钥对信息进行加密,再发送给私钥的持有者,私钥持有者使用私钥对信息进行解密,获得信息原文。因为私钥只有单一人持有,因此不用担心被他人解密获取信息原文。

二、常见加密方式

对称加密

  • 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。
  • 示例
    • 我们现在有一个原文 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 使用

1、DES

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import com.sun.org.apache.xml.internal.security.utils.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class DesAesDemo {
public static void main(String[] args) throws Exception {
// 原文
String input = "hello world";
// des加密key必须是8位
String key = "12345678";
// 算法
String algorithm = "DES";
// 加密类型
String transformation = "DES";

// 进行加密
// 传入原文,密钥,算法,加密类型
String encryptDES = getEncryptDES(input, key, algorithm, transformation);
System.out.println("加密:" + encryptDES);
// 进行解密
// 传入加密过的密文,密钥,加密类型,算法
String decryptDES = getDecryptDES(key, algorithm, transformation, encryptDES);
System.out.println("解密:" + decryptDES);

}

/**
* 使用DES加密数据
*
* @param input : 原文
* @param key : 密钥
* @param algorithm : 获取密钥的算法
* @param transformation : 获取Cipher对象的算法
* @return : 密文
*/
private static String getEncryptDES(String input, String key, String algorithm, String transformation) throws Exception {
// Cipher:密码,获取加密对象
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());
// 打印字节,因为ascii码有负数,解析不出来,所以乱码
// 打印密文乱码
// System.out.println(new String(bytes));
// 使用Base64编码,导入的apache的jar包!
String encode = Base64.encode(bytes);
return encode;
}

/**
* 使用DES解密数据
*
* @param key : 密钥
* @param algorithm : 获取密钥的算法
* @param transformation : 获取Cipher对象的算法
* @param encryptDES : 获取DES加密后的密文
* @return
*/
private static String getDecryptDES(String key, String algorithm, String transformation, String encryptDES) throws Exception {
// 获取Cipher对象
Cipher cipher = Cipher.getInstance(transformation);
// 指定密钥规则
SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
// 对解密进行初始化
cipher.init(Cipher.DECRYPT_MODE, sks);
// 解密
byte[] bytes = cipher.doFinal(Base64.decode(encryptDES));
return new String(bytes);
}
}

toString()与 new String ()用法区别

Base64 加解密是一种转换编码格式的原理

str.toString 是调用了这个 object 对象的类的 toString 方法。一般是返回这么一个 String:[class name]@[hashCode]

new String(str)是根据 parameter 是一个字节数组,使用 java 虚拟机默认的编码格式,将这个字节数组 decode 为对应的字符。若虚拟机默认的编码格式是 ISO-8859-1,按照 ascii 编码表即可得到字节对应的字符。

什么时候用

new String()一般使用字符转码的时候,byte[]数组的时候

toString()对象打印的时候使用

2、AES

与上面的 DES 不同的是 key 要求 16 位,算法和加密类型为 AES,其它的相同

1
2
3
4
5
6
// AES加密key必须是16位
String key = "1234567812345678";
// 算法
String algorithm = "AES";
// 加密类型
String transformation = "AES";

3、Base64

Base64 是网络上最常见的用于传输 8Bit 字节码的可读性编码算法之一
可读性编码算法不是为了保护数据的安全性,而是为了可读性
可读性编码不改变信息内容,只改变信息内容的表现形式
所谓 Base64,即是说在编码过程中使用了 64 种字符:大写 A 到 Z、小写 a 到 z、数字 0 到 9、“+”和“/”
Base58 是 Bitcoin(比特币)中使用的一种编码方式,主要用于产生 Bitcoin 的钱包地址
相比 Base64,Base58 不使用数字”0”,字母大写”O”,字母大写”I”,和字母小写”i”,以及”+”和”/“符号

三、加密模式

1、ECB

ECB : Electronic codebook,电子密码本。需要加密的消息按照块密码的块大小被分为数个块,并对每个块进行独立加密。

  • 优点 : 可以并行处理数据
  • 缺点 : 同样的原文生成同样的密文, 不能很好的保护数据
  • 同时加密,原文是一样的,加密出来的密文也是一样的

2、CBC

CBC : Cipher-block chaining,密码块链接。每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。

  • 优点 : 同样的原文生成的密文不一样
  • 缺点 : 串行处理数据

四、填充模式

当需要按块处理的数据, 数据长度不符合块处理需求时, 按照一定的方法填充满块长的规则

1、NoPadding

  • 不填充.
  • 在 DES 加密算法下, 要求原文长度必须是 8byte 的整数倍
  • 在 AES 加密算法下, 要求原文长度必须是 16byte 的整数倍

2、PKCS5Padding

数据块的大小为 8 位, 不够就补足

3、Tips

Cipher 算法,如果没有指定加密模式和填充模式,ECB/PKCS5Padding 就是默认值

如果使用 CBC 模式, 在初始化 Cipher 对象时, 需要增加参数

初始化向量 IV :IvParameterSpec iv = new IvParameterSpec(key.getBytes());

加密模式和填充模式(括号内数字代表多少位加密)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

DES 使用NoPadding 这种填充模式,加密的原文必须是 8 个字节的整倍数

AES 使用NoPadding 这种填充模式,加密的原文必须是 16 个字节的整倍数

4、演示

使用DES/CBC/PKCS5Padding模式,基于上面的 DES 加密进行修改

设置加密类型为DES/CBC/PKCS5Padding模式

1
2
// 加密类型
String transformation = "DES/CBC/PKCS5Padding";

加密方法getEncryptDES中添加向量,初始化时增加参数

1
2
3
4
// 添加初始化向量用来加密
IvParameterSpec iv = new IvParameterSpec(key.getBytes());
// 对加密进行初始化
cipher.init(Cipher.ENCRYPT_MODE, sks, iv);

解密方法getDecryptDES中添加向量,初始化时增加参数

1
2
3
4
// 添加解密向量
IvParameterSpec iv = new IvParameterSpec(key.getBytes());
// 对解密进行初始化
cipher.init(Cipher.DECRYPT_MODE, sks, iv);

如果使用DES/CBC/NoPadding模式,需要修改原文为 8 个字节的倍数

1
2
// 原文
String input = "hello wo";

五、消息摘要

  • 消息摘要(Message Digest)又称为数字摘要(Digital Digest)
  • 它是一个唯一对应一个消息或文本的固定长度的值,它由一个单向 Hash 加密函数对消息进行作用而产生
  • 使用数字摘要生成的值是不可以篡改的,为了保证文件或者值的安全

1、特点

无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用 MD5 算法摘要的消息有 128 个比特位,用 SHA-1 算法摘要的消息最终有 160 比特位的输出

只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出

消息摘要是单向、不可逆的

常见算法 :

MD5

SHA1

SHA256

SHA512

2、获取字符串消息摘要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.security.MessageDigest;

public class DigestDemo {
public static void main(String[] args) throws Exception {
// 原文
String input = "hello world";

String md5 = getDigest(input, "MD5");
System.out.println("md5 = " + md5);
System.out.println("---------------------------");

String sha1 = getDigest(input, "SHA-1");
System.out.println("sha1 = " + sha1);
System.out.println("---------------------------");

String sha256 = getDigest(input, "SHA-256");
System.out.println("sha256 = " + sha256);
System.out.println("---------------------------");

String sha512 = getDigest(input, "SHA-512");
System.out.println("sha512 = " + sha512);
}

private static String toHex(byte[] digest) throws Exception {

// 创建对象用来拼接
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(algorithm + ":密文的字节长度:" + digest.length);

return toHex(digest);
}
}

3、获取文件消息摘要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.security.MessageDigest;

public class DigestDemo {
public static void main(String[] args) throws Exception {

String sha1 = getDigestFile("D:\\003.jpg", "SHA-1");
System.out.println("sha1 = " + sha1);
System.out.println("---------------------------");

String sha512 = getDigestFile("D:\\003.jpg", "SHA-512");
System.out.println("sha512 = " + sha512);
}

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(algorithm + ":密文的字节长度:" + digest.length);
return toHex(digest);
}

private static String toHex(byte[] 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();
}
}

4、总结

  • MD5 算法 : 摘要结果 16 个字节, 转 16 进制后 32 个字节
  • SHA1 算法 : 摘要结果 20 个字节, 转 16 进制后 40 个字节
  • SHA256 算法 : 摘要结果 32 个字节, 转 16 进制后 64 个字节
  • SHA512 算法 : 摘要结果 64 个字节, 转 16 进制后 128 个字节

六、非对称加密

简介:

  1. 非对称加密算法又称现代加密算法
  2. 非对称加密是计算机通信安全的基石,保证了加密数据不会被破解
  3. 与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)私有密(privatekey)
  4. 公开密钥和私有密钥是一对
  5. 如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密
  6. 如果用私有密钥对数据进行加密,只有用对应的公开密钥才能解密
  7. 因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法
  • 示例
    • 首先生成密钥对, 公钥为(5,14), 私钥为(11,14)
    • 现在 A 希望将原文 2 发送给 B
    • A 使用公钥加密数据. 2 的 5 次方 mod 14 = 4 , 将密文 4 发送给 B(mod:取余运算)
    • B 使用私钥解密数据. 4 的 11 次方 mod 14 = 2, 得到原文 2
  • 特点
    • 加密和解密使用不同的密钥
    • 如果使用私钥加密, 只能使用公钥解密
    • 如果使用公钥加密, 只能使用私钥解密
    • 处理数据的速度较慢, 因为安全级别高
  • 常见算法
    • RSA
    • ECC

1、生成公钥和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.sun.org.apache.xml.internal.security.utils.Base64;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;

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("-----------------私钥、公钥分割线------------------");
// 打印公钥
System.out.println(publicKeyString);
}
}

2、私钥加密公钥解密

在上面RSAdemo的基础上加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String input = "hello world";

// 创建加密对象
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));

公钥加密、私钥解密只需要在初始化加密时公钥私钥换一下就可以了

3、保存公钥和私钥

前面代码每次都会生成 加密和解密,现在把加密和解密的方法全部到项目的根目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import com.sun.org.apache.xml.internal.security.utils.Base64;
import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher;
import java.io.File;
import java.nio.charset.Charset;
import java.security.*;

public class RSAdemo1 {
public static void main(String[] args) throws Exception {
// 加密算法
String algorithm = "RSA";
// 生成密钥对并保存在项目根目录中
generateKeyToFile(algorithm, "./public.rsa", "./private.rsa");
}

/**
* 生成密钥对并保存在本地文件中
*
* @param algorithm : 算法
* @param pubPath : 公钥保存路径
* @param priPath : 私钥保存路径
* @throws Exception
*/
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"));
}

4、读取私钥加密数据

加入读取公钥私钥方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static PrivateKey getPrivateKey(String priPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String privateKeyString = FileUtils.readFileToString(new File(priPath), Charset.defaultCharset());
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
// 生成私钥
return keyFactory.generatePrivate(spec);
}

public static PublicKey getPublicKey(String pulickPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String publicKeyString = FileUtils.readFileToString(new File(pulickPath), Charset.defaultCharset());
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
// 生成公钥
return keyFactory.generatePublic(spec);
}

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import com.sun.org.apache.xml.internal.security.utils.Base64;
import org.apache.commons.io.FileUtils;

import javax.crypto.Cipher;
import java.io.File;
import java.nio.charset.Charset;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public class RSAdemo2 {
public static void main(String[] args) throws Exception {
String input = "hello world";
// 加密算法
String algorithm = "RSA";
// 读取私钥
PrivateKey privateKey = getPrivateKey("./private.rsa", algorithm);
// 读取公钥
PublicKey publicKey = getPublicKey("./public.rsa", algorithm);
// 进行公钥加密
String s = encryptRSA(algorithm, privateKey, input);
// 进行私钥解密
String s1 = decryptRSA(algorithm, publicKey, s);
System.out.println("私钥加密后的数据:" + s);
System.out.println("--------------------------------------------------");
System.out.println("公钥解密后的数据:" + s1);
}

public static PrivateKey getPrivateKey(String priPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String privateKeyString = FileUtils.readFileToString(new File(priPath), Charset.defaultCharset());
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
// 生成私钥
return keyFactory.generatePrivate(spec);
}

public static PublicKey getPublicKey(String pulickPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String publicKeyString = FileUtils.readFileToString(new File(pulickPath), Charset.defaultCharset());
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
// 生成公钥
return keyFactory.generatePublic(spec);
}

public static String encryptRSA(String algorithm, Key key, String input) throws Exception {

Cipher cipher = Cipher.getInstance(algorithm);
// 初始化加密
cipher.init(Cipher.ENCRYPT_MODE, key);
// 私钥加密
byte[] bytes = cipher.doFinal(input.getBytes());
// 对密文进行Base64编码
System.out.println(Base64.encode(bytes));
return Base64.encode(bytes);
}

private static String decryptRSA(String algorithm, PublicKey publicKey, String s) throws Exception {

Cipher cipher = Cipher.getInstance(algorithm);
// 公钥进行解密
cipher.init(Cipher.DECRYPT_MODE, publicKey);
// 由于密文进行了Base64编码, 在这里需要进行解码
byte[] decode = Base64.decode(s);
// 对密文进行解密,不需要使用base64,因为原文不会乱码
byte[] bytes1 = cipher.doFinal(decode);
return new String(bytes1);
}
}

七、数字签名

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。数字签名是非对称密钥加密技术数字摘要技术的应用。

1、简单认识

数字签名的含义:在网络中传输数据时候,给数据添加一个数字签名,表示是谁发的数据,而且还能证明数据没有被篡改。

数字签名的主要作用就是保证了数据的有效性(验证是谁发的)和完整性(证明信息没有被篡改)。

2、原理

用私钥对消息进行加密,得到密文,就是一个签名的过程。而公钥是公开的,其他人可以很容易地使用公钥去尝试解密生成的密文,然后就可以知道这个签名是不是我签的。当然,在数字签名的过程中,对原文的保密性没有要求,所以加密、解密这样的名词在这个场景中并不准确,用签名和解签会更合适。

实际应用中,由于直接对原消息进行签名有安全性问题,而且原消息往往比较长,直接使用 RSA 算法进行签名速度会比较慢,所以我们一般对消息计算其摘要(比如 SHA-256 等),然后对摘要进行签名。只要使用的摘要算法是安全的(MD5、SHA-1 已经不安全了),那么这种方式的数字签名就是安全的。

一个具体的签名过程如下:

发送者对外发布公钥,并声明对应的私钥在自己手上
发送者对消息 M 计算其消息摘要(比如通过 sha256 ),得到消息摘要 D
发送者使用私钥对消息摘要 D 进行签名,得到签名 A
将需要发送的消息 M 和签名 A 一起发送出去

验证过程如下:

接收者首先对收到的消息 M 计算(即获取文件消息摘要,比如通过 sha256 ),得到接收者自己生成的消息摘要 d
接着使用发送者发布在网络的公钥对 A 进行解签,得到消息摘要 D
如果消息摘要 D 和 d 相同,那么证明消息 M 确实是发送者发出的,没有被篡改过

3、数字证书

上面提到我们对签名进行验证时,需要用到公钥。如果公钥是伪造的,那我们无法验证数字签名了,也就根本不可能从数字签名确定对方的合法性了。这时候证书就闪亮登场了。

数字证书这一名词并非是我国原有,而是来自于英文 digital certificate 的翻译。数字证书从本质上来说是一种电子文档,是由电子商务认证中心(以下简称为 CA 中心)所颁发的一种较为权威与公正的证书,

CA 中心采用的是以数字加密技术为核心的数字证书认证技术,通过数字证书,CA 中心可以对互联网上所传输的各种信息进行加密、解密、数字签名与签名认证等各种处理,同时也能保障在数字传输的过程中不被不法分子所侵入,或者即使受到侵入也无法查看其中的内容。

数字证书就相当于社会中的身份证,用户在进行电子商务活动时可以通过数字证书来证明自己的身份,并识别对方的身份,

4、网页加密

应用“数字证书”的实例:https 协议。这个协议主要用于网页加密

首先,客户端向服务器发出加密请求。

服务器用自己的私钥加密网页以后,连同本身的数字证书,一起发送给客户端。

客户端(即浏览器)的“证书管理器”,有“受信任的根证书颁发机构”列表。客户端会根据这张列表,查看解开数字证书的公钥是否在列表之内。

如果数字证书记载的网址,与你正在浏览的网址不一致,就说明这张证书可能被冒用,浏览器会发出警告。

如果这张数字证书不是由受信任的机构颁发的,浏览器会发出另一种警告。

如果数字证书是可靠的,客户端就可以使用证书中的服务器公钥,对信息进行加密,然后与服务器交换加密信息。

5、代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import java.io.File;
import java.nio.charset.Charset;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import com.sun.org.apache.xml.internal.security.utils.Base64;
import org.apache.commons.io.FileUtils;

public class SignatureDemo {
public static void main(String[] args) throws Exception {
String m = "hello world";

PublicKey publicKey = getPublicKey("./public.rsa", "RSA");
PrivateKey privateKey = getPrivateKey("./private.rsa", "RSA");

String signaturedData = getSignature(m, "sha256withrsa", privateKey);

boolean b = verifySignature(m, "sha256withrsa", publicKey, signaturedData);
System.out.println(b);

}

/**
* 生成签名
*
* @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));
}

public static PrivateKey getPrivateKey(String priPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String privateKeyString = FileUtils.readFileToString(new File(priPath), Charset.defaultCharset());
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(privateKeyString));
// 生成私钥
return keyFactory.generatePrivate(spec);
}

public static PublicKey getPublicKey(String pulickPath, String algorithm) throws Exception {
// 将文件内容转为字符串
String publicKeyString = FileUtils.readFileToString(new File(pulickPath), Charset.defaultCharset());
// 获取密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
// 构建密钥规范 进行Base64解码
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode(publicKeyString));
// 生成公钥
return keyFactory.generatePublic(spec);
}
}