1. 概述

加密一般分为对称加密(symmetric-key encryption)和非对称加密(asymmetric-key encryption)。对称加密又分为分组加密和序列密码。

对称分组/分块加密(block cipher)在数据加密中起着重要作用,它使得可以使用同一密钥进行加密和解密。 高级加密标准 (AES)是一种广泛使用的对称分组加密算法。

此教程中, 我们将了解如何使用JDK中的Java密码体系结构(JCA)来实现AES加密和解密。

2. AES 算法

AES算法是一种迭代的对称分组加密,支持用128,192或256位(bit)的密钥,加密和解密长度为128位的区块。下图展示了AES算法:

High Level AES Algorithm

如果要加密的数据不满足128位区块大小要求,则必须对其进行填充。

3. AES 加密模式

AES 算法有6种加密模式:

  1. ECB (Electronic Code Book, 电话本)模式
  2. CBC (Cipher Block Chaining, 密码分组链接)模式
  3. CFB (Cipher FeedBack, 加密反馈)模式
  4. OFB (Output FeedBack, 输出反馈)模式
  5. CTR (Counter, 计数)模式
  6. GCM (Galois/Counter Mode)模式

应用不同的加密模式可以增强加密算法的效果 。此外,加密模式可以将分组密码转换为流密码,每种模式都有其优势和劣势, 让我们快速回顾一下:

3.1. ECB

ECB模式是最早采用和最简单的模式。将明文分为大小为128位的块,然后每个块都使用相同的密钥和算法进行加密。 因此,对于相同的块它会得到相同的结果。 这是此模式的主要缺点,不建议用于加密。 它需要填充数据。

3.2. CBC

为了克服ECB的弱点,CBC模式使用初始化向量(IV)来增强加密。 首先,CBC将明文块与IV进行异或(xor)后,将结果加密到密文块。 在下一个块中,它将使用上一个加密结果与明文块进行异或,重复该步骤直到最后一个块。

此模式下, 加密不支持并行计算, 但解密支持并行计算。它同样需要填充数据。

3.3. CFB

This mode can be used as a stream cipher. First, it encrypts the IV, then it will xor with the plaintext block to get ciphertext. Then CFB encrypts the encryption result to xor the plaintext. It needs an IV.

In this mode, decryption can be parallelized but encryption can not be parallelized.

3.4. OFB

This mode can be used as a stream cipher. First, it encrypts the IV. Then it uses the encryption results to xor the plaintext to get ciphertext.

It doesn’t require padding data and will not be affected by the noisy block.

3.5. CTR

This mode uses the value of a counter as an IV. It's very similar to OFB, but it uses the counter to be encrypted every time instead of the IV.

This mode has two strengths, including encryption/decryption parallelization, and noise in one block does not affect other blocks.

3.6. GCM

This mode is an extension of the CTR mode. The GCM has received significant attention and is recommended by NIST. The GCM model outputs ciphertext and an authentication tag. The main advantage of this mode, compared to other operation modes of the algorithm, is its efficiency.

In this tutorial, we'll use the AES/CBC/PKCS5Padding algorithm because it is widely used in many projects.

3.7. 加密后的数据大小

如前所述,AES的块大小为128位或16个字节。 AES不会更改大小,并且密文大小或等于明文大小。 另外,在ECB和CBC模式下,我们应该使用类似于PKCS 5的填充算法。因此,加密后的数据大小为:

ciphertext_size (bytes) = cleartext_size + (16 - (cleartext_size % 16))

为了将IV与密文存储在一起,我们需要再增加16个字节。

4. AES 参数

在AES算法中,我们需要三个参数:输入数据(待加密和解密的数据),密钥和IV。 ECB模式下不使用IV。

4.1. 输入数据

输入的数据可以是字符串,文件,对象等。

4.2. 密钥

在AES中生成密钥的方法有两种:从随机数生成或从给定密码生成。 第一种方法中,应该使用像SecureRandom类这样的加密安全(伪)随机数生成器生成密钥。

