Java 常用加密解密算法全解
   软件工程   0 评论   2213 浏览

Java 常用加密解密算法全解

   软件工程   0 评论   2213 浏览

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

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

数据编码

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

储备知识Byte和bit

Byte:字节,数据存储的基本单位;

bit:比特,也叫位,一个位只能存储0或者1。

关系:1Byte = 8bit。

一个英文字符占1个字节,8位:

@Test
public void demo1() {
    String a = "a";
    byte[] bytes = a.getBytes();
    for (byte b : bytes) {
        System.out.println(b);
        System.out.println(Integer.toBinaryString(b));
    }
}

程序输出:

97
1100001

一个中文字符在不同编码下所占的字节数不同:

@Test
public void demo2() throws UnsupportedEncodingException {
    String a = "陈";
    byte[] utf8Bytes = a.getBytes("utf-8");
    for (byte b : utf8Bytes) {
        System.out.print(b);
        System.out.print("    ");
        System.out.println(Integer.toBinaryString(b));
    }
    System.out.println();
    byte[] gbkBytes = a.getBytes("gbk");
    for (byte b : gbkBytes) {
        System.out.print(b);
        System.out.print("    ");
        System.out.println(Integer.toBinaryString(b));
    }
}

程序输出:

-23        11111111111111111111111111101001
-103    11111111111111111111111110011001
-120    11111111111111111111111110001000

-77        11111111111111111111111110110011
-62        11111111111111111111111111000010

所以在UTF-8编码下,一个中文占3个字节;在GBK编码下,一个中文占2个字节。

Base64编码原理

Base64编码表:

索引对应字符索引对应字符索引对应字符索引对应字符
0A17R34i51z
1B18S35j520
2C19T36k531
3D20U37l542
4E21V38m553
5F22W39n564
6G23X40o575
7H24Y41p586
8I25Z42q597
9J26a43r608
10K27b44s619
11L28c45t62+
12M29d46u63/
13N30e47v
14O31f48w
15P32g49x
16Q33h50y

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转换为字符数组:h e l l o;
  2. 对应的ASCII码为:104 101 108 108 111;
  3. 转换为8bit二进制数:01101000 01100101 01101100 01101100 01101111
  4. 分组,每组24个bit(不足24个bit的用00000000补齐): 011010000110010101101100 011011000110111100000000;
  5. 每组24bit分为4份,每份6bit:011010 000110 010101 101100 011011 000110 111100 000000;
  6. 在每6个bit前补0,补齐8bit:00011010 00000110 00010101 00101100 00011011 00000110 00111100 00000000;
  7. 将每8bit转换为10进制数:26 6 21 44 27 6 60 0
  8. 从上面Base64编码表中找到十进制数对应的字符(末尾的0并不是A,而是用=等号补位):a G V s b G 8 =

所以hello经过Base64编码的结果为aGVsbG8=

我们可以用代码验证下(JDK8开始已经提供了Base64的实现):

import org.junit.Test;
import java.util.Base64;

public class Base64Test {

    @Test
    public void demo1() {
        System.out.println(Base64.getEncoder().encodeToString("hello".getBytes()));
    }
}

程序输出也是aGVsbG8=

URL Base64算法

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

所以common codec包下的URL Base64算法舍弃了填充符,使用了不定长URL Base64编码。

引入common codec依赖包:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>

举个例子:

import org.apache.commons.codec.binary.Base64;
import org.junit.Test;

public class Base64Test {

    @Test
    public void demo1() {
        String value = "hello";
        System.out.println(Base64.encodeBase64String(value.getBytes()));
        System.out.println(Base64.encodeBase64URLSafeString(value.getBytes()));
    }

}

输出如下:

aGVsbG8=
aGVsbG8

散列算法

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

该算法主要分为三大类:MD(Message Digest,消息摘要算法)、SHA(Secure HashAlgorithm,安全散列算法)和MAC(Message Authentication Code,消息认证码算法)。MD系列算法包括MD2、MD4和MD5共3种算法;SHA算法主要包括其代表算法SHA-1和SHA-1算法的变种SHA-2系列算法(包含SHA-224、SHA-256、SHA-384和SHA-512);MAC算法综合了上述两种算法,主要包括HmacMD5、HmacSHA1、HmacSHA256、HmacSHA384和HmacSHA512算法。这节主要记录下JDK8对这些算法的支持情况。

消息摘要算法的结果我们一般将其转换为16进制字符串,方便阅读传输。

