AES解密报错:GCM模式MAC校验失败(C#与C++跨语言适配)
解决C#端解密BCRYPT加密GCM密文时的MAC校验失败问题
问题背景
使用C#的bc-fips-1.0.2解密C++通过BCRYPT.h加密的AES-GCM密文时,抛出Org.BouncyCastle.Crypto.InvalidCipherTextException,提示MAC check in GCM failed。已确认密钥、IV、AAD一致,但问题仍存在。
核心问题排查与修复
1. C++端加密数据长度与输出缓冲区不匹配
C++代码中,加密的是origData2(字符串"test",包含末尾\0,sizeof(origData2)为5),但输出缓冲区encrypted按AES块长(16字节)分配,且最终把整个16字节缓冲区Base64编码传给C#。这导致C#端解密时处理了额外的随机填充字节,破坏MAC校验。
- 修复:加密后仅对
BCryptEncrypt返回的实际写入长度(ulWritten)的字节进行Base64编码,而非整个缓冲区长度。
2. C++端未使用AAD但C#端传入了AAD
C++代码的BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO未设置pbAuthData和cbAuthData,说明加密时未使用AAD,但C#端却传入了aadAuthData。GCM模式中AAD参与MAC计算,两端必须完全一致(要么都传,要么都不传)。
- 修复:要么C++端加密时添加AAD参数,要么C#端移除AAD写入逻辑。
3. MAC标签传递与长度一致性
C端使用authTagLengths.dwMinLength(通常为96位/12字节),C#端设置macBitSize = 96是正确的,但需确保C端完整传递标签数据,且C#端使用的标签长度与加密端完全匹配。
4. BCRYPT缓冲区未初始化的干扰
C++代码中encrypted通过HeapAlloc分配但未初始化,未被加密覆盖的字节是随机值,这些随机值被一起Base64传给C#,导致解密数据错误。必须仅传递实际加密后的字节。
修正后的关键代码示例
C++端修正后的加密逻辑片段
std::vector<BYTE> encrypted(origDataSize2); ULONG ulWritten = 0; { BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo; BCRYPT_INIT_AUTH_MODE_INFO(authInfo); authInfo.pbNonce = (PUCHAR)&origNonce[0]; authInfo.cbNonce = origNonce.size(); authInfo.pbTag = &authTag[0]; authInfo.cbTag = authTag.size(); // 若需添加AAD,取消以下两行注释并传入对应数据 // authInfo.pbAuthData = (PUCHAR)aadBuffer; // authInfo.cbAuthData = aadBufferLength; status = BCryptEncrypt ( hKeyAes, origData2, origDataSize2, &authInfo, 0, 0, &encrypted[0], encrypted.size(), &ulWritten, 0 ); } // 仅编码实际加密的字节数 std::string encryptedStringUsedAtCSharpSide = base64_encode_new(&encrypted[0], ulWritten);
C#端修正后的解密逻辑片段(移除不必要的AAD)
private static byte[] AesDecrypt() { byte[] aesKey = Convert.FromBase64String("KSO+hOFs1q5SkEnx8bvp6w=="); byte[] iv = Convert.FromBase64String("s6bbPIcMPpkkXg0c"); byte[] encryptedData = Convert.FromBase64String("q/KR75wAAAAAAAAAAAAAAA=="); int macBitSize = 96; // 与C++端标签长度一致 byte[] decryptedMessage = null; try { CryptoServicesRegistrar.SetApprovedOnlyMode(true); var key = new FipsAes.Key(aesKey); IAeadCipherService cipherService = CryptoServicesRegistrar.CreateService(key); var algorithmDetails = FipsAes.Gcm.WithIV(iv).WithMacSize(macBitSize); var aeadDecryptorBuilder = cipherService.CreateAeadDecryptorBuilder(algorithmDetails); // 注意:若C++端分开传递密文和标签,需将标签附加到密文末尾后再传入 var decryptor = aeadDecryptorBuilder.BuildAeadCipher(AeadUsage.NO_AAD, new MemoryInputStream(encryptedData)); // 移除AAD写入(C++端未使用AAD) // decryptor.AadStream.Write(aadAuthData, 0, aadAuthData.Length); using (var stream = decryptor.Stream) { decryptedMessage = Streams.ReadAll(stream); } } catch { throw; } return decryptedMessage; }
额外注意事项
- 密文与标签的拼接规则:BCRYPT默认生成独立的密文和标签,而BouncyCastle的GCM实现通常期望密文在前、标签在后拼接成一个字节数组。若C++端分开传递两者,C#端需手动拼接后再解密。
- Base64编码一致性:确保两端使用相同的Base64标准(如是否保留填充符
=、是否使用URL安全编码)。 - 明文与密文长度匹配:AES-GCM是流加密,加密后长度等于明文长度,无需额外填充,禁止传递多余的缓冲区字节。
内容的提问来源于stack exchange,提问作者kaushik
相关产品推荐
相关产品推荐