为了生成密钥,我们可以使用KeyGenerator类。 下面我们定义一种生成大小为n(128、192、256)位的AES密钥的方法:

public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(n);
    SecretKey key = keyGenerator.generateKey();
    return key;
}

在第二种方法中,可以使用基于密码的密钥派生算法(如PBKDF2)从给定的密码导出AES密钥。我们还需要加盐(salt)将密码转换为密钥。 salt也是一个随机值。

我们可以将_SecretKeyFactory_类与_PBKDF2WithHmacSHA256_算法一起使用,以根据给定的密码生成密钥。

下面定义一个方法,该方法将给定的密码通过65,536次迭代生成长度位256位的AES密钥:

public static SecretKey getKeyFromPassword(String password, String salt)
    throws NoSuchAlgorithmException, InvalidKeySpecException {
    
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
    SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
        .getEncoded(), "AES");
    return secret;
}

4.3. 初始化向量 (IV)

IV的值是随机的,其大小与加密块相同。 我们可以使用SecureRandom类生成随机IV。

下面定义生成 IV的方法:

public static IvParameterSpec generateIv() {
    byte[] iv = new byte[16];
    new SecureRandom().nextBytes(iv);
    return new IvParameterSpec(iv);
}

5. 加密与解密

5.1. 字符串类型

要实现对字符串加密,根据上一节的内容我们首先要生成密钥和IV。 下一步,我们使用_getInstance()_方法从Cipher类创建一个实例。

此外,我们需要调用_init()_方法,传入密钥,IV和加密模式这3个参数,以设置cipher实例。 最后,我们通过调用_doFinal()_方法对输入字符串进行加密。

public static String encrypt(String algorithm, String input, SecretKey key,
    IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
    InvalidAlgorithmParameterException, InvalidKeyException,
    BadPaddingException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    byte[] cipherText = cipher.doFinal(input.getBytes());
    return Base64.getEncoder()
        .encodeToString(cipherText);
}

为了解密输入的字符串,在初始化cipher对象时,传入_DECRYPT_MODE_参数:

public static String decrypt(String algorithm, String cipherText, SecretKey key,
    IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
    InvalidAlgorithmParameterException, InvalidKeyException,
    BadPaddingException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    byte[] plainText = cipher.doFinal(Base64.getDecoder()
        .decode(cipherText));
    return new String(plainText);
}

下面编写测试代码,验证结果:

@Test
void givenString_whenEncrypt_thenSuccess()
    throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
    BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException { 
    
    String input = "baeldung";
    SecretKey key = AESUtil.generateKey(128);
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    String algorithm = "AES/CBC/PKCS5Padding";
    String cipherText = AESUtil.encrypt(algorithm, input, key, ivParameterSpec);
    String plainText = AESUtil.decrypt(algorithm, cipherText, key, ivParameterSpec);
    Assertions.assertEquals(input, plainText);
}

5.2. 文件类型

现在,让我们使用AES算法对文件进行加密。 步骤相同,但是我们需要一些IO类来处理文件:

public static void encryptFile(String algorithm, SecretKey key, IvParameterSpec iv,
    File inputFile, File outputFile) throws IOException, NoSuchPaddingException,
    NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
    BadPaddingException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    FileInputStream inputStream = new FileInputStream(inputFile);
    FileOutputStream outputStream = new FileOutputStream(outputFile);
    byte[] buffer = new byte[64];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        byte[] output = cipher.update(buffer, 0, bytesRead);
        if (output != null) {
            outputStream.write(output);
        }
    }
    byte[] outputBytes = cipher.doFinal();
    if (outputBytes != null) {
        outputStream.write(outputBytes);
    }
    inputStream.close();
    outputStream.close();
}

需要注意的是,不建议直接将整个文件一次性导入内存(尤其当文件特别大时)。 而是,我们一次加密一个缓冲区。

如何解密文件,步骤同上。

同样,让我们编写验证加密和解密文件的测试代码。 我们将从测试资源目录中读取baeldung.txt文件,将其加密为一个名为baeldung.encrypted的文件,然后将该文件解密为一个新文件:

@Test
void givenFile_whenEncrypt_thenSuccess() 
    throws NoSuchAlgorithmException, IOException, IllegalBlockSizeException, 
    InvalidKeyException, BadPaddingException, InvalidAlgorithmParameterException, 
    NoSuchPaddingException {
    
    SecretKey key = AESUtil.generateKey(128);
    String algorithm = "AES/CBC/PKCS5Padding";
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    Resource resource = new ClassPathResource("inputFile/baeldung.txt");
    File inputFile = resource.getFile();
    File encryptedFile = new File("classpath:baeldung.encrypted");
    File decryptedFile = new File("document.decrypted");
    AESUtil.encryptFile(algorithm, key, ivParameterSpec, inputFile, encryptedFile);
    AESUtil.decryptFile(
      algorithm, key, ivParameterSpec, encryptedFile, decryptedFile);
    assertThat(inputFile).hasSameTextualContentAs(decryptedFile);
}

5.3. 基于密码

我们可以使用从给定密码导出的密钥进行AES加密和解密。

为了生成密钥,我们使用_getKeyFromPassword()_方法。 加密和解密步骤同“5.1字符串类型”所讲的一样。 然后,我们可以使用实例化后的cipher和生成的密钥来执行加密。

Let's write a test method:

@Test
void givenPassword_whenEncrypt_thenSuccess() 
    throws InvalidKeySpecException, NoSuchAlgorithmException, 
    IllegalBlockSizeException, InvalidKeyException, BadPaddingException, 
    InvalidAlgorithmParameterException, NoSuchPaddingException {
    
    String plainText = "www.baeldung.com";
    String password = "baeldung";
    String salt = "12345678";
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    SecretKey key = AESUtil.getKeyFromPassword(password,salt);
    String cipherText = AESUtil.encryptPasswordBased(plainText, key, ivParameterSpec);
    String decryptedCipherText = AESUtil.decryptPasswordBased(
      cipherText, key, ivParameterSpec);
    Assertions.assertEquals(plainText, decryptedCipherText);
}

5.4. Object

为了加密一个Java对象, 我们需要使用 SealedObject 类。该对象需要支持串行化( Serializable). 下面我们定义一个 Student 类:

public class Student implements Serializable {
    private String name;
    private int age;

    // 省略setters 和 getters 方法
}

下面加密 Student 对象 :

public static SealedObject encryptObject(String algorithm, Serializable object,
    SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,
    NoSuchAlgorithmException, InvalidAlgorithmParameterException, 
    InvalidKeyException, IOException, IllegalBlockSizeException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    SealedObject sealedObject = new SealedObject(object, cipher);
    return sealedObject;
}

下面是解密:

public static Serializable decryptObject(String algorithm, SealedObject sealedObject,
    SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,
    NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
    ClassNotFoundException, BadPaddingException, IllegalBlockSizeException,
    IOException {
    
    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    Serializable unsealObject = (Serializable) sealedObject.getObject(cipher);
    return unsealObject;
}

编写测试代码:

@Test
void givenObject_whenEncrypt_thenSuccess() 
    throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
    InvalidAlgorithmParameterException, NoSuchPaddingException, IOException, 
    BadPaddingException, ClassNotFoundException {
    
    Student student = new Student("Baeldung", 20);
    SecretKey key = AESUtil.generateKey(128);
    IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    String algorithm = "AES/CBC/PKCS5Padding";
    SealedObject sealedObject = AESUtil.encryptObject(
      algorithm, student, key, ivParameterSpec);
    Student object = (Student) AESUtil.decryptObject(
      algorithm, sealedObject, key, ivParameterSpec);
    assertThat(student).isEqualToComparingFieldByField(object);
}

6. 总结

总结,本文我们学习了如何在Java中使用AES算法来加密和解密,输入的数据类型可以是字符串,文件,Java对象等。 此外,我们还讨论了AES的几种加密模式以及加密后数据的大小。

和本站其他教程一样, 本文完整源代码存放在 GitHub.