常用加密算法分析实践

一、引言

日常开发中,我们经常会遇到各种各样的数据加密需求,对数据加密要求比较高的如医疗行业、金融行业、军工领域等等。信息安全极大影响了人身安全和财产安全、国家安全。近年来国家对信息安全越来越重视,出台了一系列扶持政策,我们作为软件行业从业人员就更应该掌握信息加密的常用手段,学习常用加密算法,能够根据其特点合理应用到实际加密场景中。

二、常用加密算法分类

这里笔者先解释几个大的概念名词,以便我们搞懂密码算法分类,避免别人用什么我们就跟着用什么算法来加密,却不知道算法是否适合。

  1. 可逆加密算法:加密过程可以通过使用正确的密钥或密钥对进行逆转,从而恢复原始的未加密数据(明文)。
  2. 不可逆加密算法:一旦数据被加密,就无法通过加密结果反推出原始数据。
  3. 对称加密算法:使用相同的密钥(也称为私钥或秘密密钥)来进行数据的加密和解密。
  4. 非对称加密算法:使用一对密钥,即公钥和私钥。公钥用于加密数据,可以公开分享给任何人;而私钥则需要保密,仅由数据的预期接收者持有,用于解密数据。

注:以下加粗的算法表示日常使用最多的算法,重点关注学习其特点

可逆加密算法

  • 对称加密算法
    • AES (Advanced Encryption Standard):一种广泛使用的对称加密标准,支持128、192和256位的密钥长度。
    • DES (Data Encryption Standard):早期的对称加密算法,现在被认为不够安全,通常使用三重DES(3DES)来增强安全性。
    • 3DES (Triple DES):基于DES,通过三次加密提高安全性。
    • Blowfish:一种高效且快速的对称加密算法,支持从32位到448位的密钥长度。
    • IDEA (International Data Encryption Algorithm):一种快速的块加密算法,使用128位的密钥。
    • RC4, RC5, RC6:由RSA Security开发的一系列流加密算法,其中RC4曾被广泛使用,但如今已被认为不够安全。
  • 非对称加密算法
    • RSA:基于大数因子分解的困难性,通常用于公钥基础设施(PKI),包含公钥和私钥。
    • ECC (Elliptic Curve Cryptography):基于椭圆曲线数学,提供与RSA相同的安全性但使用更短的密钥,因此更高效。
    • DSA (Digital Signature Algorithm):主要用于数字签名,基于离散对数问题。
    • ElGamal:基于离散对数问题,用于加密和签名。
    • Diffie-Hellman Key Exchange:虽然主要用于密钥交换而不是直接加密,但也是可逆的,因为它允许双方协商一个共享的秘密密钥。

不可逆加密算法

  • MD5 (Message-Digest Algorithm 5):虽然曾经广泛使用,但现在被认为不够安全,因为存在碰撞攻击的可能性。
  • SHA-1 (Secure Hash Algorithm 1):类似于MD5,已被认为不够安全,不建议用于新的安全应用。
  • SHA-2 (Secure Hash Algorithm 2):
    • SHA-224
    • SHA-256
    • SHA-384
    • SHA-512
    • 这些是SHA-1的升级版,提供更高安全性,广泛用于现代安全标准。
  • SHA-3 (Secure Hash Algorithm 3):
    • SHA3-224
    • SHA3-256
    • SHA3-384
    • SHA3-512
    • SHA-3是SHA-2之后的新一代哈希函数,设计更为安全。
  • BLAKE2:一个高效且安全的哈希函数家族,分为BLAKE2b和BLAKE2s,适用于不同场景。
  • RIPEMD-160:与SHA-1相似的160位哈希函数,尽管不那么流行,但仍在某些场景下使用。
  • Whirlpool:一个更强大的256位哈希函数,设计为抵抗多种攻击。
  • HMAC (Hash-based Message Authentication Code):结合哈希函数和密钥,用于生成消息认证码,验证数据的完整性和来源。

三、加密场景

这里就不分析每种加密算法的使用场景了,笔者按照实际软件开发中的业务加密场景来选择算法。实际场景用到的加密算法一般都是多种算法的组合,满足安全性、高效性等。

场景一:密码加密

