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

Java Spring Security AES/GCM加密密文在C# BouncyCastle中无法解密的问题排查与修复

Java Spring Security AES/GCM加密密文在C# BouncyCastle中无法解密的问题排查与修复

你是不是遇到了这样的场景:用Java Spring Security的BouncyCastleAesGcmBytesEncryptor加密了文本,现在要在C#(.NET Core 3.1)里用BouncyCastle解密,却总是失败——要么明文乱码,要么直接抛出异常?别慌,我们一步步来排查问题,给出修复方案。


一、先看你的Java端加密实现(原版本)

你用Spring Security结合Bouncy Castle实现了AES-GCM加密,生成256位AES密钥,加密后输出Base64格式的密文。先把你的代码和输出贴出来:

Java加密代码

package com.stackoflow.symmetric_token_decryptor;

import static org.apache.commons.codec.binary.Hex.encodeHex;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.security.crypto.encrypt.BouncyCastleAesGcmBytesEncryptor;
import org.springframework.security.crypto.encrypt.BytesEncryptor;

public class App {
    public static void main( String[] args ) throws Exception {
        String generatedKey = generateSymmetricKey();
        System.out.println("generatedKey = " + generatedKey);
        String encryptedToken = encrypt("abc", generatedKey);
        System.out.println("encryptedToken = " + encryptedToken);
        String plainToken = decryptToken(encryptedToken, generatedKey);
        System.out.println(plainToken);
    }

    private static String generateSymmetricKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        Key key = keyGen.generateKey();
        return new String(encodeHex(key.getEncoded()));
    }

    private static String encrypt(String text, String secretKey) {
        BytesEncryptor encryptor = new BouncyCastleAesGcmBytesEncryptor("", secretKey);
        return new String(Base64.encode(encryptor.encrypt(text.getBytes())), StandardCharsets.UTF_8);
    }

    private static String decryptToken(String encryptedToken, String generatedKey) {
        BytesEncryptor encryptor = new BouncyCastleAesGcmBytesEncryptor("", generatedKey);
        return new String(encryptor.decrypt(Base64.decode(encryptedToken.getBytes())), StandardCharsets.UTF_8);
    }
}

Java输出

generatedKey = e4deeabbdfbf504f5a980bb7e4ae6b8e9cb8896cd5e64692e162a9691449c196
encryptedToken = 52nvbvyz2mEYBIb+grW5SNXplUufgeTtlVVOVOmhqRFvmeg=

Maven依赖(pom.xml)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.stackoflow</groupId>
    <artifactId>symmetric-token-decryptor</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>symmetric-token-decryptor</name>
    <url>http://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>17</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-crypto</artifactId>
            <version>5.8.10</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <version>1.80</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.18.0</version>
        </dependency>
    </dependencies>
</project>

二、你的C#端原解密代码(问题版本)

你尝试用C#的BouncyCastle库解密,但可能遇到解密失败的情况,先看你的原代码:

using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Linq;
using System.Text;

namespace Symmetric_Decryption_AES_GCM_C_
{
    class Program
    {
        static void Main(string[] args)
        {
            String encryptedToken = "52nvbvyz2mEYBIb+grW5SNXplUufgeTtlVVOVOmhqRFvmeg=";
            String secretKey = "e4deeabbdfbf504f5a980bb7e4ae6b8e9cb8896cd5e64692e162a9691449c196";
            Console.WriteLine(DecryptPayload(encryptedToken, secretKey));
        }

        private static string DecryptPayload(string base64EncryptedToken, string stringKey)
        {
            Console.WriteLine("----C# Decryption Details----");
            byte[] encryptedToken = Convert.FromBase64String(base64EncryptedToken);
            byte[] key = Enumerable.Range(0, stringKey.Length / 2)
                .Select(i => Convert.ToByte(stringKey.Substring(i * 2, 2), 16))
                .ToArray();
            int ivLength = 16;
            int ciphertextTagLength = encryptedToken.Length - ivLength;
            byte[] iv = new byte[ivLength];
            byte[] ciphertextTag = new byte[ciphertextTagLength];
            Buffer.BlockCopy(encryptedToken, 0, iv, 0, ivLength);
            Buffer.BlockCopy(encryptedToken, ivLength, ciphertextTag, 0, ciphertextTagLength);
            return DecryptWithGCM(ciphertextTag, key, iv);
        }

