常见加解密

9/11/2023 Java

摘要

JDK:1.8.0_202

# 一:前言

数据编码、数字签名、信息加密 是前后端开发都经常需要使用到的技术,应用场景包括了用户登入、交易、信息通讯、OAuth 等等,不同的应用场景也会需要使用到不同的签名加密算法,或者需要搭配不一样的 签名加密算法 来达到业务目标。下面介绍几种常见的签名加密算法和一些典型场景下的应用。主要包括:

  • 数据编码Base64
  • 散列算法(消息摘要、签名算法)MD5/SHA/MAC
  • 对称加密算法DES/AES/RC2/RC4
  • 非对称加密算法RSA

# 二:数据编码

# 2.1 编码原理

Base64算法并不是加密算法,它的出现是为了解决ASCII码在传输过程中可能出现乱码的问题。Base64是网络上最常见的用于传输 8bit字节码 的可读性编码算法之一。可读性编码算法不是为了保护数据的安全性,而是为了可读性。可读性编码不改变信息内容,只改变信息内容的表现形式。Base64使用了64种字符:大写A到Z、小写a到z、数字0到9、"+"和"/",故得此名。

在UTF-8编码下,一个中文占3个字节;在GBK编码下,一个中文占2个字节。可以查看 汉字占多少字节

Base64编码表
索引 对应字符 索引 对应字符 索引 对应字符 索引 对应字符
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y

Base64编码的过程:

  1. 将 字符串 转换为 字符数组;
  2. 将每个 字符 转换为 ASCII码;
  3. 将 ASCII码 转换为 8bit二进制码;
  4. 然后每 3个字节 为一组(一个字节为8个bit,所以每组24个bit);
  5. 将每组的 24个bit分为 4份,每份 6个bit;
  6. 每 6个bit 前补0,补齐 8bit(前面补0不影响数值大小);
  7. 然后将 每 8bit 转换为 10进制数,根据上面的Base64编码表进行转换。

上面步骤中,为什么要将每组24个bit分为4份,每份6个bit呢?因为6bit的最大值为111111,转换为十进制为63,所以6bit的取值范围为0~63,这和base64编码表长度一致。

例子

现要对 hello 这个字符串进行Base64编码,过程如下:

  1. hello 转换为字符数组:hello
  2. 对应的 ASCII 码为:104101108108111
  3. 转换为 8bit 二进制数:0110100001100101011011000110110001101111
  4. 分组,每组 24个bit(不足24个bit的用00000000补齐): 011010000110010101101100011011000110111100000000
  5. 每组 24bit 分为4份,每份6bit:011010000110010101101100011011 000110111100000000
  6. 在 每6个bit 前补0,补齐8bit:0001101000000110000101010010110000011011000001100011110000000000
  7. 将每8bit转换为10进制数:2662144276600
  8. 从上面Base64编码表中找到十进制数对应的字符(末尾的0并不是A,而是用=等号补位):aGVsbG8=

代码验证:

@Test
public void test1() {
	System.out.println(Base64.getEncoder().encodeToString("hello".getBytes()));
}
1
2
3
4

# 2.2 URL Base64算法

Base64编码值通过URL传输会出现问题,因为Base64编码中的“+”和“/”符号是不允许出现在URL中的。同样,符号“=”用做参数分隔符,也不允许出现在URL中,根据RFC 4648中的建议,~~“”和“.”符都有可能替代“=”符号。但“”~~符号与文件系统相冲突,不能使用;如果使用“.”符号,某些文件系统认为该符号连续出现两次则为错误。所以 common codec 包下的 URL Base64 算法舍弃了填充符,使用了不定长 URL Base64 编码。

引入common codec依赖包(包含一些加解密工具类,用于增强JDK或者简化JDK相关加解密API):

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>
1
2
3
4
5

测试:

@Test
public void test2() {
    String s = "hello";
    System.out.println(org.apache.commons.codec.binary.Base64.encodeBase64String(s.getBytes()));
    System.out.println(org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(s.getBytes()));
}
1
2
3
4
5
6

# 三:散列算法

