加密算法学习


1. 加密算法

对称加密: 加密和解密用到的密钥是相同的, 这种加密方式加密速度非常快, 适合经常发送数据的场合. 缺点是密钥的传输比较麻烦.
非对称加密: 加密和解密用的密钥是不同的, 这种加密方式是用数学上的难解问题构造的, 通常加密解密的速度比较慢, 适合偶尔发送数据的场合. 优点是密钥传输方便.

2. 对称加密算法

2.1 AES

2.1.1 简介

AES(Advanced Encryption Standard)为最常见的对称加密算法(微信小程序加密传输就是用这个加密算法的).对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

AES加密流程

实际中, 一般是通过RSA加密AES的密钥, 传输到接收方, 接收方解密得到AES密钥, 然后发送方和接收方用AES密钥来通信.

2.1.2 Java中使用AES

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
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class Main {
/**
* 获得一个 密钥长度为 256 位的 AES 密钥,
* @return 返回经 BASE64 处理之后的密钥字符串
*/
public static String getStrKeyAES() throws Exception{
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = new SecureRandom(String.valueOf(System.currentTimeMillis()).getBytes("utf-8"));
keyGen.init(256, secureRandom); // 这里可以是 128、192、256、越大越安全
SecretKey secretKey = keyGen.generateKey();
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
}

/**
* 将使用 Base64 加密后的字符串类型的 secretKey 转为 SecretKey
* @param strKey
* @return SecretKey
*/
public static SecretKey strKey2SecretKey(String strKey){
byte[] bytes = Base64.getDecoder().decode(strKey);
SecretKeySpec secretKey = new SecretKeySpec(bytes, "AES");
return secretKey;
}

/**
* 加密
* @param content 待加密内容
* @param secretKey 加密使用的 AES 密钥
* @return 加密后的密文 byte[]
*/
public static byte[] encryptAES(byte[] content, SecretKey secretKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(content);
}

/**
* 解密
* @param content 待解密内容
* @param secretKey 解密使用的 AES 密钥
* @return 解密后的明文 byte[]
*/
public static byte[] decryptAES(byte[] content, SecretKey secretKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(content);
}

public static void main(String[] args) throws Exception{
// 生成密钥
String AESKey = getStrKeyAES();
SecretKey AESSecretKey = strKey2SecretKey(AESKey);
byte[] content = "Hello World!".getBytes();
// AES加密
byte[] C = encryptAES(content, AESSecretKey);
// AES解密
byte[] M = decryptAES(C, AESSecretKey);

System.out.println(new String(M));
}
}

3. 非对称加密算法

3.1 RSA

3.1.1 算法步骤

  1. 找出质数 P, Q
  2. 计算公共模数 N = P * Q
  3. 计算欧拉函数 φ(N) = (P - 1) (Q - 1) , 说明: 符号φ(N)表示小于 N 且与 N 互质的正整数的个数
  4. 计算公钥E: 1 < E < φ(N), 其中, E 的取值必须是整数, E 和 φ(N) 必须是互质数
  5. 计算私钥D: E * D % φ(N) = 1
  6. 加密: C = M^E mod N, 其中, C-密文 M-明文
  7. 解密: M =C^D mod N, 其中, C-密文 M-明文
  8. 公钥=(E, N) , 私钥=(D, N)

3.1.2 算法示例

  1. 找出质数 P,Q (P=3, Q=11)
  2. 计算公共模数, N = P Q = 3 11 = 33
  3. 计算欧拉函数 φ(N) = (P - 1) (Q - 1) = 2 * 10 = 20
  4. 计算公钥E: 1 < E < 20, 因为 E 和 φ(N) 必须是互质数, 所以E 的取值范围 {3, 7, 9, 11, 13, 17, 19} 任取 E =3, 满足条件
  5. 计算私钥D: E D % φ(N) = 1, 即 3 D % 20 = 1, 取 D = 7 满足条件
  6. 公钥加密: 设 M = 2, C = M^E mod N = 2^3 % 33 = 8
  7. 私钥解密: M =C^D mod N = 8^7 % 33 = 2
  8. 公钥=(E, N) = (3, 33), 私钥=(D, N) = (7, 33)