密码加密是各种web应用最常见的需要加密的数据,而且密码普遍要求不能以明文存储,并且即使拿到密文,也无法还原出对应的明文。一般我们选择使用不可逆加密算法,使用哈希算法(如bcrypt、scrypt或Argon2)进行密码哈希是标准做法。哈希和密文是密码学中常见的两个概念,它们之间有以下几个区别:

  1. 单向性:哈希是一种单向函数,它将任意长度的输入转换成固定长度的输出(通常是固定长度的字符串或数字)。这意味着无法从哈希值中恢复原始输入。而密文是通过加密算法将明文转换成不可读的形式,但可以通过相应的解密算法将其恢复为明文。

  2. 唯一性:哈希函数具有唯一性,即不同的输入几乎总是会产生不同的哈希值。这意味着即使输入仅有微小的变化,也会导致完全不同的哈希值。而密文可能会有相同的输出,特别是在使用较弱的加密算法或者加密模式时。

  3. 不可逆性:哈希函数是不可逆的,即无法从哈希值中逆推出原始输入。这意味着相同的输入会产生相同的哈希值,但无法通过哈希值还原出原始输入。而密文可以通过解密算法逆向转换为明文。

  4. 安全性:哈希函数的安全性主要关注碰撞问题,即是否存在不同的输入产生相同的哈希值。好的哈希函数应该具有较低的碰撞概率。而密文的安全性取决于使用的加密算法和密钥长度,以及攻击者能力的限制。

总的来说,哈希值主要用于数据的完整性校验、密码校验与索引等应用。而密文则主要用于保护数据的机密性,确保只有具备相应解密密钥的人才能恢复原始的明文。

bcrypt

bcrypt是一种广泛使用的密码哈希函数,设计用于加强密码存储的安全性。它基于Blowfish加密算法并进行了专门的适应性调整,以抵抗彩虹表攻击和暴力破解。以下是关于bcrypt的几个关键特点和用途:

  • 慢速哈希:bcrypt故意设计为计算密集型,这意味着即使在强大的硬件上,密码哈希过程也会相对较慢。这种“慢”特性增加了攻击者尝试破解密码所需的时间和资源成本,从而提升了安全性。

  • 盐值(Salt):在哈希过程中,bcrypt会为每个密码添加一个随机的盐值。这个盐值与密码结合后一起进行哈希运算,即使两个用户有相同的密码,他们的哈希值也会因为盐值的不同而不同,这大大降低了通过预先计算好的哈希表(如彩虹表)进行破解的可能性。

  • 工作因子(Cost Factor):bcrypt允许设置一个工作因子,这个参数决定了哈希计算的迭代次数。随着计算能力的提升,系统管理员可以增加工作因子来保持哈希过程的计算难度,从而维持与技术进步相匹配的安全水平。

  • 适应性:bcrypt的设计考虑到了未来计算能力的增强,通过调整工作因子,它可以随着时间推移保持密码存储的安全性。

  • 广泛适用性:bcrypt被推荐用于多种应用程序和平台上的密码存储,包括网站、移动应用和内部系统。许多编程语言都有支持bcrypt的库或模块,便于开发者集成到项目中。

  • 安全性证明:bcrypt经过了安全专家的审查,并在实际应用中证明了其对抗密码破解攻击的有效性,是密码存储领域的一个标准选择。

引入如下依赖

<dependency>
    <groupId>org.mindrot</groupId>
    <artifactId>jbcrypt</artifactId>
    <version>0.4</version> <!-- 或者使用最新版本 -->
</dependency>

工具类代码

package com.crypto;

import org.mindrot.jbcrypt.BCrypt;

public class BCryptUtil {

    /**
     * 使用BCrypt加密密码
     *
     * @param password 明文密码
     * @return 加密后的密码
     */
    public static String encrypt(String password) {
        return BCrypt.hashpw(password, BCrypt.gensalt());
    }

    /**
     * 验证密码是否与给定的哈希匹配
     *
     * @param password 明文密码
     * @param hashedPassword 哈希过的密码
     * @return 如果匹配则返回true,否则返回false
     */
    public static boolean validatePassword(String password, String hashedPassword) {
        return BCrypt.checkpw(password, hashedPassword);
    }
}

测试用例

package com.datastructures;