MD系列算法

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

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

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>

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

MD2:

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

import java.security.MessageDigest;

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "hello";
        String algorithm = "md2";
        // 获取MessageDigest实例
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        // 调用digest方法生成数字摘要
        byte[] digest = messageDigest.digest(value.getBytes());
        // 结果转换为16进制字符串(借助common-codec Hex类)
        String md2Hex = Hex.encodeHexString(digest);
        System.out.println(md2Hex);
        System.out.println(md2Hex.length());
    }
}

运行结果:

a9046c73e00331af68917d3804f70655
32

MD5(只需要将算法改为md5即可):

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

import java.security.MessageDigest;

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "hello";
        String algorithm = "md5";
        // 获取MessageDigest实例
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        // 调用digest方法生成数字摘要
        byte[] digest = messageDigest.digest(value.getBytes());
        // 结果转换为16进制字符串(借助common-codec Hex类)
        String md5Hex = Hex.encodeHexString(digest);
        System.out.println(md5Hex);
        System.out.println(md5Hex.length());
    }
}

运行结果:

5d41402abc4b2a76b9719d911017c592
32

common-codec的DigestUtils也提供了MD2和MD5算法相关方法:

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

public class Demo {

    @Test
    public void test() {
        String value = "hello";
        String md5Hex = DigestUtils.md5Hex(value.getBytes());
        System.out.println(md5Hex);

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

common-codec并没有自己实现相关算法,而是对JDK原生API进行封装,使用起来更方便。

SHA系列算法

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

SHA算法家族目前共有SHA-1、SHA-224、SHA-256、SHA-384和SHA-512五种算法,通常将后四种算法并称为SHA-2算法。

JDK8支持SHA-1、SHA-256、SHA-384、SHA-224和SHA-512五种算法,其中SHA的写法等价于SHA-1。JDK8中,SHA系列算法的实现也是通过MessageDigest类来完成的:

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

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

public class Demo {

    @Test
    public void test() throws NoSuchAlgorithmException {
        String value = "hello";
        String algorithm = "SHA-1";
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
        byte[] digest = messageDigest.digest(value.getBytes());
        System.out.println(Hex.encodeHexString(digest));
    }
}

运行结果:

aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

剩下的几种算法可以自己尝试。

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

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

public class Demo {

    @Test
    public void test() {
        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);
    }
}

MAC系列算法

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

MAC算法主要集合了MD和SHA两大系列消息摘要算法。MD系列算法有HmacMD2、HmacMD4和HmacMD5三种算法;SHA系列算法有HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384和HmacSHA512五种算法。

JDK8支持了HmacMD5、HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384和HmacSHA512这六种MAC算法,通过Mac类实现。

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

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

public class Demo {

    @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));
    }
}

输出结果:

PCg3+Q7i/C0ahZ74Vo3Nl/2wBvHnsdycoSmoAXzuxSwc5DVc1rWyHKHdt1XzlanT5GdJiKkcKhCwXGm7iN+udA==
6c40b8d59b9d0f818e94b9028b789892

实际应用

在Tomcat下载页面:https://tomcat.apache.org/download-70.cgi中,我们可以查看相关文件的摘要:

我们将32-bit Windows zip这个文件下载下来,计算出这个文件的sha512值:

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

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.security.MessageDigest;

public class Demo {

    @Test
    public void test() throws Exception {
        String algorithm = "SHA-512";
        MessageDigest messageDigest = MessageDigest.getInstance(algorithm);

        String filePath = "files/apache-tomcat-7.0.105-windows-x86.zip";
        FileInputStream fis = new FileInputStream(filePath);
        int len;
        byte[] buffer = new byte[1024];
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        while ((len = fis.read(buffer)) != -1) {
            stream.write(buffer, 0, len);
        }

        byte[] digest = messageDigest.digest(stream.toByteArray());
        System.out.println(Hex.encodeHexString(digest));
    }
}

结果:

b7a3b0629dad0d9684bc57a5d18251e38bafa172fcffeac06f7e3c40884f2afc099e7c0143a0471639887b8294c8135c35d1f1ac24f1637dee3c2b3a06aa3aa5

这和页面https://downloads.apache.org/tomcat/tomcat-7/v7.0.105/bin/apache-tomcat-7.0.105-windows-x86.zip.sha512上显示的一致,说明我们下载的文件在传输过程中没有被篡改。

对称加密算法

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

DES

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