        private static string DecryptWithGCM(byte[] ciphertextTag, byte[] key, byte[] nonce)
        {
            var cipher = new GcmBlockCipher(new AesEngine());
            cipher.Init(false, new AeadParameters(new KeyParameter(key), 128, nonce, null));
            int outputSizeDecryptedData = cipher.GetOutputSize(ciphertextTag.Length);
            byte[] decryptedBytes = new byte[outputSizeDecryptedData];
            // 原代码此处缺少完整的解密流程调用
            return Encoding.UTF8.GetString(decryptedBytes);
        }
    }
}

三、问题根源排查

为什么解密会失败?核心有两个问题:

1. 最关键:Java代码误用了加密器构造函数

你以为BouncyCastleAesGcmBytesEncryptor("", secretKey)是直接用secretKey作为AES密钥,但实际上:
Spring的这个构造函数接受的是密码(password),不是直接的AES密钥!它内部会通过PBKDF2WithHmacSHA256算法,用空盐、10000次迭代、生成256位密钥,重新推导一个AES密钥来加密。

而你在C#里直接把secretKey的Hex字符串解码成AES密钥,两边的密钥完全不匹配,解密自然失败。

2. C#解密方法实现不完整

原C#代码的DecryptWithGCM方法缺少ProcessBytesDoFinal的调用,没有完成实际的解密和Tag验证流程。


四、修复方案

我们提供两个方案,优先推荐方案一(更符合你“生成对称密钥直接加密”的初衷),方案二用于无法修改Java代码的兼容场景。

方案一:修正Java代码,直接使用AES密钥(推荐)

修改Java代码,让它直接用生成的AES密钥加密,跳过不必要的PBKDF2推导:

修改后的Java加密代码

package com.stackoflow.symmetric_token_decryptor;

import static org.apache.commons.codec.binary.Hex.encodeHex;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.util.encoders.Base64;
import org.springframework.security.crypto.encrypt.BouncyCastleAesGcmBytesEncryptor;
import org.springframework.security.crypto.encrypt.BytesEncryptor;
import org.springframework.security.crypto.encrypt.KeyParameter;

public class App {
    public static void main( String[] args ) throws Exception {
        String generatedKey = generateSymmetricKey();
        System.out.println("generatedKey = " + generatedKey);
        String encryptedToken = encrypt("abc", generatedKey);
        System.out.println("encryptedToken = " + encryptedToken);
        String plainToken = decryptToken(encryptedToken, generatedKey);
        System.out.println(plainToken);
    }

    private static String generateSymmetricKey() throws NoSuchAlgorithmException {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        Key key = keyGen.generateKey();
        return new String(encodeHex(key.getEncoded()));
    }

