bcgit / bc-csharp

BouncyCastle.NET Cryptography Library (Mirror)
https://www.bouncycastle.org/csharp
MIT License
1.68k stars 558 forks source link

The AES256_GCM encryption algorithm is inconsistent with the data encrypted by NodeJS. #494

Open China-xiaoFang opened 1 year ago

China-xiaoFang commented 1 year ago
  1. When I use NodeJS, I try to encrypt the data with AES256_GCM, set the GCM additional authentication data AAD, and obtain the authTag at the end.

Below is my NodeJS code.


// Convert request data to JSON
const req_data_json = JSON.stringify(req_data);

//Assemble additional authentication data aad
const aes_aad = url + "|" + appid + "|" + timestamp + "|" + aes_sn;

// Convert key to Byte
const aes_real_key = Buffer.from(aes_key, "base64");

// Convert additional authentication data aad to Byte
const aes_real_aad = Buffer.from(aes_aad, "utf8");

//Convert encrypted data to Byte
const aes_real_plaintext = Buffer.from(req_data_json, "utf-8");

// Create AES256_GCM encryption tool
const aes_cipher = crypto.createCipheriv("aes-256-gcm", aes_real_key, ivByte);

//Set additional authentication data aad
aes_cipher.setAAD(aes_real_aad);

let aes_cipher_update = aes_cipher.update(aes_real_plaintext);
let aes_cipher_final = aes_cipher.final();
const aes_real_ciphertext = Buffer.concat([
   aes_cipher_update,
   aes_cipher_final,
]);
const aes_real_authTag = aes_cipher.getAuthTag();

// Encrypted request data
const encrypt_req_data = {
   iv: ivByte.toString("base64"),
   // Convert encrypted data to Base64
   data: aes_real_ciphertext.toString("base64"),
   // Convert authtag to Base64
   authtag: aes_real_authTag.toString("base64"),
};
  1. When I used C# to encrypt, I found that the encrypted data was inconsistent with the data encrypted by NodeJS. I have hardcoded all the encrypted data of Key Iv AAD according to the encryption parameters of NodeJS, but The final encrypted value still cannot be connected to NodeJS, so I cannot decrypt the data encrypted in C# in NodeJS, or decrypt the data encrypted in NodeJS in C#.

Below is my C# code.


// Encrypted data, JSON format
var req_data_json = JsonSerializer.Serialize(req_data);

// Convert encrypted data to Byte
var req_data_bytes = Encoding.UTF8.GetBytes(req_data_json);

// AES256_GCM encryption

//Assemble additional authentication data aad
var aes_aad = $"{WxConfig.Url}|{WxConfig.AppId}|{WxConfig.Timestamp}|{WxConfig.AES.SN}";

// Convert additional authentication data aad to Byte
var aes_aad_bytes = Encoding.UTF8.GetBytes(aes_aad);

// Use BouncyCastle for AES256_GCM encryption
var aes_cipher = new GcmBlockCipher(new AesEngine());

//Encrypt the Key and Iv parameters used, and set additional authentication information aad
var aes_parameters =
     new AeadParameters(new KeyParameter(WxConfig.AES.KeyBytes), 128, WxConfig.IvBytes, aes_aad_bytes);

//Initialize the encryptor
aes_cipher.Init(true, aes_parameters);

// Encrypted data Byte
var aes_encrypt_data_bytes = new byte[aes_cipher.GetOutputSize(req_data_bytes.Length)];

//Add encrypted data
var aes_length = aes_cipher.ProcessBytes(req_data_bytes, 0, req_data_bytes.Length, aes_encrypt_data_bytes, 0);

// Encryption
aes_cipher.DoFinal(aes_encrypt_data_bytes, aes_length);

// Encrypt the authentication information output by GCM mode Byte, the default is 16 bytes
//var aes_auth_tag_bytes = new byte[16];
var aes_auth_tag_bytes = aes_cipher.GetMac();

// Get AES encrypted data
var aes_encrypt_data = Convert.ToBase64String(aes_encrypt_data_bytes);

// Get the authentication information output by AES GCM mode
var aes_encrypt_auth_tag = Convert.ToBase64String(aes_auth_tag_bytes);

I have been troubled by this problem for many days. I hope you can help me figure out where the problem occurred and the resulting encrypted data is inconsistent.