import com.crypto.BCryptUtil;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class BCryptUtilTest {

    @Test
    public void testEncryptAndValidatePassword() {
        // 待加密的明文密码
        String password = "mySecurePassword123";
        // 使用工具类加密密码
        String hashed = BCryptUtil.encrypt(password);
        // 确保加密后得到的哈希值不为空
        assertNotNull(hashed);

        System.out.println("密码加密的哈希值:"+hashed);

        String password1 = "mySecurePassword123";
        String password2 = "mySecurePassword250";

        boolean isValid1 = BCryptUtil.validatePassword(password1, hashed);
        boolean isValid2 = BCryptUtil.validatePassword(password2, hashed);
        System.out.println("password1校验结果:" + isValid1);
        System.out.println("password2校验结果:" + isValid2);
    }
}

结果如下
在这里插入图片描述
实际开发中,可以使用此工具把密码加密成哈希值存在数据库中,后续密码验证时则用BCryptUtil.validatePassword(传过来的密码明文,数据库存储的密文hash),结果为true或者false,对应密码验证通过与否。

scrypt

scrypt是一种密码学散列函数,旨在降低大规模计算资源(如GPU和ASIC)在彩虹表攻击中的优势。与bcrypt类似,scrypt也是用于密码存储和验证的,但是它结合了大内存需求,使得攻击者难以进行并行化攻击。scrypt的计算过程比bcrypt更复杂,因为它
包括了内存密集型的步骤。

依赖引入

  <dependency>
      <groupId>com.lambdaworks</groupId>
      <artifactId>scrypt</artifactId>
      <version>1.4.0</version> <!-- 请使用最新的稳定版本 -->
  </dependency>      

工具类代码

package com.crypto;

import com.lambdaworks.crypto.SCrypt;

import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

/**
 * @author hulei
 * 提供Scrypt密码加密和验证功能
 */
public class ScryptUtil {

    private static final int N = 16384; // 工作因子
    private static final int R = 8;//R表示轮数,即加密过程中进行的轮数
    private static final int P = 1;//表示扩展密钥的轮数
    private static final int KEY_LENGTH = 32; // 密钥长度(字节)

    /**
     * 使用Scrypt加密密码并返回包含盐值和加密密钥的Base64编码字符串。
     *
     * @param password 明文密码
     * @return 包含盐值和加密后的密码的Base64编码字符串
     */
    public static String encrypt(String password) throws GeneralSecurityException {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        byte[] hashedPassword = SCrypt.scrypt(password.getBytes(), salt, N, R, P, KEY_LENGTH);
        // 使用Base64编码以文本形式安全存储盐值和哈希
        return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hashedPassword);
    }

    /**
     * 验证密码是否与给定的Scrypt哈希匹配。
     *
     * @param password 明文密码
     * @param saltAndHashedPassword 包含盐值和加密密码的Base64编码字符串(盐值与哈希用":"分隔)
     * @return 如果匹配则返回true,否则返回false
     */
    public static boolean validatePassword(String password, String saltAndHashedPassword) throws GeneralSecurityException {
        // 使用Base64解码盐值和哈希
        String[] parts = saltAndHashedPassword.split(":");
        byte[] salt = Base64.getDecoder().decode(parts[0]);
        byte[] hashedPassword = Base64.getDecoder().decode(parts[1]);
        byte[] derivedKey = SCrypt.scrypt(password.getBytes(), salt, N, R, P, hashedPassword.length);
        // 比较解密的哈希和提供的哈希
        return Arrays.equals(derivedKey, hashedPassword);
    }
}

测试用例

package com.datastructures;

import com.crypto.ScryptUtil;

import org.junit.jupiter.api.Test;

import java.security.GeneralSecurityException;


class ScryptUtilTest {

    private static final String PASSWORD = "mySecurePassword";

    private static final String newPassword = "mySecurePassword11111";
    
    @Test
    void testValidatePasswordValid() throws GeneralSecurityException {

        String saltAndHashedPassword = ScryptUtil.encrypt(PASSWORD);
        System.out.println("原密码加密后的密文:"+saltAndHashedPassword);
        boolean isValid = ScryptUtil.validatePassword(PASSWORD, saltAndHashedPassword);
        System.out.println("原密码校验结果: " + isValid);

        boolean isValid2 = ScryptUtil.validatePassword(newPassword, saltAndHashedPassword);
        System.out.println("新密码校验结果: " + isValid2);

    }

}