JDK8仅支持56位长度的DES秘钥,下面举个JDK8实现DES加密的例子:

import org.junit.Test;

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 Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        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));
    }
}

转换模式transformation这里先设置为和加密算法一样,下面介绍了加密模式和填充模式后再作说明。

上面步骤看着挺多,其实可以总结为如下几步:

  1. 生成加密秘钥;

1.1. 通过KeyGenerator生成一个指定位数的秘钥;

1.2. 通过上面生成的秘钥实例化算法对应的秘钥材料KeySpec;

1.3. 使用秘钥材料通过秘钥工厂SecretKeyFactory生成算法秘钥SecretKey。

  1. 通过转换模式实例化Cipher;
  2. 指定Cipher模式和秘钥,进行加解密操作。

运行结果如下:

待加密值:rawchen
DES秘钥:+/T39D2eqLU=
DES加密结果:Ry+BepIFjbQ=
DES解密结果:rawchen

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

DESede

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

JDK8支持112位或168位长度的DESede秘钥,下面举个JDK8实现DESede加密的例子:

import org.junit.Test;

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 Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        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));
    }
}

过程和DES一致,区别仅在于使用的加密算法为DESede,秘钥长度指定为112,秘钥材料对象为DESedeKeySpec类。

上面程序输出如下:

待加密值:rawchen
DESede秘钥:+BOw+/GtyIAsjB+J2tnZRvgTsPvxrciA
DESede加密结果:ytgazq4MzR4=
DESede解密结果:rawchen

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

AES

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

JDK8支持128位、192位和256位长度的AES秘钥,下面举个JDK8实现AES加密的例子:

import org.junit.Test;

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

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        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));
    }
}

过程和上面例子大体一致,唯一区别是使用AES算法无需指定特定类型的秘钥材料,直接用SecretKeySpec对象即可。

程序输出如下:

待加密值:rawchen
AES秘钥:kSiEi1NwlB+dRdzNtI+ACzcZVk5jX5C6jASByxTHBqg=
AES加密结果:l6+wvdlNmasW2PpVjAhWhw==
AES解密结果:rawchen

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

RC2、RC4

RC2和RC4算法也可以用于替换DES算法,特点是秘钥长度较为灵活,RC2和RC4的秘钥长度范围为40到1024位。

JDK8支持RC2和RC4算法:

import org.junit.Test;

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

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "RC2";
        // 转换模式
        String transformation = "RC2";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
        // 初始化秘钥长度
        keyGenerator.init(666);
        // 生成秘钥
        SecretKey secretKey = keyGenerator.generateKey();
        // 生成秘钥材料
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
        System.out.println("RC2秘钥:" + 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("RC2加密结果:" + Base64.getEncoder().encodeToString(encrypt));
        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("RC2解密结果:" + new String(decrypt));
    }
}

程序输出如下:

待加密值:rawchen
RC2秘钥:SFs8RCETIODcILhXOr32PPLlXjux9mpXcnFMPnI1cbkpDLgMa2LHpyo7sF4YCQsN0Ljgq+Gpuvx1eJCDX/opskD4ZODnx5c5f4wHeo4Jt8ZaAxBI
RC2加密结果:Cy24K0Ufrqo=
RC2解密结果:rawchen

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

RC4读者可以自己玩一玩。

Blowfish

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

JDK8支持Blowfish算法:

import org.junit.Test;

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

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        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));
    }
}

程序运行结果:

待加密值:rawchen
Blowfish秘钥:21oBhuAuQ0nxASM/Xqfk5Q==
Blowfish加密结果:C23UISFQaig=
Blowfish解密结果:rawchen

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

PBE

PBE(Password Based Encryption,基于口令加密)算法是一种基于口令的加密算法,特点是没有秘钥的概念,信息交互双方事先拟定好口令即可。单纯的口令很容易通过穷举攻击方式破译,所以PBE也加入了“盐”的概念。

PBE算法是对称加密算法的综合性算法,常见算法如PBEWithMD5AndDES,该算法使用了MD5和DES算法构建PBE算法。JDK8支持PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40、PBEWithSHA1AndRC2_128、PBEWithSHA1AndRC4_40、PBEWithSHA1AndRC4_128、PBEWithHmacSHA1AndAES_128、PBEWithHmacSHA224AndAES_128、PBEWithHmacSHA256AndAES_128、PBEWithHmacSHA384AndAES_128、PBEWithHmacSHA512AndAES_128、PBEWithHmacSHA1AndAES_256、PBEWithHmacSHA224AndAES_256、PBEWithHmacSHA256AndAES_256、PBEWithHmacSHA384AndAES_256和PBEWithHmacSHA512AndAES_256,其中算法名称中包含Hmac的需要配和初始化向量使用。