cipherboy commented 9 months ago

@Net-18K Aha!

First, note that aes_encrypt_data (the output from BouncyCastle's DoFinal(...)) contains aes_encrypt_auth_tag. This is consistent with most crypto libraries as the tag is usually appended to the ciphertext. You'll want to trim that off if you want them separate.

Second, I looked at some test vectors and used standard test vectors.

Bouncy Castle will passed with this code. ```c# using System.Text; using System.Text.Json; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; var WxConfig_Url = "https://github.com/bcgit/bc-csharp/issues/494"; var WxConfig_AppId = "Net-18K"; var WxConfig_Timestamp = "Oct 18, 2023"; var WxConfig_AESSN = "1234567890"; // var WxConfig_AES_KeyBytes_Base64 = "snN6cLBw5dyCApmP+Jc/JndIzlwpOeAChg2MGMkOX4I="; var WxConfig_AES_KeyBytes_Base64 = "/v/pkoZlcxxtao+UZzCDCP7/6ZKGZXMcbWqPlGcwgwg="; var WxConfig_AES_KeyBytes = Convert.FromBase64String(WxConfig_AES_KeyBytes_Base64); // var WxConfig_IvBytes_Base64 = "bvHvv8iZ2X7fHZZe"; // var WxConfig_IvBytes_Base64 = "WxMxLsrFzCpTmSGaKJN8aA=="; var WxConfig_IvBytes_Base64 = "kxMiXfiEBuVVkJxa/1Jpqmp6lThTT32h5MMD0qMYpyjDwMlRVoCVOfzw4kKaa1JUFq7b9aDealemN7Ob"; var WxConfig_IvBytes = Convert.FromBase64String(WxConfig_IvBytes_Base64); // Encrypted data, JSON format // var req_data_json = JsonSerializer.Serialize(req_data); var req_data_json = "{\"type\":\"error\",\"message\":\"interop error?\"}"; // Convert encrypted data to Byte // var req_data_bytes = Encoding.UTF8.GetBytes(req_data_json); var req_data_bytes = Convert.FromBase64String("2TEyJfiEBuWlWQnFr/UmmoanqVMVNPfaLkwwPYoxinIcPAyVlWgJUy/PDiRJprUlsWrt9aoN5le6Y3s5"); // AES256_GCM encryption //Assemble additional authentication data aad var aes_aad = $"{WxConfig_Url}|{WxConfig_AppId}|{WxConfig_Timestamp}|{WxConfig_AESSN}"; // Convert additional authentication data aad to Byte // var aes_aad_bytes = Encoding.UTF8.GetBytes(aes_aad); var aes_aad_bytes = Convert.FromBase64String("/u36zt6tvu/+7frO3q2+76ut2tI="); // Use BouncyCastle for AES256_GCM encryption var aes_cipher = new GcmBlockCipher(new AesEngine()); //Encrypt the Key and Iv parameters used, and set additional authentication information aad var aes_parameters = new AeadParameters(new KeyParameter(WxConfig_AES_KeyBytes), 128, WxConfig_IvBytes, aes_aad_bytes); //Initialize the encryptor aes_cipher.Init(true, aes_parameters); // Encrypted data Byte var aes_encrypt_data_bytes = new byte[aes_cipher.GetOutputSize(req_data_bytes.Length)]; //Add encrypted data var aes_length = aes_cipher.ProcessBytes(req_data_bytes, 0, req_data_bytes.Length, aes_encrypt_data_bytes, 0); // Encryption aes_cipher.DoFinal(aes_encrypt_data_bytes, aes_length); // Encrypt the authentication information output by GCM mode Byte, the default is 16 bytes //var aes_auth_tag_bytes = new byte[16]; var aes_auth_tag_bytes = aes_cipher.GetMac(); // Get AES encrypted data var aes_encrypt_data = Convert.ToBase64String(aes_encrypt_data_bytes); // Get the authentication information output by AES GCM mode var aes_encrypt_auth_tag = Convert.ToBase64String(aes_auth_tag_bytes); Console.WriteLine("ciphertext:"); Console.WriteLine(aes_encrypt_data); Console.WriteLine("\nAuth Tag:"); Console.WriteLine(aes_encrypt_auth_tag); ```
$ dotnet run
ciphertext:
Wo3vLwyeU/H3XXhTZZ4qIO6ysiqv3mQZoFirT290a/QPwMO3gPJERS2j6/HF2CzeokGJlyAO+C5Ern4/pEqCZu4cjrDItdTPWunxmg==

Auth Tag:
pEqCZu4cjrDItdTPWunxmg==

(these match the values when converted hex to base64: Wo3vLwyeU/H3XXhTZZ4qIO6ysiqv3mQZoFirT290a/QPwMO3gPJERS2j6/HF2CzeokGJlyAO+C5Ern4/ and pEqCZu4cjrDItdTPWunxmw==.

But NodeJS failed with similar code: ```js const crypto = require('crypto'); var url = "https://github.com/bcgit/bc-csharp/issues/494"; var appid = "Net-18K"; var timestamp = "Oct 18, 2023"; var aes_sn = "1234567890"; // var aes_key = "snN6cLBw5dyCApmP+Jc/JndIzlwpOeAChg2MGMkOX4I="; var aes_key = "/v/pkoZlcxxtao+UZzCDCP7/6ZKGZXMcbWqPlGcwgwg="; // Line 501, boringssl // var ivByte = "bvHvv8iZ2X7fHZZe"; // var ivByte = "WxMxLsrFzCpTmSGaKJN8aA=="; var ivByte = "kxMiXfiEBuVVkJxa/1Jpqmp6lThTT32h5MMD0qMYpyjDwMlRVoCVOfzw4kKaa1JUFq7b9aDealemN7Ob"; // Line 502, boringssl var req_data = {"type": "error", "message": "interop error?"}; // Convert request data to JSON // const req_data_json = JSON.stringify(req_data); const req_data_json = Buffer.from("2TEyJfiEBuWlWQnFr/UmmoanqVMVNPfaLkwwPYoxinIcPAyVlWgJUy/PDiRJprUlsWrt9aoN5le6Y3s5", "base64"); //Assemble additional authentication data aad const aes_aad = url + "|" + appid + "|" + timestamp + "|" + aes_sn; console.log(Buffer.from(aes_aad, "utf8").toString("base64")); // Convert key to Byte const aes_real_key = Buffer.from(aes_key, "base64"); // Convert additional authentication data aad to Byte // const aes_real_aad = Buffer.from(aes_aad, "utf8"); aes_real_aad = Buffer.from("/u36zt6tvu/+7frO3q2+76ut2tI=", "base64"); //Convert encrypted data to Byte const aes_real_plaintext = Buffer.from(req_data_json, "utf-8"); // Create AES256_GCM encryption tool const aes_cipher = crypto.createCipheriv("aes-256-gcm", aes_real_key, ivByte); //Set additional authentication data aad aes_cipher.setAAD(aes_real_aad); let aes_cipher_update = aes_cipher.update(aes_real_plaintext); let aes_cipher_final = aes_cipher.final(); const aes_real_ciphertext = Buffer.concat([ aes_cipher_update, aes_cipher_final, ]); const aes_real_authTag = aes_cipher.getAuthTag(); // Encrypted request data const encrypt_req_data = { iv: ivByte.toString("base64"), // Convert encrypted data to Base64 data: aes_real_ciphertext.toString("base64"), // Convert authtag to Base64 authtag: aes_real_authTag.toString("base64"), }; var encrypt_req_json = JSON.stringify(encrypt_req_data); console.log(encrypt_req_json); ```
$ nodejs ./index.js
{"iv":"kxMiXfiEBuVVkJxa/1Jpqmp6lThTT32h5MMD0qMYpyjDwMlRVoCVOfzw4kKaa1JUFq7b9aDealemN7Ob","data":"HXm3r5NtevjyMEJUtnt/EPAVQw9GuVhQpIuRadF7vUUUScCLoU9mmkVMmpXI1cudTWVA4E6r5hG+a0c6","authtag":"pwRUJ5co3gzVwmkzJC4beg=="}

This shows that the issue lies on the NodeJS side.

I'm not quite sure why, though. The inputs look OK from my PoV, likely there's something with the integration with OpenSSL.

I'm on the following NodeJS version:

$ node --version
v18.13.0

(with OpenSSL 3.0.10).

Hope this helps!