概述
现代对称加密算法,如高级加密标准(AES),是目前最常用的加密方法之一。本篇文章基于Java 加密架构(Java Cryptography Architecture, JCA)循序渐进的带你实现加密算法,通过系列文章最终实现一个完成的文件加密系统。
AES的工作原理(了解)
AES 通过一系列的轮(rounds)进行加密。轮的数量取决于密钥长度:
- 128 位密钥使用 10 轮。
- 192 位密钥使用 12 轮。
- 256 位密钥使用 14 轮。
每一轮包括以下步骤:
- 字节代换(SubBytes):使用一个固定的 S-Box 将每个字节替换为另一个字节。
- 行移位(ShiftRows):对状态矩阵的行进行循环移位。
- 列混淆(MixColumns):通过线性变换混合每一列的数据。
- 轮密钥加(AddRoundKey):将当前状态与轮密钥进行异或操作。
最后一轮省略了列混淆步骤。
最基础的的加密-加密字符串
下面我以一个最基础的案例"对字符串进行加密"来引出最基础的加密步骤。JCA中核心是通过一个Cipher类和SecretKey来实现加密和解密功能的,下面对Cipher 和 SecretKey进行基本介绍,如果不关心具体内容,可直接忽略下面两部分介绍,跳到代码实战段落。
Cipher类的基本介绍及使用
Cipher
类是 Java 加密架构(Java Cryptography Architecture, JCA)的一部分,位于 javax.crypto
包中。它是一个用于加密和解密操作的核心类,提供了对称加密、非对称加密以及流密码等多种加密方式的支持。通过它,你可以将数据从明文转换为密文(加密),或者将密文转换回明文(解密)。
这里只对基础功能做介绍,以便读者快速理解整个加解密的框架,后面涉及到更复杂加密时,再对其进行详细讲解。
下面是对 Cipher
类的使用步骤的详细介绍:
1. 基本功能
Cipher
类提供了加密和解密功能。
2. 实例化 Cipher
要使用 Cipher
,首先需要获取一个 Cipher
对象实例。可以通过 Cipher.getInstance()
方法来实现,该方法接受一个字符串参数,用于指定加密算法和模式。例如:
Cipher cipher = Cipher.getInstance("AES");
在这个例子中,"AES"
指定了使用 AES 算法。你也可以指定加密模式和填充方案,例如 "AES/CBC/PKCS5Padding"
。
3. 初始化 Cipher
在加密或解密之前,必须初始化 Cipher
对象。初始化时需要指定操作模式(加密或解密)以及密钥:
- 加密模式 :
Cipher.ENCRYPT_MODE
- 解密模式 :
Cipher.DECRYPT_MODE
例如,初始化用于加密的 Cipher
:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
这里的 secretKey
是一个 SecretKey
对象,代表对称加密算法所用的密钥,将在下文对SecretKey
进行介绍。
4. 加密和解密操作
一旦 Cipher
被正确初始化,就可以进行加密或解密操作。使用 doFinal()
方法,该方法接受一个字节数组作为输入,并返回一个字节数组作为输出。
-
加密:
byte[] encrypted = cipher.doFinal(plaintext.getBytes());
-
解密:
byte[] decrypted = cipher.doFinal(encryptedBytes);
SecretKey
在 AES 加密中,SecretKey
是一个接口,代表对称加密算法所使用的密钥。它封装了加密算法所需的密钥材料,通常以字节数组的形式存储。
SecretKey 的生成
随机生成
在 Java 中,SecretKey
的实例通常通过 KeyGenerator
类生成,下面是一个简单的示例:
java
class SecretKeyDemo{
public static SecretKey generateSecretKey() throws NoSuchAlgorithmException {
// 创建一个 KeyGenerator 对象,用于 AES 算法
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// 初始化 KeyGenerator,指定密钥长度(128、192 或 256 位)
keyGen.init(128);
// 生成 SecretKey
SecretKey secretKey = keyGen.generateKey();
return secretKey;
}
}
指定key生成
除了通过 KeyGenerator
类生成一个随机的SecretKey,也可以通过SecretKeySpec类指定"密码"生成。
java
class SecretKeyDemo{
// Method to generate a SecretKey from a given string key
public static SecretKey getKeyFromPassword(String password) throws Exception {
byte[] key = password.getBytes("UTF-8");
return new SecretKeySpec(key, 0, 16, "AES");
}
SecretKeySpec
是 SecretKey
的一个实现类,它允许我们直接从字节数组中构建一个密钥。
key
参数是我们之前从字符串密码生成的字节数组。0
和16
分别指定了字节数组中密钥材料的起始索引和长度。在这个例子中,我们选择了前 16 个字节作为 AES 密钥。"AES"
参数指定了这个密钥将被用于 AES 加密算法。
扩展
需要注意的是,这种从密码生成密钥的方法并不是最安全的做法。直接使用密码的字节作为密钥可能会导致密钥的熵不足,容易受到暴力破解攻击。更好的做法是使用一个密钥派生函数(Key Derivation Function, KDF)来从密码中生成密钥,例如 PBKDF2 或 Argon2。这些函数可以增加密钥的熵并使其更加安全。
实现对字符串的加解密
在上述对Cipher和SecretKey进行基础介绍后,我们便可以实现一个对字符串的加密功能。
java
class AESBaseV1{
public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
String plaintext = "Hello, World!";
System.out.println("origin Text:" + plaintext);
// 生成密钥
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128); // 选择密钥长度
SecretKey secretKey = keyGen.generateKey();
// 加密
Cipher cipher = Cipher.getInstance("AES");// 使用AES进行加密
cipher.init(Cipher.ENCRYPT_MODE, secretKey);// 初始化加密模式
byte[] encrypted = cipher.doFinal(plaintext.getBytes());// 加密,输入为加密前的字节数字,输出为加密后的字节数组
// 打印加密后的文本
String encryptedBase64 = Base64.getEncoder().encodeToString(encrypted);
System.out.println("Encrypted: " + encryptedBase64);
// 解密
cipher.init(Cipher.DECRYPT_MODE, secretKey);// 初始化解密模式
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));// 解密,输入为加密后的字节数组,输出为解密后的字节数组
System.out.println("Decrypted: " + new String(decrypted));
}
}
实现对文件的加解密
加密
上文中,我们已实现了对文本的加密,那么我们如何对一个文件进行加密呢?其实很简单,无非就是将cipher.doFinal()
的字节数据换为文件对应的字节数组即可,下面我们来实现一下,为了后面解密测试方便,我们这里就使用字符串自己生成一个SecretKey
,加密的核心方法为:encryptFile
。
java
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class AESBaseV1 {
/**
* 使用AES算法对文件进行加密。
* @param secretKey 加密所需的密钥。
* @param originFilePath 待加密的文件路径。
* @param encryptFilePath 加密后的文件输出路径。
* @throws IOException 如果读写文件时发生I/O错误。
* @throws NoSuchPaddingException 如果指定的填充算法不存在。
* @throws NoSuchAlgorithmException 如果指定的加密算法不存在。
* @throws InvalidKeyException 如果密钥无效。
* @throws IllegalBlockSizeException 如果加密或解密的数据长度不符合块大小。
* @throws BadPaddingException 如果加密或解密的数据填充不正确。
*/
private static void encryptFile(SecretKey secretKey,String originFilePath,String encryptFilePath) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// 加密
Cipher cipher = Cipher.getInstance("AES");// 使用AES进行加密
cipher.init(Cipher.ENCRYPT_MODE, secretKey);// 初始化加密模式
// 获取文件的字节数组
byte[] fileContent = Files.readAllBytes(Paths.get(originFilePath));
// 获取解密后的字节数组
byte[] encrypted = cipher.doFinal(fileContent);// 加密,输入为加密前的字节数字,输出为加密后的字节数组
// 将加密后的字节数组写出为文件
Files.write(Paths.get(encryptFilePath), encrypted);
}
public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
// 注意密码长度,一定长于或等于 SecretKeySpec 的构造函数中的取值
String password = "0123456789012345";
// 生成密钥
byte[] key = password.getBytes("UTF-8");
SecretKeySpec secretKey = new SecretKeySpec(key, 0, 16, "AES");
String originFilePath = /Downloads/加密文件原文件/机器人头像.jpg";
String encryptFilePath = "/Downloads/加密后文件/secret.env";
// 加密文件
encryptFile(secretKey, originFilePath, encryptFilePath);
}
}
加密后我们去查看这个文件发现已经不能被打开了。现在我们对其进行解密:
解密
解密的方法其实和加密没有什么区别,无非是改变cipher
的模式为解密而已,下面是关于解密的代码,核心方法为decryptFile
。
java
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class AESBaseV1 {
/**
* 使用AES算法对文件进行加密。
* @param secretKey 加密所需的密钥。
* @param originFilePath 待加密的文件路径。
* @param encryptFilePath 加密后的文件输出路径。
* @throws IOException 如果读写文件时发生I/O错误。
* @throws NoSuchPaddingException 如果指定的填充算法不存在。
* @throws NoSuchAlgorithmException 如果指定的加密算法不存在。
* @throws InvalidKeyException 如果密钥无效。
* @throws IllegalBlockSizeException 如果加密或解密的数据长度不符合块大小。
* @throws BadPaddingException 如果加密或解密的数据填充不正确。
*/
private static void encryptFile(SecretKey secretKey, String originFilePath, String encryptFilePath) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
// 加密
Cipher cipher = Cipher.getInstance("AES");// 使用AES进行加密
cipher.init(Cipher.ENCRYPT_MODE, secretKey);// 初始化加密模式
// 获取文件的字节数组
byte[] fileContent = Files.readAllBytes(Paths.get(originFilePath));
// 获取解密后的字节数组
byte[] encrypted = cipher.doFinal(fileContent);// 加密,输入为加密前的字节数字,输出为加密后的字节数组
// 将加密后的字节数组写出为文件
Files.write(Paths.get(encryptFilePath), encrypted);
}
/**
* 解密文件
* @param secretKey 用于解密的密钥
* @param encryptFilePath 加密文件的路径
* @param decryptFilePath 解密后文件的路径
* @throws NoSuchPaddingException 如果没有找到指定的填充算法
* @throws NoSuchAlgorithmException 如果没有找到指定的加密算法
* @throws InvalidKeyException 如果密钥无效
* @throws IOException 如果读写文件时发生I/O错误
* @throws IllegalBlockSizeException 如果加密块的大小不正确
* @throws BadPaddingException 如果加密块的填充不正确
*/
private static void decryptFile(SecretKey secretKey,String encryptFilePath,String decryptFilePath) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException {
// 加密
Cipher cipher = Cipher.getInstance("AES");// 使用AES进行加密
cipher.init(Cipher.DECRYPT_MODE, secretKey);// 初始化解密模式
// 获取加密文件的字节数组
byte[] fileContent = Files.readAllBytes(Paths.get(encryptFilePath));
// 获取解密后的字节数组
byte[] decrypted = cipher.doFinal(fileContent);
// 将加密后的字节数组写出为文件
Files.write(Paths.get(decryptFilePath), decrypted);
}
public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
// 注意密码长度,一定长于或等于 SecretKeySpec 的构造函数中的取值
String password = "0123456789012345";
// 生成密钥
byte[] key = password.getBytes("UTF-8");
SecretKeySpec secretKey = new SecretKeySpec(key, 0, 16, "AES");
String originFilePath = "/Downloads/加密文件原文件/机器人头像.jpg";
String encryptFilePath = "/Downloads/加密后文件/secret.env";
// 加密
// encryptFile(secretKey, originFilePath, encryptFilePath);
// 解密
String decryptFilePath = "/Downloads/解密后文件/解密后的机器人头像哦.jpg";
decryptFile(secretKey,encryptFilePath,decryptFilePath);
}
}
加密后我们发现,嘿!文件又复原了,神奇不神奇!
文件加密的待优化项
上文我们实现了对文件的基础加密,但是存在以下几个问题:
- 加密后的文件名需要我们自己指定,能否自动生成随机文件名?
- 解密后也需要我们手动指定文件名及文件后缀,能否解密时自动还原加密时的原始文件名?
针对上述问题,我将在后续的文章中进行探讨和解决。
以上,祝你今天愉快!