3.1.3 JDK 自带 RSA 算法示例

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
89
90
91
92
93
94
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import java.io.*;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class Main {
public static void main(String[] args) throws Exception{
// 1.生成RSA密钥对
// 1.1 生成密钥对
// 获取指定算法的密钥对生成器
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器(指定密钥长度, 使用默认的安全随机数源)
gen.initialize(2048);
// 随机生成一对密钥(包含公钥和私钥)
KeyPair keyPair = gen.generateKeyPair();
// 获取 公钥 和 私钥
PublicKey pubKey = keyPair.getPublic();
PrivateKey priKey = keyPair.getPrivate();
// 1.2 保存密钥
// 1.2.1 Base64 编码保存
// 获取 公钥和私钥 的 编码格式(通过该 编码格式 可以反过来 生成公钥和私钥对象)
byte[] pubEncBytes = pubKey.getEncoded();
byte[] priEncBytes = priKey.getEncoded();
// 把 公钥和私钥 的 编码格式 转换为 Base64文本 方便保存
String pubEncBase64 = new BASE64Encoder().encode(pubEncBytes);
String priEncBase64 = new BASE64Encoder().encode(priEncBytes);
// 保存 公钥和私钥 到指定文件
IOUtils.writeFile(pubEncBase64, new File("pub.txt"));
IOUtils.writeFile(priEncBase64, new File("pri.txt"));
// 1.2.2 对象序列化保存
// 创建对象输出流, 保存到指定的文件
ObjectOutputStream pubOut = new ObjectOutputStream(new FileOutputStream("pub.obj"));
ObjectOutputStream priOut = new ObjectOutputStream(new FileOutputStream("pri.obj"));
// 将 公钥/私钥 对象序列号写入 对象输出流
pubOut.writeObject(pubKey);
priOut.writeObject(priKey);
// 刷新并关闭流
pubOut.flush();
priOut.flush();
pubOut.close();
priOut.close();
// 1.3 读取密钥
// 1.3.1 读取密钥的 Base64 文本生成密钥对象
// 读取公钥
// 从 公钥保存的文件 读取 公钥的Base64文本
String pubKeyBase64 = IOUtils.readFile(new File("pub.txt"));
// 把 公钥的Base64文本 转换为已编码的 公钥bytes
byte[] encPubKey = new BASE64Decoder().decodeBuffer(pubKeyBase64);
// 创建 已编码的公钥规格
X509EncodedKeySpec encPubKeySpec = new X509EncodedKeySpec(encPubKey);
// 获取指定算法的密钥工厂, 根据 已编码的公钥规格, 生成公钥对象
PublicKey pubKey_1 = KeyFactory.getInstance("RSA").generatePublic(encPubKeySpec);
// 读取私钥
// 从 私钥保存的文件 读取 私钥的base文本
String priKeyBase64 = IOUtils.readFile(new File("pri.txt"));
// 把 私钥的Base64文本 转换为已编码的 私钥bytes
byte[] encPriKey = new BASE64Decoder().decodeBuffer(priKeyBase64);
// 创建 已编码的私钥规格
PKCS8EncodedKeySpec encPriKeySpec = new PKCS8EncodedKeySpec(encPriKey);
// 获取指定算法的密钥工厂, 根据 已编码的私钥规格, 生成私钥对象
PrivateKey priKey_1 = KeyFactory.getInstance("RSA").generatePrivate(encPriKeySpec);
// 1.3.2 反序列化生成密钥对象
// 公钥和私钥对象被序列号保存后,可以通过反序列化生成回对象。
// 创建对象输如流, 读取保存到指定文件的序列化对象
ObjectInputStream pubIn = new ObjectInputStream(new FileInputStream("pub.obj"));
ObjectInputStream priIn = new ObjectInputStream(new FileInputStream("pri.obj"));
// 从读取输如流读取对象, 反序列化生成 公钥/私钥 对象
PublicKey pubKey_2 = (PublicKey) pubIn.readObject();
PrivateKey priKey_2 = (PrivateKey) priIn.readObject();
// 关闭流
pubIn.close();
priIn.close();
// 2. RSA 加密/解密数据
// 2.1 公钥加密
// 获取指定算法的密码器
Cipher cipher = Cipher.getInstance("RSA");
// 初始化密码器(公钥加密模型)
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
// 加密数据, 返回加密后的密文
byte[] plainData = "Hello World!".getBytes();
byte[] cipherData = cipher.doFinal(plainData);
// 2.2 私钥解密
// 获取指定算法的密码器
Cipher cipher_2 = Cipher.getInstance("RSA");
// 初始化密码器(私钥解密模型)
cipher_2.init(Cipher.DECRYPT_MODE, priKey);
// 解密数据, 返回解密后的明文
byte[] plainData_2 = cipher_2.doFinal(cipherData);

System.out.println(new String(plainData_2));
}
}