不需要指定初始化向量的PBE算法族(PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40、PBEWithSHA1AndRC2_128、PBEWithSHA1AndRC4_40、PBEWithSHA1AndRC4_128)例子:

import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "PBEWithSHA1AndDESede";
        // 转换模式
        String transformation = "PBEWithSHA1AndDESede";
        // 密码(口令)
        String password = "rawchen@qq.com";
        System.out.println("PBE口令:" + password);
        // 迭代次数
        int count = 99;

        // 实例化安全随机数
        SecureRandom secureRandom = new SecureRandom();
        // 生成盐
        byte[] salt = secureRandom.generateSeed(8);
        System.out.println("盐值:" + Base64.getEncoder().encodeToString(salt));
        // 通过密码生成秘钥材料
        PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
        // 实例化秘钥工厂
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
        // 生成秘钥
        SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
        // 实例化PBE参数材料
        PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, count);

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 初始化
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println("PBE加密结果:" + Base64.getEncoder().encodeToString(encrypt));

        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("PBE解密结果:" + new String(decrypt));
    }
}

程序输出如下:

待加密值:rawchen
PBE口令:rawchen@qq.com
盐值:H7SLVckG2Fs=
PBE加密结果:Gfvt/blSQgg=
PBE解密结果:rawchen

算法名称包含Hmac的PBE算法需要指定初始化向量,比如PBEWithHmacSHA1AndAES_128:

import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "PBEWithHmacSHA1AndAES_128";
        // 转换模式
        String transformation = "PBEWithHmacSHA1AndAES_128";
        // 密码(口令)
        String password = "rawchen@qq.com";
        System.out.println("PBE口令:" + password);
        // 迭代次数
        int count = 99;

        // 实例化安全随机数
        SecureRandom secureRandom = new SecureRandom();
        // 生成盐
        byte[] salt = secureRandom.generateSeed(8);
        System.out.println("盐值:" + Base64.getEncoder().encodeToString(salt));
        // 通过密码生成秘钥材料
        PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
        // 实例化秘钥工厂
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
        // 生成秘钥
        SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
        // 创建初始化向量
        IvParameterSpec iv = new IvParameterSpec("123456789abcdefg".getBytes());
        // 实例化PBE参数材料
        PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, count, iv);

        // 实例化密码对象
        Cipher cipher = Cipher.getInstance(transformation);
        // 初始化
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
        // 加密
        byte[] encrypt = cipher.doFinal(value.getBytes());
        System.out.println("PBE加密结果:" + Base64.getEncoder().encodeToString(encrypt));

        // 解密
        // 设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
        byte[] decrypt = cipher.doFinal(encrypt);
        System.out.println("PBE解密结果:" + new String(decrypt));
    }
}

通过IvParameterSpec类创建初始化向量,创建初始化向量的秘钥必须为16字节,这里为123456789abcdefg,实例化PBE参数材料的时候通过构造参数传入初始化向量。

程序输出如下:

待加密值:rawchen
PBE口令:rawchen@qq.com
盐值:yLqBCWhPON4=
PBE加密结果:zi7jLKRJ6B5/sUUTdha1Og==
PBE解密结果:rawchen

加密模式

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

上面介绍的这些都属于块密码加密。不同的算法侧重点不同,有的强调效率,有的强调安全,有的强调容错性。根据数据加密时每个加密区块间的关联方式来区分,可以分为4种加密模式:电子密码本模式(Electronic Code Book,ECB)、密文链接模式(Cipher Book Chaining,CBC)、密文反馈模式(Cipher Feed Back,CFB)、输出反馈模式(Output Feed Back,OFB)。AES标准除了推荐上述4种工作模式外,还推荐了一种新的工作模式—计数器模式(Counter,CTR)。这些工作模式可适用于各种分组密码算法。

ECB

ECB模式加解密过程如下图所示:

明文分为若干块,每次加密均产生独立的密文分组,每组的加密结果不会对其他分组产生影响,相同的明文加密后对应产生相同的密文。

CBC

CBC模式加解密过程如下图所示:

明文分为若干块,每次加密前,明文块都和前一个明文块加密后的内容进行异或处理,然后再用秘钥加密。因为第一个明文块没有可以用来异或处理的密文块,所以我们需要提供一个初始化向量来替代。