测试代码中,先用原密码生成哈希值密文,在分别用原密码和新密码分别进行密码校验,测试结果如下:
在这里插入图片描述

这个密文也是动态的,同样的明文每次生成密文都是不一样的,只要输入的密码明文和数据库存储的密文哈希值校验通过,说明输入的密码正确。

argon2

Argon2是密码学中的一种密码哈希函数,设计用于抵御现代密码攻击,特别是针对图形处理单元(GPU)和定制硬件的攻击。它在2015年赢得了“Password Hashing Competition”(密码哈希竞赛),成为推荐的密码存储标准。

依赖引入

    <dependency>
         <groupId>de.mkammerer</groupId>
         <artifactId>argon2-jvm</artifactId>
         <version>2.10</version>
     </dependency>

工具类代码

package com.crypto;

import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;

/**
 * argon2算法使用示例
 * @author hulei
 * @date 2024/5/15 11:34
 */
public class Argon2Util {

    private static final Argon2Factory.Argon2Types ARG_TYPES = Argon2Factory.Argon2Types.ARGON2id; // 哈希类型
    private static final int ITERATIONS = 5;// 迭代次数
    private static final int MEMORY_COST = 64 * 1024; // 内存复杂度(以KB为单位)
    private static final int PARALLELISM = 4; // 并行度,最小为4,太小会报错 java.lang.IllegalStateException: Output is too short (-2)

    public static String hashPassword(String password) {
        Argon2 argon2 = Argon2Factory.create(ARG_TYPES, MEMORY_COST, PARALLELISM);
        return argon2.hash(ITERATIONS, MEMORY_COST, PARALLELISM, password.toCharArray());
    }

    public static boolean validatePassword(String password, String encodedHash) {
        Argon2 argon2 = Argon2Factory.create(ARG_TYPES, MEMORY_COST, PARALLELISM);
        return argon2.verify(encodedHash, password.toCharArray());
    }

}

测试用例

package com.datastructures;

import com.crypto.Argon2Util;
import org.junit.jupiter.api.Test;

class Argon2UtilTest {

    // 测试hashPassword方法
    @Test
    void testHashPassword() {
        // 准备阶段
        String password = "testPassword123";

        String newPassword = "testPassword789";

        // 操作阶段
        String hashed = Argon2Util.hashPassword(password);

        System.out.println(hashed);

        boolean isValid = Argon2Util.validatePassword(password, hashed);
        System.out.println("原密码校验是否通过:"+isValid);

        boolean isValid2 = Argon2Util.validatePassword(newPassword, hashed);
        System.out.println("新密码校验是否通过:"+isValid2);
    }

}

测试方法与前两种是一样的,都是用原密码的哈希值和输入的密码进行校验,如果输入的密码和原密码一致,通过校验则说明密码输入正确,否则不正确。实际开发中,一般是新用户注册时,把密码明文进行哈希运算,存到数据库中,后面登录校验时,把输入的密码明文和存储的哈希值进行匹配即可。注意密码每次的哈希值都是不一样的,不能把输入的密码明文进行哈希运算去和表里存的哈希值去比较。

算法选择

在选择密码哈希算法时,通常考虑以下几个因素:安全性、计算成本、内存需求和实现的复杂性。以下是Bcrypt、Scrypt和Argon2之间的对比:

  1. Bcrypt
  • 优点:广泛使用,成熟且经过时间检验的安全性,内存需求相对较低。
  • 缺点:相对较慢,但相比Scrypt和Argon2,内存需求较低,这可能使其对某些攻击的防护不够强。
  1. Scrypt
  • 优点:增加了内存需求,使得针对GPU和ASIC的暴力攻击更难,比Bcrypt更难并行化。
  • 缺点:相比于Bcrypt,计算和内存成本更高,可能对资源有限的环境不利。
  1. Argon2
  • 优点:设计时考虑到抵御现代攻击,包括GPU和ASIC攻击,内存和计算需求可配置,是Password Hashing Competition的胜出者。
  • 缺点:计算和内存成本最高,实现可能更复杂,对资源敏感的环境可能是个挑战。