    private static String encrypt(String text, String secretKeyHex) throws DecoderException {
        // 直接将Hex密钥解码为字节数组,作为AES密钥使用
        byte[] keyBytes = Hex.decodeHex(secretKeyHex);
        BytesEncryptor encryptor = new BouncyCastleAesGcmBytesEncryptor(new KeyParameter(keyBytes));
        return new String(Base64.encode(encryptor.encrypt(text.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
    }

    private static String decryptToken(String encryptedToken, String secretKeyHex) throws DecoderException {
        byte[] keyBytes = Hex.decodeHex(secretKeyHex);
        BytesEncryptor encryptor = new BouncyCastleAesGcmBytesEncryptor(new KeyParameter(keyBytes));
        byte[] decryptedBytes = encryptor.decrypt(Base64.decode(encryptedToken.getBytes(StandardCharsets.UTF_8)));
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }
}

适配的C#解密代码(修复后)

现在C#可以直接用密钥Hex解码的方式,同时完善解密流程:

using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Linq;
using System.Text;

namespace Symmetric_Decryption_AES_GCM_C_
{
    class Program
    {
        static void Main(string[] args)
        {
            // 替换为修改后Java代码输出的密文和密钥
            String encryptedToken = "xxx";
            String secretKey = "xxx";
            try
            {
                string plainText = DecryptPayload(encryptedToken, secretKey);
                Console.WriteLine("解密成功,明文:" + plainText);
            }
            catch (Exception ex)
            {
                Console.WriteLine("解密失败:" + ex.Message);
            }
        }

        private static string DecryptPayload(string base64EncryptedToken, string stringKey)
        {
            Console.WriteLine("----C# Decryption Details----");
            // 1. Base64解码密文
            byte[] encryptedData = Convert.FromBase64String(base64EncryptedToken);
            
            // 2. Hex解码AES密钥
            byte[] key = Enumerable.Range(0, stringKey.Length / 2)
                .Select(i => Convert.ToByte(stringKey.Substring(i * 2, 2), 16))
                .ToArray();
            
            // 3. 拆分IV、密文+Tag:Spring加密器输出格式为 [IV(16字节)] + [密文] + [GCM Tag(16字节)]
            int ivLength = 16;
            int tagLength = 16;
            if (encryptedData.Length < ivLength + tagLength)
            {
                throw new ArgumentException("密文长度无效");
            }
            
            byte[] iv = new byte[ivLength];
            byte[] ciphertextWithTag = new byte[encryptedData.Length - ivLength];
            
            Buffer.BlockCopy(encryptedData, 0, iv, 0, ivLength);
            Buffer.BlockCopy(encryptedData, ivLength, ciphertextWithTag, 0, ciphertextWithTag.Length);
            
            // 4. 执行GCM解密
            return DecryptWithGCM(ciphertextWithTag, key, iv);
        }

        private static string DecryptWithGCM(byte[] ciphertextTag, byte[] key, byte[] nonce)
        {
            var cipher = new GcmBlockCipher(new AesEngine());
            var parameters = new AeadParameters(new KeyParameter(key), 128, nonce, null);
            cipher.Init(false, parameters);
            
            byte[] decryptedBytes = new byte[cipher.GetOutputSize(ciphertextTag.Length)];
            int processedBytes = cipher.ProcessBytes(ciphertextTag, 0, ciphertextTag.Length, decryptedBytes, 0);
            // 完成解密并验证GCM Tag
            cipher.DoFinal(decryptedBytes, processedBytes);
            
            return Encoding.UTF8.GetString(decryptedBytes);
        }
    }
}

方案二:兼容现有Java代码(不修改Java,调整C#)

如果无法修改Java代码,那C#必须严格复刻Spring的PBKDF2密钥推导逻辑,再解密:

完整的C#兼容代码

using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace Symmetric_Decryption_AES_GCM_C_
{
    class Program
    {
        static void Main(string[] args)
        {
            String encryptedToken = "52nvbvyz2mEYBIb+grW5SNXplUufgeTtlVVOVOmhqRFvmeg=";
            String secretKey = "e4deeabbdfbf504f5a980bb7e4ae6b8e9cb8896cd5e64692e162a9691449c196";
            try
            {
                string plainText = DecryptPayload(encryptedToken, secretKey);
                Console.WriteLine("解密成功,明文:" + plainText);
            }
            catch (Exception ex)
            {
                Console.WriteLine("解密失败:" + ex.Message);
            }
        }

        private static string DecryptPayload(string base64EncryptedToken, string password)
        {
            Console.WriteLine("----C# Decryption Details----");
            // 1. Base64解码密文
            byte[] encryptedData = Convert.FromBase64String(base64EncryptedToken);
            
            // 2. 复刻Spring的PBKDF2密钥推导逻辑
            byte[] salt = new byte[0]; // 对应Java的空盐字符串
            int iterationCount = 10000; // Spring默认迭代次数
            int keyLength = 32; // 256位密钥
            byte[] key;
            using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA256))
            {
                key = pbkdf2.GetBytes(keyLength);
            }
            
            // 3. 拆分IV、密文+Tag
            int ivLength = 16;
            int tagLength = 16;
            if (encryptedData.Length < ivLength + tagLength)
            {
                throw new ArgumentException("密文长度无效");
            }
            
            byte[] iv = new byte[ivLength];
            byte[] ciphertextWithTag = new byte[encryptedData.Length - ivLength];
            
            Buffer.BlockCopy(encryptedData, 0, iv, 0, ivLength);
            Buffer.BlockCopy(encryptedData, ivLength, ciphertextWithTag, 0, ciphertextWithTag.Length);
            
            // 4. GCM解密
            return DecryptWithGCM(ciphertextWithTag, key, iv);
        }

        private static string DecryptWithGCM(byte[] ciphertextTag, byte[] key, byte[] nonce)
        {
            var cipher = new

火山引擎 最新活动