CFB

CFB模式加解密过程如下图所示:

明文分为若干块,每次加密前,先将前一个密文块使用秘钥加密,加密结果和当前明文块异或处理得到密文块。同样的,需要为第一个明文块加密提供初始化向量。

OFB

OFB模式加解密过程如下图所示:

过程和CFB类似,区别在于OFB第一次使用秘钥对初始化向量进行加密(结果为A),加密结果和明文块异或处理得到密文块,下一次操作时候,不是使用秘钥加密前一个密文块,而是使用秘钥加密A的结果再和明文块异或处理,得到当前密文块。

CTR

CTR模式加解密过程如下图所示:

CTR含义是计数器模式,所以它维护了一个递增的计数器。秘钥加密计数器,结果和明文块异或得到密文块,依次类推。

填充模式

当需要按块处理的数据, 数据长度不符合块处理需求时, 按照一定的方法填充满块长的规则。如果不填充,待加密的数据块长度不符合要求时程序会抛出异常。

JDK8中主要支持NoPadding和PKCS5Padding填充模式。

  1. NoPadding:不填充;
  2. PKCS5Padding:数据块的大小为8位, 不够就补足。

加密、填充模式实战

在了解了加密模式和填充模式后,我们回头看前面代码中的transformation参数,实例化Cipher对象的时候需要指定transformation转换模式,转换模式主要有两种格式:

  1. 算法;
  2. 算法/加密模式/填充模式。

下面就AES算法来实践不同的加密、填充模式。

当转换模式为AES/ECB/PKCS5Padding时:

import org.junit.Test;

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

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "AES";
        // 转换模式
        String transformation = "AES/ECB/PKCS5Padding";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        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));
    }
}

程序运行解果:

待加密值:rawchen
AES秘钥:YZAD6zhczY0bpRTZ8ndzM+3kiN8rtNaBWpgNuwTJFuo=
AES加密结果:/st75df6cEvzOYxM5q2XMA==
AES解密结果:rawchen

将转换模式改为AES/CBC/PKCS5Padding时,程序输出抛出java.security.InvalidKeyException: Parameters missing异常。

因为该模式需要指定初始化向量,将代码修改为:

import org.junit.Test;

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

public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "AES";
        // 转换模式
        String transformation = "AES/CBC/PKCS5Padding";
        // --- 生成秘钥 ---
        // 实例化秘钥生成器
        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()));

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

程序输出如下:

待加密值:rawchen
AES秘钥:+hAO/51KV9FAgMHMEPwc+RpUlpEfBjyzJ699LRYFLf8=
AES加密结果:/pnOn0QjFc8QYjZTuuAKTg==
AES解密结果:rawchen

将转换模式改为AES/CBC/NoPadding时,程序抛出javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes异常。因为rawchen不是16字节的倍数。

更多对称加密算法和加密模式填充模式的组合可以自己尝试下,或者参考下面的算法总结。

手动指定秘钥

在使用对称加密算法加解密的时候,秘钥一般是双方事先约定好的,假如现在通过转换模式为AES/CTR/PKCS5Padding加密得到AES秘钥xaBsoZIBs1Dz6veBfwfzpPwzrmMsu8mKqu4Lljk+zZo=,AES密文VKOtjAOQJQ==,初始化向量秘钥为123456789abcdefg,如何通过秘钥和密文进行解密呢,可以参考下面的代码(即演示如何手动指定秘钥并解密):

import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

    @Test
    public void test() throws Exception {
        String algorithm = "AES";
        String transformation = "AES/CTR/PKCS5Padding";
        String key = "xaBsoZIBs1Dz6veBfwfzpPwzrmMsu8mKqu4Lljk+zZo=";
        String encrypt = "VKOtjAOQJQ==";
        String ivKey = "123456789abcdefg";

        Cipher cipher = Cipher.getInstance(transformation);
        SecretKeySpec secretKeySpec = new SecretKeySpec(Base64.getDecoder().decode(key), algorithm);
        IvParameterSpec iv = new IvParameterSpec(ivKey.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
        byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(encrypt));
        System.out.println("AES解密结果:" + new String(decrypt));
    }
}

输出如下:

AES解密结果:rawchen

算法总结

对上面这些算法进行总结:

算法秘钥长度(位)工作模式填充模式初始化向量秘钥长度(字节)
DES56ECB、CBC、CFB、OFB、CTR等NoPadding、 PKCS5Padding、 ISO10126Padding8
DESede112、168ECB、CBC、CFB、OFB、CTR等NoPadding、 PKCS5Padding、 ISO10126Padding8
AES128、192、256ECB、CBC、CFB、OFB、CTR等NoPadding、 PKCS5Padding、 ISO10126Padding16
RC240~1024ECB、CBC、CFB、OFB、CTR等NoPadding、 PKCS5Padding、 ISO10126Padding8
RC440~1024ECBNoPadding
Blowfish32~448,8的倍数ECB、CBC、CFB、OFB、CTR等NoPadding、 PKCS5Padding、 ISO10126Padding8
PBECBCPKCS5Padding16(带Hmac)

非对称加密算法

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

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算法:

算法秘钥长度加密模式填充模式
RSA512~16384位,64倍数ECBNoPadding PKCS1Padding OAEPWithMD5AndMGF1Padding OAEPWithSHA1AndMGF1Padding OAEPWithSHA-1AndMGF1Padding OAEPWithSHA-224AndMGF1Padding OAEPWithSHA-256AndMGF1Padding OAEPWithSHA-384AndMGF1Padding OAEPWithSHA-512AndMGF1Padding OAEPWithSHA-512/224AndMGF1Padding OAEPWithSHA-512/2256ndMGF1Padding

代码例子:

import org.junit.Test;

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 Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        // 加密算法
        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));
    }
}

程序输出如下:

RSA公钥: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALSAyeMI6Xu/87Lz4V+zdqh002OOxskYvtUoby9UYxTC/0EJyREEzpty3N72bX8VWIWdurCvDpoAvx+Mkos40P0CAwEAAQ==
RSA私钥: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtIDJ4wjpe7/zsvPhX7N2qHTTY47GyRi+1ShvL1RjFML/QQnJEQTOm3Lc3vZtfxVYhZ26sK8OmgC/H4ySizjQ/QIDAQABAkAwb4a9J30Pufh5ArxtY8jpdz/qLTvmZn9+z3TWbHyR6zWEeWDs5aNzpKdcbUYBIvLty3O+ZElEXSwysQ6bYBG5AiEA7bA/3VZY/yictNd1GJs+i8Mjbmy52cL0bbqDeXf6bq8CIQDCaLb57s5kjVuhNyfbmEYwduvSBIwFseRk9he5Mj2GEwIhAJEZr2Mne10JJeEgRtOmsiAQGGko5qwRX7Y8zlYw8CjxAiAY7TpA05jNFb7g7eSDaIPfZPAZrpGRjVyegVtLWKDA1wIhANLmqDjYrOYZZ4WgSZwUoIQxqWeGLj12i3ot66VQEW01
RSA公钥加密后数据: rrVx6AYgzMOnF1mpm48yGCu3L6m2nYKE2HZBET89xN8NBnhNRnN/KXCsIAmwuJu4oegVpTFYXldgPHgfpN6ASQ==
RSA私钥解密后数据: rawchen
RSA私钥加密后数据: kxaYRfSI8eQROz/hgE24sUNY+QmA32Byh6N1Uh6r2T3b2AuAFFWTHvxSu1IfySCD4VqvsV+r8QyY+fHs68ABiQ==
RSA公钥解密后数据: rawchen

可以看到,公钥加密私钥解密和私钥加密公钥解密的模式都可行。

公私钥获取

假如现在有RSA公钥:

MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALSAyeMI6Xu/87Lz4V+zdqh002OOxskYvtUoby9UYxTC/0EJyREEzpty3N72bX8VWIWdurCvDpoAvx+Mkos40P0CAwEAAQ==

RSA私钥:

MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtIDJ4wjpe7/zsvPhX7N2qHTTY47GyRi+1ShvL1RjFML/QQnJEQTOm3Lc3vZtfxVYhZ26sK8OmgC/H4ySizjQ/QIDAQABAkAwb4a9J30Pufh5ArxtY8jpdz/qLTvmZn9+z3TWbHyR6zWEeWDs5aNzpKdcbUYBIvLty3O+ZElEXSwysQ6bYBG5AiEA7bA/3VZY/yictNd1GJs+i8Mjbmy52cL0bbqDeXf6bq8CIQDCaLb57s5kjVuhNyfbmEYwduvSBIwFseRk9he5Mj2GEwIhAJEZr2Mne10JJeEgRtOmsiAQGGko5qwRX7Y8zlYw8CjxAiAY7TpA05jNFb7g7eSDaIPfZPAZrpGRjVyegVtLWKDA1wIhANLmqDjYrOYZZ4WgSZwUoIQxqWeGLj12i3ot66VQEW01

