给你的小秘密加点隐私——Java实现AES加密全攻略

概述

现代对称加密算法,如高级加密标准(AES),是目前最常用的加密方法之一。本篇文章基于Java 加密架构(Java Cryptography Architecture, JCA)循序渐进的带你实现加密算法,通过系列文章最终实现一个完成的文件加密系统。

AES的工作原理(了解)

AES 通过一系列的轮(rounds)进行加密。轮的数量取决于密钥长度:

  • 128 位密钥使用 10 轮。
  • 192 位密钥使用 12 轮。
  • 256 位密钥使用 14 轮。

每一轮包括以下步骤:

  1. 字节代换(SubBytes):使用一个固定的 S-Box 将每个字节替换为另一个字节。
  2. 行移位(ShiftRows):对状态矩阵的行进行循环移位。
  3. 列混淆(MixColumns):通过线性变换混合每一列的数据。
  4. 轮密钥加(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");  
    }

SecretKeySpecSecretKey 的一个实现类,它允许我们直接从字节数组中构建一个密钥。

  • key 参数是我们之前从字符串密码生成的字节数组。
  • 016 分别指定了字节数组中密钥材料的起始索引和长度。在这个例子中,我们选择了前 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);  
    }  
  
}

加密后我们发现,嘿!文件又复原了,神奇不神奇!

文件加密的待优化项

上文我们实现了对文件的基础加密,但是存在以下几个问题:

  • 加密后的文件名需要我们自己指定,能否自动生成随机文件名?
  • 解密后也需要我们手动指定文件名及文件后缀,能否解密时自动还原加密时的原始文件名?

针对上述问题,我将在后续的文章中进行探讨和解决。

以上,祝你今天愉快!

相关推荐
hikktn15 分钟前
Java 兼容读取WPS和Office图片,结合EasyExcel读取单元格信息
java·开发语言·wps
迪迦不喝可乐15 分钟前
软考 高级 架构师 第十一章 面向对象分析 设计模式
java·设计模式
檀越剑指大厂1 小时前
【Java基础】使用Apache POI和Spring Boot实现Excel文件上传和解析功能
java·spring boot·apache
苹果酱05671 小时前
Golang的网络流量分配策略
java·spring boot·毕业设计·layui·课程设计
孑么1 小时前
GDPU Android移动应用 重点习题集
android·xml·java·okhttp·kotlin·android studio·webview
未命名冀2 小时前
微服务面试相关
java·微服务·面试
Heavydrink2 小时前
ajax与json
java·ajax·json
阿智智2 小时前
纯手工(不基于maven的pom.xml、Web容器)连接MySQL数据库的详细过程(Java Web学习笔记)
java·mysql数据库·纯手工连接
fangxiang20083 小时前
spring boot 集成 knife4j
java·spring boot
王先生技术栈3 小时前
思维导图,Android版本实现
java·前端