You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Java解密OpenSSL AES-256-CTR加密文件失败,求排查方案

解决OpenSSL AES-256-CTR加密文件的Java解密哈希不一致问题

你猜的没错,密钥问题确实是核心原因!你的代码里有几个关键错误,导致生成的密钥、IV和OpenSSL加密时用的不匹配,最终解密出的文件哈希和原文件不一致。我来帮你逐一梳理并修正:

核心问题:密钥派生函数不匹配

OpenSSL的enc命令在使用-pass stdin时,默认不会使用PBKDF2,而是用它自己的EVP_BytesToKey算法来派生密钥和IV。你代码里用的PBKDF2WithHmacSHA1和OpenSSL的默认逻辑完全不同,这直接导致生成的密钥和加密时的密钥不一致。

另外,还有几个次要但同样致命的错误:

  • CTR模式是流加密,不需要PKCS5填充,你指定的AES/CTR/PKCS5PADDING会导致解密时额外添加填充字节,破坏文件完整性。
  • IV不能硬编码为全0,OpenSSL会通过EVP_BytesToKey一起派生密钥和IV,CTR模式下IV(准确说是nonce+计数器)必须和加密时一致。
  • 你读取salt的逻辑虽然最终拿到了正确的salt,但写法有问题,容易混淆(先读了"Salted__"标记覆盖salt数组,再读真正的salt覆盖回去)。

修正后的Java实现

首先,我们需要实现OpenSSL的EVP_BytesToKey算法(默认用MD5哈希,迭代次数1),然后修正解密流程:

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class OpenSSLAESDecryptor {

    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, Exception {
        File encryptedFile = new File("/tmp/encrypted.zip.enc");
        File decryptedFile = new File("/tmp/decrypted.zip");
        char[] password = "myPassphrase".toCharArray();

        try (FileInputStream fis = new FileInputStream(encryptedFile);
             FileOutputStream fos = new FileOutputStream(decryptedFile)) {

            // 读取并验证OpenSSL的加密文件标记:"Salted__"
            byte[] header = new byte[8];
            int read = fis.read(header);
            if (read != 8 || !new String(header).equals("Salted__")) {
                throw new IllegalArgumentException("不是OpenSSL enc加密的文件");
            }

            // 读取8字节salt
            byte[] salt = new byte[8];
            fis.read(salt);

            // 使用EVP_BytesToKey派生32字节AES密钥和16字节IV
            byte[] keyAndIv = evpBytesToKey(32, 16, MessageDigest.getInstance("MD5"), password, salt, 1);
            byte[] key = new byte[32];
            byte[] iv = new byte[16];
            System.arraycopy(keyAndIv, 0, key, 0, 32);
            System.arraycopy(keyAndIv, 32, iv, 0, 16);

            // 初始化CTR模式的Cipher,注意用NoPadding
            SecretKey secretKey = new SecretKeySpec(key, "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(iv);
            Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);

            // 解密并写入文件
            try (CipherInputStream cis = new CipherInputStream(fis, cipher)) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = cis.read(buffer)) != -1) {
                    fos.write(buffer, 0, len);
                }
            }
        }
    }

    // 实现OpenSSL的EVP_BytesToKey算法
    private static byte[] evpBytesToKey(int keyLen, int ivLen, MessageDigest md, char[] password, byte[] salt, int iterations) {
        byte[] passwordBytes = new String(password).getBytes();
        byte[] result = new byte[keyLen + ivLen];
        int resultIndex = 0;
        byte[] hash = null;

        while (resultIndex < keyLen + ivLen) {
            md.reset();
            if (hash != null) {
                md.update(hash);
            }
            md.update(passwordBytes);
            md.update(salt);
            hash = md.digest();

            for (int i = 1; i < iterations; i++) {
                md.reset();
                md.update(hash);
                hash = md.digest();
            }

            int copyLen = Math.min(result.length - resultIndex, hash.length);
            System.arraycopy(hash, 0, result, resultIndex, copyLen);
            resultIndex += copyLen;
        }
        return result;
    }
}

关键修正点说明

  1. EVP_BytesToKey实现
    • OpenSSL默认用MD5作为哈希函数,迭代次数为1,我们的实现完全匹配这个逻辑,派生的32字节密钥和16字节IV和加密时完全一致。
  2. CTR模式无填充
    • 改用AES/CTR/NoPadding,避免流加密模式下不必要的填充操作导致文件损坏。
  3. 正确读取文件头和salt
    • 单独用header数组读取"Salted__"标记,避免和salt数组混淆,同时增加了文件合法性校验。
  4. 正确派生IV
    • IV不再硬编码,而是从EVP_BytesToKey的结果中提取,保证和加密时的IV一致。

现在运行修正后的代码,解密后的文件哈希应该和原文件完全一致了。

内容的提问来源于stack exchange,提问作者Norbert Koch

火山引擎 最新活动