选择建议:
如果你关心的是广泛的兼容性和成熟度,Bcrypt可能是一个不错的选择,尽管它的安全性略低于Scrypt和Argon2。
如果你的系统资源充足,且希望提供更高级别的防护,尤其是对抗专门硬件的攻击,那么Scrypt或Argon2更适合。
最新的最佳实践通常推荐使用Argon2,因为它在设计时考虑了最新的威胁模型,并且在安全性方面有优势。

场景二:一般数据加密

这个就是我们一般的业务数据加密场景了,目前使用最多最成熟的就是AES高级加密标准算法。美国政府在2001年采用的一种加密标准,替代了之前不安全的DES,尽管后来出现了三重DES,但也没什么人用了。下面就通过java代码的方式来展示这一算法的实际应用。

注意:AES算法是对称加密算法,它的密钥长度目前只能是128位、192位、256位,
对应的字节数是16,24,32个字节。java环境下可能最大就是支持到128位,如果需要在Java中使用128位以上的密钥,需要下载并安装Java Cryptography Extension (JCE)无限制强度管辖策略文件,以解除这一限制。安装后,就可以按照上述提到的密钥长度生成相应的AES密钥了。

在正式编写代码之前介绍下AES的加密模式

AES加密模式是指在AES算法中,对待加密的数据进行分块处理的方式。常见的AES加密模式有ECB、CBC、CFB、OFB和CTR等。

  1. ECB(Electronic Codebook)模式:将待加密的数据分成若干个块,每个块独立加密,相同的明文块会加密成相同的密文块。ECB模式简单快速,适合只加密小数据块的情况,但安全性较差。

  2. CBC(Cipher Block Chaining)模式:每个明文块与前一个密文块进行异或操作,然后再加密。因为每个密文块都依赖前一个密文块,所以即使明文相同,加密后的密文也不同。CBC模式更安全,适合加密大数据块,但加密效率较低。

  3. CFB(Cipher Feedback)模式:将前一个密文块作为输入,然后进行加密生成密文块,再与明文块进行异或操作,得到加密后的密文块。CFB模式可以实现流加密,适用于对实时数据进行加密和解密。

  4. OFB(Output Feedback)模式:将前一个密文块作为输入,然后进行加密生成密文块,再与明文块进行异或操作,得到加密后的密文块。OFB模式也可以实现流加密,适用于对实时数据进行加密和解密。

  5. CTR(Counter)模式:使用一个计数器和密钥生成一系列的伪随机数流,与明文进行异或操作得到密文。CTR模式可以实现流加密,适用于对实时数据进行加密和解密,同时充分利用了计算机的并行处理能力。

  6. GCM(Galois/Counter Mode)模式:结合了CTR模式的高效性和认证加密(AEAD),提供了数据加密和完整性校验。常用于需要高性能和安全认证的应用场景。

这些加密模式都在AES算法的基础上进行了一些改进和优化,以适应不同场景下的加密需求。选择合适的加密模式需要综合考虑安全性、效率和实际应用情况。

工具类代码

package com.crypto;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * AES工具类,用于加密和解密数据。
 *
 * @author hulei
 */
public class AESUtils {

    private static final String ALGORITHM = "AES";

    private static final String ALGORITHM_CBC = "AES/CBC/PKCS5Padding";

    private static final String ALGORITHM_ECB = "AES/ECB/PKCS5Padding";

    private static final String ALGORITHM_CFB = "AES/CFB/NoPadding";

    private static final String ALGORITHM_OFB = "AES/OFB/NoPadding";

    private static final String ALGORITHM_CTR = "AES/CTR/NoPadding";

    private static final String ALGORITHM_GCM = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH_BITS = 128; // 默认128位(16字节)的标签长度


    /**
     * 生成一个用于AES加密的随机初始化向量(IV)。
     * AES加密模式中,IV是用来确保加密数据的唯一性和随机性的。本方法生成一个长度为16字节的随机IV,
     * 适用于AES的典型应用场合。
     *
     * @return 生成的随机初始化向量,作为字节数组返回。
     */
    public static byte[] generateIV() {
        byte[] iv = new byte[16];//GCM模式更推荐12个字节
        new SecureRandom().nextBytes(iv);
        return iv;
    }