3.2 MD5算法

MD5的英文全称是Message Digest Algorithm MD5,译为消息摘要算法第五版,是众多哈希算法中的一种(哈希算法是一种可以将任意长度的输入转化为固定长度输出的算法)
因此MD5算法是一种哈希算法,严格来说不能称之为一种加密算法.

3.2.1 MD5算法的优点

  1. 容易计算:主流的编程语言基本都支持MD5算法的实现,所以非常容易计算出一个数据的MD5值.
  2. 不可逆性:无法通过常规的方式从MD5值倒推出它的原文.
  3. 压缩性:任意长度的数据,其MD5值都是一个32位长度的十六进制字符串,区分大小写.
  4. 抗修改性:对原数据做一丁点的改动,MD5值就会有巨大的变动.
  5. 抗碰撞性:知道了原数据的MD5值,想要碰撞出这个MD5值,从而猜测出原数据,是非常困难的.所有的MD5值一共有2的128次方种可能性.

3.2.2 MD5算法的不足

目前存在比较优的碰撞算法,因此在对安全性要求较高的场合,不建议直接使用MD5算法.

3.2.3 应用场景

数据库中用户信息的存储.

但是因为彩虹表的存在,所以也并不可靠.(彩虹表就是一个庞大的数据库,这个数据库里收集了着所有人常用的密码,以及这些密码对应的MD5值,SHA-X值等哈希值)

针对彩虹表问题,可以使用加盐的解决方案.

加盐一般的做法:

  1. 客户端MD5(pwd)
  2. 服务端MD5(客户端MD5(pwd)+salt),服务端保存密码 和 盐

3.2.4 Java中的使用MD5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.security.MessageDigest;
public class Main {
public static String string2MD5(String inStr) throws Exception{
MessageDigest md5 = MessageDigest.getInstance("MD5");
char[] charArray = inStr.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++){
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16)
hexValue.append("0");
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
public static void main(String args[]) throws Exception{
String input = "Glenn";
System.out.println("MD5:"+string2MD5(input));
}
}

3.2.5 MD5算法原理

  1. 数据填充: 对消息进行数据填充,使消息的长度对512取模得448,设消息长度为X, 即满足X mod 512=448. 根据此公式得出需要填充的数据长度. 填充方法:在消息后面进行填充, 填充第一位为1, 其余为0. 填充完后, 信息的长度就是512 * N+448.

  2. 添加消息长度: 用剩余的位置(512-448=64位)记录原文的真正长度,把长度的二进制值补在最后.这样处理后的信息长度就是512*(N+1). 如果消息长度大于2^64, 则只使用其低64位的值, 即 消息长度 对 2^64取模. 在此步骤进行完毕后, 最终消息长度就是512的整数倍.

  3. 数据处理(设置初始值):MD5的哈希结果长度为128位,按每32位分成一组共4组.这4组结果是由4个初始值A,B,C,D经过不断演变得到.MD5的官方实现中,A,B,C,D的初始值如下(16进制):
    4个常数:
    A = 0x01234567
    B = 0x89ABCDEF
    C = 0xFEDCBA98
    D = 0x76543210

  4. 数据处理(循环加工):
    使用如下四个函数循环加工,每一次循环都会让旧的ABCD产生新的ABCD.最后一次循环的结果即为MD5值.
    4个函数:
    F(X,Y,Z)=(X & Y) | ((~X) & Z)
    G(X,Y,Z)=(X & Z) | (Y & (~Z))
    H(X,Y,Z)=X ^ Y ^ Z
    I(X,Y,Z)=Y ^ (X | (~Z))

4. 参考文献

AES 加密算法的原理详解
Java实现AES和RSA算法
RSA公钥密码体制的原理及应用
RSA 非对称加密原理(小白也能看懂哦~)
Java 实现 RSA 非对称加密算法:生成密钥对、保存/读取密钥、加密/解密
第一篇、MD5算法和SHA-1算法
java中使用MD5加密技术
MD5算法底层原理
MD5加密算法原理及实现

谢谢你请我吃糖果!