You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用AES/GCM/NoPadding动态IV加密Postgres数据的查询问题

问题解决方案:随机IV下的加密数据查询问题

核心问题分析

原逻辑用固定IV时,相同明文加密后密文一致,可直接通过加密后的明文匹配数据库记录。改为随机IV后,每次加密生成的密文不同,无法匹配数据库中存储的{Base64编码IV}密文格式数据,导致查询失败。

可行解决方案

方案1:新增哈希索引字段(推荐)

  • 在用户表中新增字段(如email_hash),存储明文邮箱的加盐哈希值(优先选择SHA-256等强哈希算法)。
  • 查询流程:接收输入邮箱后,计算其加盐哈希值,用哈希值查询数据库获取对应记录,再解密记录中的邮箱字段做最终验证(规避哈希碰撞风险)。
  • 优势:不破坏现有加密存储逻辑,查询效率高,安全性有保障。
  • 注意事项:盐值需从配置文件读取,禁止硬编码,避免彩虹表攻击。

方案2:数据库端解密查询(不推荐)

  • 借助PostgreSQL的pgcrypto扩展,在数据库层实现解密逻辑:
    1. 创建自定义函数,解析{IV}密文格式字符串,提取IV和密文后执行AES-GCM解密。
    2. 查询时调用该函数匹配明文邮箱。
  • 劣势:增加数据库计算负载,密钥需在数据库端配置,存在泄露风险,不适用于高并发场景。

方案3:预存多版本加密值(不推荐)

  • 对需查询字段提前用历史IV加密并存储,完全违背随机IV的安全设计初衷,扩展性极差,不建议采用。

代码调整示例(方案1实现参考)

1. 新增哈希工具类

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import org.apache.commons.lang3.StringUtils;

public class HashUtils {
    private static final String SALT = "your-configured-salt-value"; // 从配置中心读取
    private static final String HASH_ALGORITHM = "SHA-256";

    private HashUtils() throws IllegalAccessException {
        throw new IllegalAccessException("HashUtils mustn't be instantiated");
    }

    public static String generateSaltedHash(String input) {
        if (StringUtils.isBlank(input)) {
            return input;
        }
        try {
            MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
            byte[] hashBytes = digest.digest((input + SALT).getBytes());
            return Base64.getEncoder().encodeToString(hashBytes);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Failed to generate salted hash", e);
        }
    }
}

2. 数据入库流程调整

用户数据保存时,同时存储:

  • 加密后的邮箱({Base64 IV}密文格式)
  • 邮箱的加盐哈希值(调用HashUtils.generateSaltedHash(email)生成)

3. 查询流程调整

// 接收前端传入的邮箱参数
String inputEmail = request.getPathVariable("email");
// 计算加盐哈希值
String emailHash = HashUtils.generateSaltedHash(inputEmail);
// 用哈希值查询数据库
User user = userRepository.findByEmailHash(emailHash);
// 验证解密后的邮箱是否匹配
if (user != null) {
    String decryptedEmail = CipherUtils.decrypt(user.getEncryptedEmail());
    if (inputEmail.equals(decryptedEmail)) {
        // 返回用户数据
    } else {
        // 哈希碰撞,返回无结果
    }
}

随机IV加密的正确实现(补充)

原加密/解密方法需调整为支持随机IV:

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
import org.apache.commons.lang3.StringUtils;

public class CipherUtils {

    private CipherUtils() throws IllegalAccessException {
        throw new IllegalAccessException("CipherUtil mustn't be instantiated");
    }
    private static final MyLogger logger = MyLogManager.getLogger(CipherUtils.class);
    private static final SecretKeySpec SECRET_KEY;
    private static final String AES_ALG = "AES";
    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
    private static final String ENCRYPTION_KEY = "d3BhcKLHLKhbsddffbG9jYasdfadsJ4";
    private static final int GCM_TAG_LENGTH = 128;
    private static final int IV_LENGTH = 12; // GCM推荐12字节IV

    static {
        SECRET_KEY = new SecretKeySpec(ENCRYPTION_KEY.getBytes(), AES_ALG);
    }

    public static String encrypt(final String strToEncrypt) {
        if (StringUtils.isBlank(strToEncrypt)) {
            return strToEncrypt;
        }
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            // 生成随机IV
            byte[] iv = new byte[IV_LENGTH];
            new SecureRandom().nextBytes(iv);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            
            cipher.init(Cipher.ENCRYPT_MODE, SECRET_KEY, gcmSpec);
            byte[] encryptedBytes = cipher.doFinal(strToEncrypt.getBytes());
            
            // 拼接IV和密文
            String ivBase64 = Base64.getEncoder().encodeToString(iv);
            String encryptedBase64 = Base64.getEncoder().encodeToString(encryptedBytes);
            return String.format("{%s}%s", ivBase64, encryptedBase64);
        } catch (Exception e) {
            logger.error(ENCRYPTION_ERROR_MSG, e).log();
            throw new EncryptDecryptException(e.getMessage());
        }
    }

    public static String decrypt(final String strToDecrypt) {
        if (StringUtils.isBlank(strToDecrypt)) {
            return strToDecrypt;
        }
        try {
            // 解析IV和密文
            int ivEndIdx = strToDecrypt.indexOf('}');
            String ivBase64 = strToDecrypt.substring(1, ivEndIdx);
            String encryptedBase64 = strToDecrypt.substring(ivEndIdx + 1);
            
            byte[] iv = Base64.getDecoder().decode(ivBase64);
            byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);
            
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.DECRYPT_MODE, SECRET_KEY, gcmSpec);
            
            return new String(cipher.doFinal(encryptedBytes));
        } catch (Exception e) {
            logger.error(DECRYPTION_ERROR_MSG, e).log();
            throw new EncryptDecryptException(e.getMessage(), null);
        }
    }
}

内容的提问来源于stack exchange,提问作者Vijay Kambala

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 16:14:58