散列算法(消息摘要算法、签名算法)是单向不可逆的,无法通过加密后的散列值反推原始值,相同的内容用同样的摘要算法获得的散列值是一样的,所以常用于验证数据的完整性。该算法主要分为三大类:

  1. MD(Message Digest,消息摘要算法)
    • MD2
    • MD4
    • MD5
  2. SHA(Secure HashAlgorithm,安全散列算法)
    • SHA-1
    • SHA-1的变种SHA-2系列(包含SHA-224、SHA-256、SHA-384和SHA-512)
  3. MAC(Message Authentication Code,消息认证码算法)综合了上述两种算法
    • HmacMD5
    • HmacSHA1
    • HmacSHA256
    • HmacSHA384
    • HmacSHA512

# 3.1 MD 系列算法

MD5算法是MD系列算法的代表,由MD2、MD4等算法演变而来。无论采用哪种MD算法,结果都是32字节的16进制字符串。JDK8只支持MD2和MD5两种MD算法。

新建一个maven项目,引入common-codec依赖:

JDK8中,MD系列算法的实现是通过 MessageDigest 类来完成的,下面演示下使用JDK8原生API实现MD2和MD5加密(算法名称不区分大小写)。

import org.apache.commons.codec.binary.Hex;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MdTest {

    @Test
    public void test1() throws Exception {
        String value = "hello";
        // MD2 加密
        String md2 = convert(value, "MD2");
        System.out.println(md2);
        System.out.println(md2.length());
        // MD5 加密
        String md5 = convert(value, "MD5");
        System.out.println(md5);
        System.out.println(md5.length());
    }

    /**
     * MD加密 转十六进制
     *
     * @param value     待加密字符串
     * @param algorithm 加密类型
     * @return 十六进制加密内容
     */
    private String convert(String value, String algorithm) throws NoSuchAlgorithmException {
        // 获取 MessageDigest 实例
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        // 调用 digest 方法生成数字摘要
        byte[] digest = messageDigest.digest(value.getBytes());
        // 结果转换为 16 进制字符串(借助 common-codec Hex类)
        return Hex.encodeHexString(digest);
    }

}
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

common-codec 的 DigestUtils 也提供了MD2和MD5算法相关方法,不过他们并没有自己实现相关算法,而是对JDK原生API进行封装,使用起来更方便。

import org.apache.commons.codec.digest.DigestUtils;

public class MdTest {
	@Test
    public void test2() {
        String value = "hello";
        System.out.println(DigestUtils.md2Hex(value.getBytes()));
        System.out.println(DigestUtils.md5Hex(value.getBytes()));
    }
}
1
2
3
4
5
6
7
8
9
10

# 3.2 SHA 系列算法

SHA 算法是基于 MD4 算法实现的,作为 MD 算法的继任者,成为了新一代的消息摘要算法的代表。SHA 与 MD 算法不同之处主要在于摘要长度,SHA 算法的摘要更长,安全性更高。

SHA 算法家族目前共有 SHA-1SHA-224SHA-256SHA-384SHA-512 五种算法,通常将后四种算法并称为 SHA-2 算法。JDK8 支持上述五种算法,其中 SHA 的写法等价于 SHA-1。JDK8中,SHA 系列算法的实现也是通过 MessageDigest 类来完成的:

import org.apache.commons.codec.binary.Hex;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class ShaTest {

    @Test
    public void test1() throws NoSuchAlgorithmException {
        String value = "hello";
        MessageDigest m1 = MessageDigest.getInstance("SHA-1");
        System.out.println(Hex.encodeHexString(m1.digest(value.getBytes())));
        MessageDigest m2 = MessageDigest.getInstance("SHA-256");
        System.out.println(Hex.encodeHexString(m2.digest(value.getBytes())));
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

common-codec同样也提供了SHA相关算法的API:

import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;

public class ShaTest {

    @Test
    public void test2() {
        String value = "hello";

        String sha1Hex = DigestUtils.sha1Hex(value);
        System.out.println(sha1Hex);

        String sha256Hex = DigestUtils.sha256Hex(value);
        System.out.println(sha256Hex);

        String sha384Hex = DigestUtils.sha384Hex(value);
        System.out.println(sha384Hex);

        String sha512Hex = DigestUtils.sha512Hex(value);
        System.out.println(sha512Hex);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3.3 MAC 系列算法

MAC 算法结合了 MD 和 SHA 算法的优势,并加入秘钥的支持,是一种更为安全的消息摘要算法。因为 MAC 算法融合了秘钥散列函数(keyed-Hash),通常也把 MAC 称为 HMAC(keyed-Hash Message Authentication Code)。

MAC 算法主要集合了 MDSHA 两大系列消息摘要算法。MD 系列算法有 HmacMD2HmacMD4HmacMD5 三种算法;SHA 系列算法有 HmacSHA1HmacSHA224HmacSHA256HmacSHA384HmacSHA512 五种算法。

JDK8支持了 HmacMD5HmacSHA1HmacSHA224HmacSHA256HmacSHA384HmacSHA512 这六种MAC算法,通过 Mac 类实现。

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class MacTest {

    @Test
    public void test() throws Exception {
        String value = "hello";
        // JDK支持 HmacMD5、HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384 和 HmacSHA512 六种算法
        String algorithm = "HmacMD5";
        // 初始化KeyGenerator
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 构建秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 获得秘钥
        byte[] key = secretKey.getEncoded();
        // 还原秘钥
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, algorithm);
        // 打印秘钥
        System.out.println(Base64.encodeBase64String(secretKeySpec.getEncoded()));
        // 实例化Mac
        Mac mac = Mac.getInstance(algorithm);
        // 初始化Mac
        mac.init(secretKeySpec);
        // 获取消息摘要
        byte[] bytes = mac.doFinal(value.getBytes());
        // 转换为16进制
        System.out.println(Hex.encodeHexString(bytes));
    }

}
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

# 四:对称加密算法

对称加密算法加密和解密使用的是同一份秘钥,解密是加密的逆运算。对称加密算法加密速度快,密文可逆,一旦秘钥文件泄露,就会导致原始数据暴露。对称加密的结果一般使用 Base64 算法编码,便于阅读和传输。JDK8支持的对称加密算法主要有 DESDESedeAESBlowfish,以及 RC2RC4 等。不同的算法秘钥长度不同,秘钥长度越长,加密安全性越高。

# 4.1 DES

DES(Data Encryption Standard,数据加密标准)算法是对称加密算法领域中的典型算法,DES 算法秘钥较短,以现在计算机的计算能力,DES 算法加密的数据在24小时内可能被破解。所以 DES 算法已经被淘汰,建议使用 AES 算法,不过这里还是简单了解下。

JDK8仅支持 56 位长度的 DES秘钥

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.util.Base64;

public class DesTest {

    @Test
    public void test() throws Exception {
        String value = "helloWorld";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "DES";
        // 转换模式
        String transformation = "DES";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 初始化秘钥长度
        keyGenerator.init(56);
        // 生成秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 实例化DES秘钥材料
        DESKeySpec desKeySpec = new DESKeySpec(secretKey.getEncoded());
        // 实例化秘钥工厂
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
        // 生成DES秘钥
        SecretKey desSecretKey = secretKeyFactory.generateSecret(desKeySpec);
        System.out.println("DES秘钥:" + Base64.getEncoder().encodeToString(desSecretKey.getEncoded()));

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
        cipher.init(Cipher.ENCRYPT_MODE, desSecretKey);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println("DES加密结果:" + Base64.getEncoder().encodeToString(encrypt));
        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, desSecretKey);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("DES解密结果:" + new String(decrypt));
    }

}
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

上面步骤总结为如下几步:

  1. 生成加密秘钥; 1.1 通过 keyGenerator 生成一个指定位数的秘钥; 1.2 通过上面生成的秘钥实例化算法对应的秘钥材料 KeySpec; 1.3 使用秘钥材料通过秘钥工厂 SecretKeyFactory 生成算法秘钥 SecretKey
  2. 通过转换模式实例化 Cipher
  3. 指定 Cipher 模式和秘钥,进行加解密操作。

如果在生成秘钥的时候,不指定为 56 位,则会抛出 java.security.InvalidParameterException: Wrong keysize: must be equal to 56 异常。

# 4.2 DESede

作为 DES 算法的一种改良,DESede 算法(也称为 3DES,三重DES)针对其秘钥长度偏短和迭代次数偏少等问题做了相应改进,提高了安全强度,但同时也造成处理速度较慢、秘钥计算时间加长、加密效率不高的问题。所以这里还是简单了解下,实际还是推荐用AES

JDK8支持 112位或 168位长度的DESede秘钥

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import java.util.Base64;

public class DESedeTest {

    @Test
    public void test() throws Exception {
        String value = "helloWorld";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "DESede";
        // 转换模式
        String transformation = "DESede";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 初始化秘钥长度
        keyGenerator.init(112);
        // 生成秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 实例化DESede秘钥材料
        DESedeKeySpec desKeySpec = new DESedeKeySpec(secretKey.getEncoded());
        // 实例化秘钥工厂
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
        // 生成DES秘钥
        SecretKey desSecretKey = secretKeyFactory.generateSecret(desKeySpec);
        System.out.println("DESede秘钥:" + Base64.getEncoder().encodeToString(desSecretKey.getEncoded()));

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
        cipher.init(Cipher.ENCRYPT_MODE, desSecretKey);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println("DESede加密结果:" + Base64.getEncoder().encodeToString(encrypt));
        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, desSecretKey);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("DESede解密结果:" + new String(decrypt));
    }

}
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

如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Wrong keysize: must be equal to 112 or 168 异常。

# 4.3 AES

AES(AdvancedEncryption Standard,高级数据加密标准)算法支持 128 位、192 位和256 位的秘钥长度,加密速度比 DES 和 DESede 都快,至今还没有被破解的报道。经过验证,目前采用的 AES 算法能够有效抵御已知的针对 DES 算法的所有攻击方法,如部分差分攻击、相关秘钥攻击等。AES 算法因秘钥建立时间短、灵敏性好、内存需求低等优点,在各个领域得到广泛的研究与应用。

JDK8支持 128 位、192 位和256 位长度的AES秘钥

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AesTest {

    @Test
    public void test() throws Exception {
        String value = "helloWorld";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "AES";
        // 转换模式
        String transformation = "AES";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 初始化秘钥长度
        keyGenerator.init(256);
        // 生成秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 生成秘钥材料
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
        System.out.println("AES秘钥:" + Base64.getEncoder().encodeToString(secretKey.getEncoded()));

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println("AES加密结果:" + Base64.getEncoder().encodeToString(encrypt));
        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("AES解密结果:" + new String(decrypt));
    }

}
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

如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256 异常。

# 4.4 RC2、RC4

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class TcTest {

    @Test
    public void test() throws Exception {
        String value = "helloWorld";
        convert(value, "RC2", "RC2");
        System.out.println("============================");
        convert(value, "RC4", "RC4");
    }

    /**
     * 加密
     *
     * @param value          待加密值
     * @param algorithm      加密算法
     * @param transformation 转换模式
     */
    private void convert(String value, String algorithm, String transformation) throws Exception {
        System.out.println("待加密值:" + value);
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 初始化秘钥长度
        keyGenerator.init(666);
        // 生成秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 生成秘钥材料
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
        System.out.println(algorithm + "秘钥:" + Base64.getEncoder().encodeToString(secretKey.getEncoded()));

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println(algorithm + "加密结果:" + Base64.getEncoder().encodeToString(encrypt));
        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println(transformation + "解密结果:" + new String(decrypt));
    }

}
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

如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Key length for RC2 must be between 40 and 1024 bits 异常。

# 4.5 Blowfish

Blowfish 算法也可以用于替换 DES,Blowfish 算法的秘钥长度范围为 32到448位,并且必须为8的倍数

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class BlowfishTest {

    @Test
    public void test() throws Exception {
        String value = "helloWorld";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "Blowfish";
        // 转换模式
        String transformation = "Blowfish";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 初始化秘钥长度
        keyGenerator.init(128);
        // 生成秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 生成秘钥材料
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
        System.out.println("Blowfish秘钥:" + Base64.getEncoder().encodeToString(secretKey.getEncoded()));

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println("Blowfish加密结果:" + Base64.getEncoder().encodeToString(encrypt));
        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("Blowfish解密结果:" + new String(decrypt));
    }

}
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

如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Keysize must be multiple of 8, and can only range from 32 to 448 (inclusive) 异常。

# 五:非对称加密算法

非对称加密和对称加密算法相比,多了一把秘钥,为双秘钥模式,一个公开称为公钥,一个保密称为私钥。遵循公钥加密私钥解密,或者私钥加密公钥解密。非对称加密算法源于DH算法,后又有基于椭圆曲线加密算法的密钥交换算法ECDH,不过目前最为流行的非对称加密算法是RSA,本文简单记录下RSA的使用。

# 5.1 RSA 算法

RSA算法是最为典型的非对称加密算法,该算法由美国麻省理工学院(MIT)的Ron Rivest、Adi Shamir和Leonard Adleman三位学者提出,并以这三位学者的姓氏开头字母命名,称为RSA算法。

RSA算法的数据交换过程分为如下几步:

  1. A构建RSA秘钥对;
  2. A向B发布公钥;
  3. A用私钥加密数据发给B;
  4. B用公钥解密数据;
  5. B用公钥加密数据发给A;
  6. A用私钥解密数据。

JDK8支持RSA算法:

算法 秘钥长度 加密模式 填充模式
RSA 512~16384位,64倍数 ECB NoPadding
PKCS1Padding
OAEPWithMD5AndMGF1Padding
OAEPWithSHA1AndMGF1Padding
OAEPWithSHA-1AndMGF1Padding
OAEPWithSHA-224AndMGF1Padding
OAEPWithSHA-256AndMGF1Padding
OAEPWithSHA-384AndMGF1Padding
OAEPWithSHA-512AndMGF1Padding
OAEPWithSHA-512/224AndMGF1Padding
OAEPWithSHA-512/2256ndMGF1Padding
import javax.crypto.Cipher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;

public class RsaTest {

    @Test
    public void test1() throws Exception {
        String value = "helloWorld";
        // 加密算法
        String algorithm = "RSA";
        // 转换模式
        String transform = "RSA/ECB/PKCS1Padding";
        // 实例化秘钥对生成器
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
        // 初始化,秘钥长度512~16384位,64倍数
        keyPairGenerator.initialize(512);
        // 生成秘钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        // 公钥
        PublicKey publicKey = keyPair.getPublic();
        System.out.println("RSA公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
        // 私钥
        PrivateKey privateKey = keyPair.getPrivate();
        System.out.println("RSA私钥: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));

        // ------ 测试公钥加密,私钥解密 ------
        Cipher cipher = Cipher.getInstance(transform);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] pubEncryptBytes = cipher.doFinal(value.getBytes());
        System.out.println("RSA公钥加密后数据: " + Base64.getEncoder().encodeToString(pubEncryptBytes));

        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] priDecryptBytes = cipher.doFinal(pubEncryptBytes);
        System.out.println("RSA私钥解密后数据: " + new String(priDecryptBytes));

        // ------ 测试私钥加密,公钥解密 ------
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] priEncryptBytes = cipher.doFinal(value.getBytes());
        System.out.println("RSA私钥加密后数据: " + Base64.getEncoder().encodeToString(priEncryptBytes));

        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] pubDecryptBytes = cipher.doFinal(priEncryptBytes);
        System.out.println("RSA公钥解密后数据: " + new String(pubDecryptBytes));
    }

}
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

