Java 中的加密算法: AES

叉叉哥
English

加密算法

加密算法通常分为对称加密算法和非对称加密算法:

  • 对称加密算法(symmetric-key cryptography):加密和解密时使用相同的密钥。常用的对称加密算法有 DES、AES。
  • 非对称加密算法(asymmetric-key cryptography):加密和解密使用不同的密钥,例如公钥加密的内容只能用私钥解密,所以又称为公钥加密算法(public-key cryptography)。使用最广泛的非对称加密算法是 RSA 算法。

两者有不同的使用场景,而且经常会一起搭配起来使用,例如 SSL/TLS 协议就结合了对称加密算法和非对称加密算法。

本文主要介绍最常用的对称加密算法:AES。

AES

AES 全称 Advanced Encryption Standard,是一种对称加密算法。AES 的出现主要是用来取代 DES 加密算法,因为 AES 的安全性相对更高。

AES 使用非常广泛,可以说只要上网,无论是使用手机 APP 还是 Web 应用,几乎都离不开 AES 加密算法。因为目前大部分网站,包括手机 APP 后端接口,都已经使用 HTTPS 协议,而 HTTPS 在数据传输阶段大部分都是使用 AES 对称加密算法。

在学习 AES 之前,首先要知道以下规则:

  • AES 是一种区块加密算法,加密时会将原始数据按大小拆分成一个个区块进行加密,区块大小固定为 128 比特(即 16 字节)
  • AES 密钥长度可以是 128、192 或 256 比特(即 16、25 或 32 字节),密钥长度越长,安全性越高,而性能也就越低

AES 工作模式

AES加密算法有多种工作模式(mode of operation),如:ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM。不同的模式参数和加密流程不同,但是核心仍然是 AES 算法。

本文主要介绍 ECB、CBC、GCM 三种模式。

AES 填充方式

由于 AES 是一种区块加密算法,加密时会将原始数据按大小拆分成一个个 128 比特(即 16 字节)区块进行加密,如果需要加密的原始数据不是 16 字节的整数倍时,就需要对原始数据进行填充,使其达到 16 字节的整数倍。

常用的填充方式有 PKCS5Padding、ISO10126Padding 等,另外如果能保证待加密的原始数据大小为 16 字节的整数倍,也可以选择不填充,即 NoPadding。

Java 中的 AES

Java 中的 javax.crypto.Cipher 类提供加密和解密的功能。

创建一个 Cipher

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

CiphergetInstance 方法需传递一个加密算法的名称作为参数,用来创建对应的 Cipher,其格式为 algorithm/mode/padding,即 算法名称/工作模式/填充方式,例如 AES/CBC/PKCS5Padding。具体有哪些可选的加密方式,可以参考文档:

https://docs.oracle.com/javas...

ECB

ECB 全称为电子密码本(Electronic codebook),将待加密的数据拆分成块,并对每个块进行独立加密。

ECB 加密
ECB 解密

代码:

public static byte[] encryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static byte[] decryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
    String data = "Hello World"; // 待加密的明文
    String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节

    byte[] ciphertext = encryptECB(data.getBytes(), key.getBytes());
    System.out.println("ECB 模式加密结果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

    byte[] plaintext = decryptECB(ciphertext, key.getBytes());
    System.out.println("解密结果:" + new String(plaintext));
}

由于加密后的密文是二进制格式而非字符串,所以这里使用了 Base64 编码方式将其转换成字符串方便输出查看。输出:

ECB 模式加密结果(Base64):bB0gie8pCE2RBQoIAAIxeA==
解密结果:Hello World

需要注意,AES 密钥长度只能是 16、25 或 32 字节,如果不符合要求则会异常:

java.security.InvalidKeyException: Invalid AES key length

CBC 模式有一个致命的缺点,由于该模式对每个块进行独立加密,会导致同样的明文块被加密成相同的密文块,相对来说并不是非常安全。下图就是一个很好的例子:

ECB 模式的问题

CBC

CBC 全称为密码分组链接(Cipher-block chaining),它的出现解决 ECB 同样的明文块会被加密成相同的密文块的问题。

CBC 引入了初始向量的概念(IV,Initialization Vector),第一个明文块先与 IV 进行异或后再加密,后续每个明文块先与前一个密文块进行异或后再加密。

CBC 加密
CBC 解密

代码:

public static byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static byte[] decryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
    byte[] result = cipher.doFinal(data);
    return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
    String data = "Hello World"; // 待加密的原文
    String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节
    String iv = "iviviviviviviviv"; // CBC 模式需要用到初始向量参数

    byte[] ciphertext = encryptCBC(data.getBytes(), key.getBytes(), iv.getBytes());
    System.out.println("CBC 模式加密结果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

    byte[] plaintext = decryptCBC(ciphertext, key.getBytes(), iv.getBytes());
    System.out.println("解密结果:" + new String(plaintext));
}

输出:

CBC 模式加密结果(Base64):K7bSB51+KxfqaMjJOsPAQg==
解密结果:Hello World

由于 CBC 每个明文块加密都依赖前一个块的加密结果,所以其主要缺点在于加密过程是串行的,无法被并行化。

GCM

GCM 的全称是 Galois/Counter Mode,它是一种认证加密(authenticated encryption)算法。它不但提供了加密解密,还提供了数据完整性校验,防止篡改。

AES-GCM 模式是目前使用最广泛的模式,可以尝试抓包看一下目前主流的 https 网站,其中大部分都是基于 GCM 模式。下图是使用抓包工具 Charles 查看浏览器访问 https 网站所使用的加密算法:

抓包

可以看到浏览器一般支持 AES-GCM 和 AES-CBC 模式,最终服务器选择使用 AES-GCM。

AES-GCM 认证加密需要用到以下参数:

  • 待加密的明文
  • 密钥
  • 初始向量 IV
  • additional authenticated data (AAD)

代码:

public static byte[] encryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
    cipher.updateAAD(aad);
    byte[] result = cipher.doFinal(data);
    return result;
}

public static byte[] decryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
    cipher.updateAAD(aad);
    byte[] result = cipher.doFinal(data);
    return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
    String data = "Hello World"; // 待加密的原文
    String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节
    String iv = "iviviviviviviviv";
    String aad = "aad"; // AAD 长度无限制,可为空

    byte[] ciphertext = encryptGCM(data.getBytes(), key.getBytes(), iv.getBytes(), aad.getBytes());
    System.out.println("GCM 模式加密结果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

    byte[] plaintext = decryptGCM(ciphertext, key.getBytes(), iv.getBytes(), aad.getBytes());
    System.out.println("解密结果:" + new String(plaintext));
}

输出:

GCM 模式加密结果(Base64):1UxXmFpdUwMnpI7rh0XfmFqtdZSHTbNC/08g
解密结果:Hello World

AES-GCM 是流加密(Stream cipher)算法,所以对应的填充模式为 NoPadding,即无需填充。

参考文档

关注我的公众号

扫码关注

阅读 1.1k

Java论道
定期分享Java技术文章,囊括Spring全家桶、Linux、架构设计、分布式、微服务、消息中间件、面试题等。微...
3.7k 声望
52 粉丝
0 条评论
你知道吗?

3.7k 声望
52 粉丝
宣传栏