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方法缺少ProcessBytes和DoFinal的调用,没有完成实际的解密和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