    /**
     * 生成AES密钥的字节数组。
     *
     * @param keySize 密钥大小,必须为128、192或256位。
     * @return 返回根据指定密钥大小生成的AES密钥的字节数组。
     * @throws Exception 如果传入的密钥大小不是128、192或256位,将抛出异常。
     */
    public static byte[] generateKeyBytes(int keySize) throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
        keyGen.init(keySize);
        SecretKey secretKey = keyGen.generateKey();
        return secretKey.getEncoded();
    }

    /**
     * 使用AES算法对明文进行加密,并将加密后的数据与初始化向量IV合并,然后使用Base64编码。
     *
     * @param plaintext 需要加密的明文字符串。
     * @param keyBytes  用于加密的密钥字节数组。
     * @param iv        加密算法的初始化向量,用于增加加密数据的随机性和唯一性。
     * @return 返回加密后的数据与初始化向量合并,并经过Base64编码的字符串。
     * @throws Exception 如果加密过程中出现错误,则抛出异常。
     */
    public static String encryptCBC(String plaintext, byte[] keyBytes, byte[] iv) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_CBC);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        byte[] combined = new byte[iv.length + encryptedBytes.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length);
        return Base64.getEncoder().encodeToString(combined);
    }

    /**
     * 解密经过Base64编码的密文。
     *
     * @param ciphertextBase64 经过Base64编码的密文字符串。
     * @param keyBytes         用于解密的密钥字节数组。
     * @return 解密后的明文字符串。
     * @throws Exception 如果解密过程中发生错误,则抛出异常。
     */
    public static String decryptCBC(String ciphertextBase64, byte[] keyBytes) throws Exception {
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertextBase64);
        byte[] iv = new byte[16];
        byte[] encryptedBytes = new byte[decodedCiphertext.length - iv.length];
        System.arraycopy(decodedCiphertext, 0, iv, 0, iv.length);
        System.arraycopy(decodedCiphertext, iv.length, encryptedBytes, 0, encryptedBytes.length);
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_CBC);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));

        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 使用ECB模式对明文进行加密。
     *
     * @param plaintext 需要加密的明文字符串。
     * @param keyBytes  用于加密的密钥字节数组。
     * @return 加密后的字符串,使用Base64编码。
     * @throws Exception 如果加密过程中出现错误,则抛出异常。
     */
    public static String encryptECB(String plaintext, byte[] keyBytes) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);

        Cipher cipher = Cipher.getInstance(ALGORITHM_ECB);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);

        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 使用ECB模式解密密文。
     *
     * @param ciphertextBase64 经过Base64编码的密文字符串。
     * @param keyBytes         解密使用的密钥字节数组。
     * @return 解密后的明文字符串。
     * @throws Exception 如果解密过程中发生错误,则抛出异常。
     */
    public static String decryptECB(String ciphertextBase64, byte[] keyBytes) throws Exception {
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertextBase64);

        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_ECB);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);

        byte[] decryptedBytes = cipher.doFinal(decodedCiphertext);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 使用CFB模式对明文进行加密。
     *
     * @param plaintext 需要加密的明文字符串。
     * @param keyBytes 用于加密的密钥字节数组。
     * @param iv 初始化向量,用于CFB模式的加密过程。
     * @return 加密后的密文字符串,使用Base64编码。
     * @throws Exception 如果加密过程中出现错误,则抛出异常。
     */
    public static String encryptCFB(String plaintext, byte[] keyBytes, byte[] iv) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_CFB);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv), new SecureRandom());

        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 使用CFB模式解密经过Base64编码的密文。
     *
     * @param ciphertextBase64 经过Base64编码的密文字符串。
     * @param keyBytes 解密使用的密钥,为字节数组。
     * @param iv 初始化向量,用于CFB模式的解密过程,为字节数组。
     * @return 解密后的明文字符串。
     * @throws Exception 如果解密过程中出现任何错误,则抛出异常。
     */
    public static String decryptCFB(String ciphertextBase64, byte[] keyBytes, byte[] iv) throws Exception {
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertextBase64);

        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_CFB);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv), new SecureRandom());

        byte[] decryptedBytes = cipher.doFinal(decodedCiphertext);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 使用OFB模式加密明文
     *
     * @param plaintext 需要加密的明文字符串
     * @param keyBytes 用于加密的密钥字节数组
     * @param iv 初始化向量,用于OFB模式的加密过程
     * @return 加密后的字符串,使用Base64编码
     * @throws Exception 如果加密过程中出现错误,则抛出异常
     */
    public static String encryptOFB(String plaintext, byte[] keyBytes, byte[] iv) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_OFB);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv), new SecureRandom());

        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 使用OFB模式解密密文。
     *
     * @param ciphertextBase64 用Base64编码的密文字符串。
     * @param keyBytes 解密用的密钥字节数组。
     * @param iv 初始化向量,用于OFB模式的解密过程。
     * @return 解密后的明文字符串。
     * @throws Exception 如果解密过程中出现任何错误,则抛出异常。
     */
    public static String decryptOFB(String ciphertextBase64, byte[] keyBytes, byte[] iv) throws Exception {
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertextBase64);

        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_OFB);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv), new SecureRandom());

        byte[] decryptedBytes = cipher.doFinal(decodedCiphertext);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 使用CTR模式对明文进行加密。
     *
     * @param plaintext 需要加密的明文字符串。
     * @param keyBytes 用于加密的密钥字节数组。
     * @param iv 初始化向量,用于CTR模式加密过程。
     * @return 加密后的字符串,使用Base64编码。
     * @throws Exception 如果加密过程中出现错误,则抛出异常。
     */
    public static String encryptCTR(String plaintext, byte[] keyBytes, byte[] iv) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_CTR);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));

        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 使用CTR模式解密经过Base64编码的密文。
     *
     * @param ciphertextBase64 经过Base64编码的密文字符串。
     * @param keyBytes 解密使用的密钥,为字节数组。
     * @param iv 初始化向量,用于CTR模式的解密,为字节数组。
     * @return 解密后的明文字符串。
     * @throws Exception 如果解密过程中发生错误,则抛出异常。
     */
    public static String decryptCTR(String ciphertextBase64, byte[] keyBytes, byte[] iv) throws Exception {
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertextBase64);

        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_CTR);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));

        byte[] decryptedBytes = cipher.doFinal(decodedCiphertext);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 使用GCM模式加密明文。
     *
     * @param plaintext 需要加密的明文字符串。
     * @param keyBytes 用于加密的密钥字节数组。
     * @param iv 初始化向量,用于GCM模式的加密过程。
     * @return 返回加密后的密文字符串,使用Base64编码。
     * @throws Exception 如果加密过程中出现错误,则抛出异常。
     */
    public static String encryptGCM(String plaintext, byte[] keyBytes, byte[] iv) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_GCM);
        // 初始化Cipher,设置密钥和IV,以及标签长度
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
        // GCM模式下,doFinal返回的结果已经包含了密文和认证标签,不需要单独处理
        byte[] encryptedBytes =  cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 使用GCM模式解密密文。
     *
     * @param ciphertextWithTag 密文和认证标签的Base64编码字符串。解密时需要使用这个字符串来还原密文。
     * @param keyBytes 解密使用的密钥,为字节数组。
     * @param iv 初始化向量,用于GCM模式的解密过程,也是一个字节数组。
     * @return 解密后的明文字符串。
     * @throws Exception 如果解密过程中出现任何错误,则抛出异常。
     */
    public static String decryptGCM(String ciphertextWithTag, byte[] keyBytes, byte[] iv) throws Exception {
        byte[] decodedCiphertext = Base64.getDecoder().decode(ciphertextWithTag);
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM_GCM);
        // 初始化Cipher,设置密钥、IV以及从加密结果中提取的参数
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
        // 执行解密操作,直接传入包含密文和标签的数组
        byte[] decryptedBytes = cipher.doFinal(decodedCiphertext);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

}