需要将它们还原为PublicKey和PrivateKey对象,可以参考如下代码:

import org.junit.Test;

import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;


public class Demo {

    @Test
    public void test() throws Exception {
        String value = "rawchen";
        // 加密算法
        String algorithm = "RSA";
        // 转换模式
        String transform = "RSA/ECB/PKCS1Padding";
        // RSA公钥BASE64字符串
        String rsaPublicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALSAyeMI6Xu/87Lz4V+zdqh002OOxskYvtUoby9UYxTC/0EJyREEzpty3N72bX8VWIWdurCvDpoAvx+Mkos40P0CAwEAAQ==";
        // RSA私钥BASE64字符串
        String rsaPrivateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtIDJ4wjpe7/zsvPhX7N2qHTTY47GyRi+1ShvL1RjFML/QQnJEQTOm3Lc3vZtfxVYhZ26sK8OmgC/H4ySizjQ/QIDAQABAkAwb4a9J30Pufh5ArxtY8jpdz/qLTvmZn9+z3TWbHyR6zWEeWDs5aNzpKdcbUYBIvLty3O+ZElEXSwysQ6bYBG5AiEA7bA/3VZY/yictNd1GJs+i8Mjbmy52cL0bbqDeXf6bq8CIQDCaLb57s5kjVuhNyfbmEYwduvSBIwFseRk9he5Mj2GEwIhAJEZr2Mne10JJeEgRtOmsiAQGGko5qwRX7Y8zlYw8CjxAiAY7TpA05jNFb7g7eSDaIPfZPAZrpGRjVyegVtLWKDA1wIhANLmqDjYrOYZZ4WgSZwUoIQxqWeGLj12i3ot66VQEW01";

        // ------- 还原公钥 --------
        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));
    }
}

程序输出如下:

RSA公钥加密数据: TVP7qWjF4XL5yvfJ63bCKbZpK0uGZyi20gLs5fNXJQTLPQzLFFmWWGJ5xzsSYhzVbWmY4vRAxf1aYSsD6AK4rA==
RSA私钥解密数据: rawchen

分段加解密

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

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

以秘钥长度为1024bits为例:

import org.junit.Test;

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 Demo {

    @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";
        // 转换模式
        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));
    }
}

程序会抛出如下异常:

待加密内容长度: 120
RSA公钥: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC79DkQcppMMl11r21OMYcTlLWMJP9ZZw9BdszZPu+D1kHijbETrae84AwOrNPqrl8/vpPh2q9BLkrkfQuvSLQHk6tuefVEyWRnnnEwYJzIbjuQPhEwKU7khqjhNXdoW/27AN7kyQwFFnbLHfkc/lh6V6N6S2g5J2NmQL4hfqVgGwIDAQAB
RSA私钥: MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALv0ORBymkwyXXWvbU4xhxOUtYwk/1lnD0F2zNk+74PWQeKNsROtp7zgDA6s0+quXz++k+Har0EuSuR9C69ItAeTq2559UTJZGeecTBgnMhuO5A+ETApTuSGqOE1d2hb/bsA3uTJDAUWdssd+Rz+WHpXo3pLaDknY2ZAviF+pWAbAgMBAAECgYA2ksoC6ZO9rh4O7rnpK15SJCq2n4N5HQCD/I+sQKbg+9QziPqygQikQdWeaTY6/Rhw9NARkyKx5VQfleNPqOeEj1KwNK8pctD7nkb/PL/LZofH1uk1J0sgaSPpox2LUrIabWFs/dztbHpR/aiaQLbLfrdOgkGoQWM3FB8hMEsbSQJBAPhD/US0C6VQuMqLytuOsB7imqNttD8F6gGQAXAGz2YcgDHSHdayzhT1q12J0nrzfJGfZLZpc+4t9szS9Oh3VvcCQQDBzznlqTbaq7KaxAacdo6BRQszVMuy9kJXupINUUyw+wEaCiz4sxCJsa8ASfJBnxRGFEPyi9Hea6ijOwckDYL9AkEAkAPYqn8K9mYCHDTFg2GdVv06mS0tTxXeLfPccaDxtJk54Cyz9HSayVvNgaBOgdY2376nzI0VnAf7z8tcGHIJ9wJAcPwb5pU1U1mRL8RjjkdXYGkd1Hj0n4oMtxQfHQBuUyahR8ry2LGbTIp3WRXC0xqoOQqLahS07pOYpkA9M3llCQJBAN4oaSLXsSpZtnwekocGapsBaY62Kn9QIZGaGHJkmAwXBXEdXZfr/16BhUd4JSlfGgL3CvdP57OaWjl0CPZ0wxs=

javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes

    at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:344)
    at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:389)
    at javax.crypto.Cipher.doFinal(Cipher.java:2164)
    at cc.mrbird.security.temp.rsa.Demo.test(Demo.java:42)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

对于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;
            }
            byte[] decryptedData = out.toByteArray();
            return new String(decryptedData);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

测试:

import org.junit.Test;

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


public class Demo {

    @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);

    }
}

程序输出如下:

待加密内容长度: 120
RSA公钥: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJcyiFAbUHBYXe5BTY/TF5Bn4/fW4L0dK2eaDSPJr4uqTFxIj+sRDqRq71yZw3KJk0qxmmGbtMRQGuR+GVAyJ/0E2R3q2RM+aWCZmkzyDnq6xHIvV0d3mU3N8EDtPS6iO+ANOEPNKfdzr+BJN8NKnpXC2ii7phvMk/QlYqjVAbIwIDAQAB
RSA私钥: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIlzKIUBtQcFhd7kFNj9MXkGfj99bgvR0rZ5oNI8mvi6pMXEiP6xEOpGrvXJnDcomTSrGaYZu0xFAa5H4ZUDIn/QTZHerZEz5pYJmaTPIOerrEci9XR3eZTc3wQO09LqI74A04Q80p93Ov4Ek3w0qelcLaKLumG8yT9CViqNUBsjAgMBAAECgYEAhaU2UdVuGoyxNR9acf4GW6IHoV4pYU68bnbm+2S4Xn7EdhN6DQNH6jOeLRjCTxOnnAF95/Z/GlLCpp335nbs1B1wlb6MP5l3keO2KhYuvPhnZdPInNV0aKWzOoX7gcmy12g5IAQgoYoc/IohPIpMmkgrbuGQGk2+jxnGPETgNdECQQDhDyP3VIH5AucbHHeL6fSU9kswO1eejLzRmvlKIwk15wE1xteFvIqpLOLlR8wJm+Eb5uB0HEr4X7rlWDQLq/OZAkEAnFilYybZfp4rRctsnYjFZf0QGUCCBV9hFa7xoGztV2rAkLLmsnayXzSJpOYYAOI7ekrqfLL3xALQKn8DZtr6GwJAecd9iKl7oshFUVA4B8dShwA2cyTJJou06B5ZYhpPM5GKABVWLZF13lDhfXs6FsD4L+bf8TQWBQuXz93IW8BxkQJAZqfR2BuPHRMPiKE77Of77K5PnrT7ajmpDkqy/knnQMmoLJo63Z0QG3Dsm6g0xIfG09JSypPcGQhb1DtXaXaIVwJAVVSPa1caRWLKYlEKAi1gBbrC5Zt7aTQ/ska2E3ksAhaVhScPBOEIoQf9EdbGajmpuueWeH9IlVrqQv0vFNY4gA==
RSA公钥加密后数据: cy7Var2L72bgne9F8iGro+SCQxs2ejIMPwQDJ5hQFTLvyqtT4ZJYM5i2ClgOD9viAP2Tp/X5cCX0+K1xz88hf5w/xNPWonzdaJNa2J5gQv7KGxNe/pW4mtpf878u4sIvO9sT8AktWtJC3jFtvxL9u9vJdzWl99RSRf/3sNqWj3gLRM/YpCcGM0HPuDsyUdOA4q+Tn+d2nOf36XrBtjIl2QyOTwnMoMCbC9Hlt6jN8fMsSFW8oiFNqV/+HPlhs5ZtFixhUE6SryketJfzXGmUSXH5cM/+11pB2bBxrCvtqRUE5/MZKjL2kKmrZan3kDHi4aiwLSDdpYdZn0urrJObAA==
RSA私钥解密后数据: 18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm18cm

建议

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

本文由 RawChen 发表, 最后编辑时间为:2021-05-19 01:02
如果你觉得我的文章不错,不妨鼓励我继续写作。

发表评论
选择表情
Top