如果拥有RSA公钥或私钥,需要将它们还原为 PublicKey 和 PrivateKey 对象

@Test
public void test2() throws Exception {
    String value = "hw";
    // 加密算法
    String algorithm = "RSA";
    // 转换模式
    String transform = "RSA/ECB/PKCS1Padding";
    // RSA公钥BASE64字符串
    String rsaPublicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANRWZfv5tdLJBtI8/L5uZuHpiLalxcmvwuTkVK5TUQXMrkqBdBSOcC+WFTHNXAggrJMWEopwSzgYATJd9jb6EHkCAwEAAQ==";
    // RSA私钥BASE64字符串
    String rsaPrivateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA1FZl+/m10skG0jz8vm5m4emItqXFya/C5ORUrlNRBcyuSoF0FI5wL5YVMc1cCCCskxYSinBLOBgBMl32NvoQeQIDAQABAkEAozGYBjYgQVWRcYm/8pglaGG1WjNENUNpdcPrNWQBdIM5Y93PFeDg+/D0fZvT+WlH53vlDLuShwZuDMUx4PqApQIhAPVafQOrhbqh36WJwav1mtkokRhW3kumlGjnbL8+xUrTAiEA3Y0m1nAUGtafDKbW6IHSq+fvntxd6WAOeTzaMqnvEAMCIBtxDX51lrVzGXKIX9L9213ifaf9P0uyy/KXv7/8I1DlAiBeMGIwjFmfx1q68Dsxge/ksahHq3wpeXLtzBcfrus5rQIhAIpifvoyqLf8MJAftohw9Lu+pTUqKc+UQSvj2SdQ3ZJv";

    // ------- 还原公钥 --------
    byte[] publicKeyBytes = Base64.getDecoder().decode(rsaPublicKey);
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
    PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);

    // ------- 还原私钥 --------
    byte[] privateKeyBytes = Base64.getDecoder().decode(rsaPrivateKey);
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
    PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);

    // ------- 测试加解密 --------
    Cipher cipher = Cipher.getInstance(transform);
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte[] pubEncryptBytes = cipher.doFinal(value.getBytes());
    System.out.println("RSA公钥加密数据: " + Base64.getEncoder().encodeToString(pubEncryptBytes));

    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] priDecryptBytes = cipher.doFinal(pubEncryptBytes);
    System.out.println("RSA私钥解密数据: " + new String(priDecryptBytes));
}
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