以上代码包含了6种加密模式,加密模式后面都是建议的填充方式(NoPadding是不填充的意思),后面几种流加密的方式不需要填充字节了,实在需要填充自行修改就好,代码还是比较全的,有不少重复代码,笔者没有进行整合了,请读者朋友自行优化整合。

测试用例代码

package com.datastructures;

import com.crypto.AESUtils;
import org.junit.jupiter.api.Test;

/**
 * @author hulei
 * @date 2024/5/16 13:50
 */


public class AESUtilsTest {
    @Test
    public void testECB() throws Exception {
        System.out.println("ECB模式测试加密解密");
        //获取128位密钥
        byte[] keyBytes = AESUtils.generateKeyBytes(128);
        String originalText = "这是一个ECB测试明文";
        String encryptedText = AESUtils.encryptECB(originalText, keyBytes);
        System.out.println("Encrypted (ECB): " + encryptedText);
        String decryptedText1 = AESUtils.decryptECB(encryptedText, keyBytes);
        System.out.println("Decrypted (ECB): " + decryptedText1);
    }

    @Test
    public void testCBC() throws Exception {
        System.out.println("CBC模式测试加密解密");
        //获取256位密钥
        byte[] keyBytes = AESUtils.generateKeyBytes(192);
        //生成初始化向量
        byte[] iv = AESUtils.generateIV();
        String originalText = "这是一个CBC测试明文";
        String encryptedText = AESUtils.encryptCBC(originalText, keyBytes, iv);
        System.out.println("EncryptedByCBC: " + encryptedText);
        String decryptedText = AESUtils.decryptCBC(encryptedText, keyBytes);
        System.out.println("DecryptedByCBC: " + decryptedText);
    }

