AES GCM模式下BouncyCastle二次解密失败问题求助
AES-GCM加解密对接C系统的IV问题解决
问题背景
使用BouncyCastle实现AES-GCM加解密逻辑,对接C语言开发的第三方系统。首次加解密基于第三方提供的会话密钥与IV可正常执行,但二次加解密失败,推测因重复使用同一IV导致。需解决新IV的生成逻辑及传递方案(必须传递IV,否则对方无法完成解密)。
当前Java加解密代码
public byte[] encrypt(byte[] plainBytes) { byte[] encryptedBytes = new byte[0]; try { GCMBlockCipher cipher = new GCMBlockCipher(new AESFastEngine()); AEADParameters parameters = new AEADParameters(new KeyParameter(key), MacBitSize, iv); cipher.init(true, parameters); encryptedBytes = new byte[cipher.getOutputSize(plainBytes.length)]; int retLen = cipher.processBytes (plainBytes, 0, plainBytes.length, encryptedBytes, 0); cipher.doFinal(encryptedBytes, retLen); } catch (IllegalArgumentException | IllegalStateException | DataLengthException | InvalidCipherTextException ex) { System.out.println(ex.getMessage()); } System.arraycopy(aad, 0, encryptedBytes, plainBytes.length, 16); byte[] final_encr = new byte[plainBytes.length]; System.arraycopy(encryptedBytes, 0, final_encr, 0, plainBytes.length); return final_encr; } public byte[] decrypt(byte[] encryptedBytes) { byte[] plainBytes = new byte[0]; try { GCMBlockCipher cipher = new GCMBlockCipher(new AESFastEngine()); AEADParameters parameters = new AEADParameters(new KeyParameter(key), MacBitSize, iv); cipher.init(false, parameters); cipher.processAADBytes(aad, 0, aad.length); plainBytes = new byte[encryptedBytes.length]; int retLen = cipher.processBytes (encryptedBytes, 0, encryptedBytes.length, plainBytes, 0); retLen += cipher.processBytes(aad, 0, aad.length, plainBytes, retLen); retLen += cipher.doFinal(plainBytes, retLen); } catch (IllegalArgumentException | IllegalStateException | DataLengthException | InvalidCipherTextException ex) { System.out.println(ex.getMessage()); } return plainBytes; }
已尝试的无效方案
- 修改IV末尾四位并递增
- 采用C JNI库实现
- 使用kalium、JCE等其他库
第三方C语言加解密实现(基于OpenSSL)
第三方文档中的加解密方法
对称加解密方法说明:
加密
初始化
void encrypt_EVP_aes_256_cbc_init(EVP_CIPHER_CTX ** ctx, unsigned char * key, unsigned char * iv) { if (!( * ctx = EVP_CIPHER_CTX_new())) handleErrors(); if (1 != EVP_EncryptInit_ex( * ctx, EVP_aes_256_gcm(), NULL, key, iv)) handleErrors(); }
加密逻辑
void encrypt(EVP_CIPHER_CTX * ctx, unsigned char * plaintext, int plaintext_len, unsigned char * ciphertext, int * ciphertext_len) { int len; if (1 != EVP_EncryptUpdate(ctx, ciphertext, & len, plaintext, plaintext_len)) handleErrors(); * ciphertext_len = len; }
解密
初始化
void decrypt_EVP_aes_256_cbc_init(EVP_CIPHER_CTX ** ctx, unsigned char * key, unsigned char * iv) { if (!( * ctx = EVP_CIPHER_CTX_new())) handleErrors(); if (1 != EVP_DecryptInit_ex( * ctx, EVP_aes_256_gcm(), NULL, key, iv)) handleErrors(); }
解密逻辑
int decrypt(EVP_CIPHER_CTX * ctx, unsigned char * ciphertext, int ciphertext_len, unsigned char * plaintext, int * plaintext_len) { int len; if (1 != EVP_DecryptUpdate(ctx, plaintext, & len, ciphertext, ciphertext_len)) handleErrors(); * plaintext_len = len; }
说明
- 加粗部分为OpenSSL库函数
- plaintext为实际消息缓冲区
- ciphertext为加密消息缓冲区
解决方案
1. 合规的IV生成逻辑
AES-GCM要求同一密钥下的IV必须完全唯一,绝对不能重复。推荐两种标准生成方式:
- 随机IV:生成12字节(96位)的随机字节数组,这是NIST推荐的GCM最优IV长度,安全性最高。代码实现:
SecureRandom secureRandom = new SecureRandom(); byte[] newIv = new byte[12]; // 12字节=96位,GCM标准推荐长度 secureRandom.nextBytes(newIv); - 计数器IV:若需要有序IV,可基于初始IV维护一个全局计数器,每次加密后递增(需确保密钥有效期内计数器不会溢出)。注意计数器需采用大端模式,长度建议为12字节。
2. IV的传递方案
解密方必须获取对应IV才能解密,常用两种传递方式:
- 前置IV到密文中:加密完成后,将IV拼接在密文最前端,最终传输内容格式为:
[IV(12字节)] + [密文+GCM标签]。接收方先读取前12字节作为IV,剩余部分作为密文+标签进行解密。修改后的加密和解密代码示例:
解密代码:public byte[] encrypt(byte[] plainBytes) { try { // 生成新IV SecureRandom secureRandom = new SecureRandom(); byte[] newIv = new byte[12]; secureRandom.nextBytes(newIv); GCMBlockCipher cipher = new GCMBlockCipher(new AESFastEngine()); // 传入AAD参数(若第三方需要AAD需提前约定) AEADParameters parameters = new AEADParameters(new KeyParameter(key), 128, newIv, aad); cipher.init(true, parameters); int outputSize = cipher.getOutputSize(plainBytes.length); byte[] cipherWithTag = new byte[outputSize]; int retLen = cipher.processBytes(plainBytes, 0, plainBytes.length, cipherWithTag, 0); cipher.doFinal(cipherWithTag, retLen); // 拼接IV + 密文+标签 byte[] finalEncrypted = new byte[newIv.length + cipherWithTag.length]; System.arraycopy(newIv, 0, finalEncrypted, 0, newIv.length); System.arraycopy(cipherWithTag, 0, finalEncrypted, newIv.length, cipherWithTag.length); return finalEncrypted; } catch (Exception ex) { System.out.println(ex.getMessage()); return new byte[0]; } }public byte[] decrypt(byte[] encryptedBytes) { if (encryptedBytes.length < 12) { return new byte[0]; } // 拆分IV byte[] receivedIv = new byte[12]; System.arraycopy(encryptedBytes, 0, receivedIv, 0, 12); byte[] cipherWithTag = new byte[encryptedBytes.length - 12]; System.arraycopy(encryptedBytes, 12, cipherWithTag, 0, cipherWithTag.length); try { GCMBlockCipher cipher = new GCMBlockCipher(new AESFastEngine()); AEADParameters parameters = new AEADParameters(new KeyParameter(key), 128, receivedIv, aad); cipher.init(false, parameters); // 处理AAD(需和第三方保持一致) cipher.processAADBytes(aad, 0, aad.length); byte[] plainBytes = new byte[cipher.getOutputSize(cipherWithTag.length)]; int retLen = cipher.processBytes(cipherWithTag, 0, cipherWithTag.length, plainBytes, 0); cipher.doFinal(plainBytes, retLen); return plainBytes; } catch (Exception ex) { System.out.println(ex.getMessage()); return new byte[0]; } } - 独立字段传递:如果对接协议中有单独的IV字段,可将生成的IV存入该字段,与密文一同发送给第三方。
3. 额外注意事项
- 标签长度:GCM标签建议使用128位(16字节),与OpenSSL默认值一致,确保双方兼容。
- AAD处理:原代码中错误地将AAD拼接在密文中后又截断,正确做法是通过
processAADBytes在加密前传入,若第三方需使用AAD,需提前约定传递方式。 - 密文完整性:原加密代码截断了GCM标签(仅返回明文长度的内容),导致解密无法验证完整性,这也是二次解密失败的可能原因,必须保留完整的密文+标签。
内容的提问来源于stack exchange,提问作者Suraj Putta
相关产品推荐
相关产品推荐

