给你的小秘密加点隐私——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);  
    }  
  
}

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

文件加密的待优化项

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

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

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

以上,祝你今天愉快!

相关推荐
B站计算机毕业设计超人41 分钟前
计算机毕业设计制造业MES生产管理平台 MES 生产制造源码+文档+运行视频+讲解视频)
java·spring boot·mysql·eclipse·tomcat·maven·web
技术咖啡馆C2 小时前
二、通义灵码插件保姆级教学-IDEA(使用篇)
java·intellij-idea·通义灵码·ai助手·idea-plugin
星星点点洲2 小时前
【SpringBoot实现全局API限频】 最佳实践
java·spring boot·后端
linwq82 小时前
Java网络编程学习(一)
java·网络·学习
lllsure2 小时前
【快速入门】SpringMVC
java·后端·spring·mvc
翻晒时光2 小时前
24、深入理解与使用 Netty:Java 高性能网络编程的利器
java·网络
阿芯爱编程3 小时前
java面试题
java·后端
吴天德少侠3 小时前
设计模式中的关联和依赖区别
java·开发语言·设计模式
道友老李3 小时前
【Java】多线程和高并发编程(三):锁(下)深入ReentrantReadWriteLock
java·系统架构
geovindu4 小时前
java: framework from BLL、DAL、IDAL、MODEL、Factory using postgresql 17.0
java·开发语言·postgresql