    @Test
    public void testCFB() throws Exception {
        System.out.println("CFB模式测试加密解密");
        //获取128位密钥
        byte[] keyBytes = AESUtils.generateKeyBytes(256);
        //生成初始化向量
        byte[] iv = AESUtils.generateIV();
        String originalText = "这是一个CFB测试明文";
        String encryptedText = AESUtils.encryptCFB(originalText, keyBytes, iv);
        System.out.println("EncryptedByCFB: " + encryptedText);
        String decryptedText = AESUtils.decryptCFB(encryptedText, keyBytes, iv);
        System.out.println("DecryptedByCFB: " + decryptedText);
    }

    @Test
    public void testOFB() throws Exception {
        System.out.println("OFB模式测试加密解密");
        //获取128位密钥
        byte[] keyBytes = AESUtils.generateKeyBytes(128);
        byte[] iv = AESUtils.generateIV();
        String originalText = "这是一个OFB测试明文";
        String encryptedText = AESUtils.encryptOFB(originalText, keyBytes, iv);
        System.out.println("EncryptedByOFB: " + encryptedText);
        String decryptedText = AESUtils.decryptOFB(encryptedText, keyBytes, iv);
        System.out.println("DecryptedByOFB: " + decryptedText);
    }

    @Test
    public void testCTR() throws Exception {
        System.out.println("CTR模式测试加密解密");
        //获取192位密钥
        byte[] keyBytes = AESUtils.generateKeyBytes(192);
        byte[] iv = AESUtils.generateIV();
        String originalText = "这是一个CTR测试明文";
        String encryptedText = AESUtils.encryptCTR(originalText, keyBytes, iv);
        System.out.println("EncryptedByCTR: " + encryptedText);
        String decryptedText = AESUtils.decryptCTR(encryptedText, keyBytes, iv);
        System.out.println("DecryptedByCTR: " + decryptedText);
    }

    @Test
    public void testGCM() throws Exception {
        System.out.println("GCM模式测试加密解密");
        //获取256位密钥
        byte[] keyBytes = AESUtils.generateKeyBytes(256);
        byte[] iv = AESUtils.generateIV();
        String originalText = "这是一个GCM测试明文";
        String encryptedText = AESUtils.encryptGCM(originalText, keyBytes, iv);
        System.out.println("EncryptedByGCM: " + encryptedText);
        String decryptedText = AESUtils.decryptGCM(encryptedText, keyBytes, iv);
        System.out.println("DecryptedByGCM: " + decryptedText);
    }
}

测试结果如下

ECB模式
在这里插入图片描述
CBC模式
在这里插入图片描述
CFB模式
在这里插入图片描述
OFB模式
在这里插入图片描述
CTR模式
在这里插入图片描述
GCM模式
在这里插入图片描述
测试用例的代码主要展示了几种加密模式的使用方法

    byte[] keyBytes = AESUtils.generateKeyBytes(192);
    byte[] iv = AESUtils.generateIV();

密钥和iv初始向量,密钥是每种加密模式都必须的,iv则是有的不需要,有的需要。实际开发中,这两个一般是自己生成保存好,或是需要和第三方对接通过网络传输的,则最好加密下再传输,保证安全,使用base64编码后再传输。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值