# 5.2 分段加解密

RSA加解密中必须考虑到的密钥长度、明文长度和密文长度问题。明文长度需要小于密钥长度,而密文长度则等于密钥长度。因此当加密内容长度大于密钥长度时,有效的RSA加解密就需要对内容进行分段。

这是因为,RSA算法本身要求加密内容也就是明文长度m必须满足 0<m<密钥长度n。如果小于这个长度就需要进行padding,因为如果没有padding,就无法确定解密后内容的真实长度,字符串之类的内容问题还不大,以0作为结束符,但对二进制数据就很难,因为不确定后面的0是内容还是内容结束符。而只要用到padding,那么就要占用实际的明文长度,于是实际明文长度需要减去padding字节长度。我们一般使用的padding标准有 NoPPaddingOAEPPaddingPKCS1Padding等,其中PKCS#1建议的padding就占用了11个字节。

以秘钥长度为1024bits为例:

@Test
public void test3() throws Exception {
    StringBuilder value = new StringBuilder();
    for (int i = 0; i <= 29; i++) {
    	value.append("18cm");
    }
    System.out.println("待加密内容长度: " + value.toString().length());
    // 加密算法
    String algorithm = "RSA";
    // 转换模式
    String transform = "RSA/ECB/PKCS1Padding";
    // 实例化秘钥对生成器
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
    // 初始化,秘钥长度512~16384位,64倍数
    keyPairGenerator.initialize(1024);
    // 生成秘钥对
    KeyPair keyPair = keyPairGenerator.generateKeyPair();
    // 公钥
    PublicKey publicKey = keyPair.getPublic();
    System.out.println("RSA公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
    // 私钥
    PrivateKey privateKey = keyPair.getPrivate();
    System.out.println("RSA私钥: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));

    // ------ 测试公钥加密,私钥解密 ------
    Cipher cipher = Cipher.getInstance(transform);
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte[] pubEncryptBytes = cipher.doFinal(value.toString().getBytes());
    System.out.println("RSA公钥加密后数据: " + Base64.getEncoder().encodeToString(pubEncryptBytes));

    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] priDecryptBytes = cipher.doFinal(pubEncryptBytes);
    System.out.println("RSA私钥解密后数据: " + new String(priDecryptBytes));

    // ------ 测试私钥加密,公钥解密 ------
    cipher.init(Cipher.ENCRYPT_MODE, privateKey);
    byte[] priEncryptBytes = cipher.doFinal(value.toString().getBytes());
    System.out.println("RSA私钥加密后数据: " + Base64.getEncoder().encodeToString(priEncryptBytes));

    cipher.init(Cipher.DECRYPT_MODE, publicKey);
    byte[] pubDecryptBytes = cipher.doFinal(priEncryptBytes);
    System.out.println("RSA公钥解密后数据: " + new String(pubDecryptBytes));
}
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

对于1024长度的密钥。128字节(1024bits/8)减去PKCS#1建议的padding就占用了11个字节正好是117字节。所以加密的明文长度120字节大于117字节,程序抛出了异常。

要解决这个问题,可以采用分段加密的手段。编写一个分段加解密的工具类:

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;

/**
 * RSA分段加解密
 * 针对秘钥长度为1024bits
 */
public class RsaUtil {

    // 最大加密块长度 1024/8 - 11
    private static final int MAX_ENCRYPT_BLOCK = 117;
    // 最大解密块长度 1024/8
    private static final int MAX_DECRYPT_BLOCK = 128;
    private static final String TRANSFORM = "RSA/ECB/PKCS1Padding";

    /**
     * 公钥加密
     *
     * @param publicKey 公钥
     * @param value     待加密值
     * @return 加密值
     */
    public static String encrypt(PublicKey publicKey, String value) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            Cipher cipher = Cipher.getInstance(TRANSFORM);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] bytes = value.getBytes();
            int length = bytes.length;

            int offSet = 0;
            byte[] cache;
            int i = 0;
            // 对数据分段加密
            while (length - offSet > 0) {
                if (length - offSet > MAX_ENCRYPT_BLOCK) {
                    cache = cipher.doFinal(bytes, offSet, MAX_ENCRYPT_BLOCK);
                } else {
                    cache = cipher.doFinal(bytes, offSet, length - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * MAX_ENCRYPT_BLOCK;
            }
            byte[] encryptedData = out.toByteArray();
            return Base64.getEncoder().encodeToString(encryptedData);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 私钥解密
     *
     * @param privateKey 私钥
     * @param encrypt    带解密值
     * @return 解密值
     */
    public static String decrypt(PrivateKey privateKey, String encrypt) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            Cipher cipher = Cipher.getInstance(TRANSFORM);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] bytes = Base64.getDecoder().decode(encrypt);
            int length = bytes.length;

            int offSet = 0;
            byte[] cache;
            int i = 0;
            // 对数据分段解密
            while (length - offSet > 0) {
                if (length - offSet > MAX_DECRYPT_BLOCK) {
                    cache = cipher.doFinal(bytes, offSet, MAX_DECRYPT_BLOCK);
                } else {
                    cache = cipher.doFinal(bytes, offSet, length - offSet);
                }
                out.write(cache, 0, cache.length);
                i++;
                offSet = i * MAX_DECRYPT_BLOCK;
            }
            return out.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}
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

测试:

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

public class RsaUtilTest {

    @Test
    public void test() throws Exception {
        StringBuilder value = new StringBuilder();
        for (int i = 0; i <= 29; i++) {
            value.append("18cm");
        }
        System.out.println("待加密内容长度: " + value.toString().length());
        // 加密算法
        String algorithm = "RSA";
        // 实例化秘钥对生成器
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
        // 初始化,秘钥长度512~16384位,64倍数
        keyPairGenerator.initialize(1024);
        // 生成秘钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        // 公钥
        PublicKey publicKey = keyPair.getPublic();
        System.out.println("RSA公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
        // 私钥
        PrivateKey privateKey = keyPair.getPrivate();
        System.out.println("RSA私钥: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));

        // ------ 测试公钥加密,私钥解密 ------
        String pubEncrypt = RsaUtil.encrypt(publicKey, value.toString());
        System.out.println("RSA公钥加密后数据: " + pubEncrypt);

        String priDecrypt = RsaUtil.decrypt(privateKey, pubEncrypt);
        System.out.println("RSA私钥解密后数据: " + priDecrypt);

    }

}
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

# 5.3 建议

  1. 公钥是通过A发送给B的,其在传递过程中很有可能被截获,也就是说窃听者很有可能获得公钥。如果窃听者获得了公钥,向A发送数据,A是无法辨别消息的真伪的。因此,虽然可以使用公钥对数据加密,但这种方式还是会有存在一定的安全隐患。如果要建立更安全的加密消息传递模型,就需要AB双方构建两套非对称加密算法密钥,仅遵循“私钥加密,公钥解密”的方式进行加密消息传递;
  2. RSA不适合加密过长的数据,虽然可以通过分段加密手段解决,但过长的数据加解密耗时较长,在响应速度要求较高的情况下慎用。一般推荐使用非对称加密算法传输对称加密秘钥,双方数据加密用对称加密算法加解密。

# 六:参考文献

最后更新: 9/23/2023, 3:55:03 PM