Reputation: 387
I'm trying to encrypt data in javascript and pass to C# for decryption and I got error mac check in GCM failed. This backend code is provided by backend guy and I have to use javascript to encrypt data to accomodate the backend, hence I cannot change any code in the backend. I have no idea what went wrong and hope someone could point me the correct direction.
In C#
public static string Decrypt(string privateKey, byte[] cliperText)
{
X9ECParameters ecParams = ECNamedCurveTable.GetByName(_curveName);
PemReader pr = new(new StringReader(privateKey));
AsymmetricCipherKeyPair keyPairServer = (AsymmetricCipherKeyPair) pr.ReadObject();
ECPrivateKeyParameters serverECPrivateKeyParameters = (ECPrivateKeyParameters) keyPairServer.Private;
int uncompressedPointKeyLengthBytes = 2 * (serverECPrivateKeyParameters.Parameters.N.BitLength + 8 - 1) / 8;
byte[] sharedInfo = cliperText[0..uncompressedPointKeyLengthBytes];
byte[] encryptedDataAndGcmTag = cliperText[uncompressedPointKeyLengthBytes..cliperText.Length];
int keySizeLengthBytes = (sharedInfo.Length - 1) / 2;
BigInteger x = new(1, sharedInfo[1..(keySizeLengthBytes + 1)]);
BigInteger y = new(1, sharedInfo[(keySizeLengthBytes + 1)..sharedInfo.Length]);
ECDomainParameters domainParameters = new(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed());
ECPublicKeyParameters pubkeyParam = new(ecParams.Curve.CreatePoint(x, y), domainParameters);
ECDHCBasicAgreement keyAgreement = new();
keyAgreement.Init(serverECPrivateKeyParameters);
BigInteger symmetricKey = keyAgreement.CalculateAgreement(pubkeyParam);
var counterData = new byte[] { 0x00, 0x00, 0x00, 0x01 };
var preHashKey = symmetricKey.ToByteArrayUnsigned().Concat(counterData).Concat(sharedInfo).Select(i => i).ToArray();
var hashedKey = SHA256.HashData(preHashKey);
AeadParameters parameters = new(new KeyParameter(hashedKey[0..16]), 128, hashedKey[16..32], null);
GcmBlockCipher cipher = new(new AesEngine());
cipher.Init(false, parameters);
byte[] plainBytes = new byte[cipher.GetOutputSize(encryptedDataAndGcmTag.Length)];
// This is where i get Mac check error. plainBytes is 21bytes but all are 0s
cipher.DoFinal(plainBytes, cipher.ProcessBytes(encryptedDataAndGcmTag, 0, encryptedDataAndGcmTag.Length, plainBytes, 0));
return Encoding.UTF8.GetString(plainBytes).TrimEnd("\r\n\0".ToCharArray());
}
string privateKeyServerASN1 = @"
-----BEGIN EC PRIVATE KEY-----
some private keys
-----END EC PRIVATE KEY-----
";
// This js encrypted data is generated from my javascript code
var js = "BArRePhFlOuJkzMyydytWrFmlsy8sbAJBU5b/bxNimQ5hkwkz0RjK4dGVrdK9WinpDjjoJ5DnWcP4+zp2RvgSCfPBbSHC4ZaHBXuDKhxVbKc9Fk6Cp6aE/awVLUj4Tjyv/l2R8bY";
string decryptedDataAndroid = ECIES.Decrypt(privateKeyServerASN1, Convert.FromBase64String(js));
In javascript:
async function performEncryption() {
const publicKeyBase64 = "public key";
const plaintextBytes = new TextEncoder().encode("Hello");
// Convert the base64 public key to ArrayBuffer
const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), c => c.charCodeAt(0)).buffer;
// Import the public key
const importedPublicKey = await crypto.subtle.importKey(
"spki",
publicKeyBytes,
{
name: "ECDH",
namedCurve: "P-256",
},
true,
[]
);
// Generate an ephemeral EC key pair
const ephemeralEcKeyPair = await crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-256",
},
true,
["deriveKey"]
);
// Use ECDH to generate a symmetric key
const sharedSecret = await crypto.subtle.deriveKey(
{
name: "ECDH",
public: importedPublicKey,
},
ephemeralEcKeyPair.privateKey,
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
// Export the raw public key (x coordinate) as sharedInfo
const rawPublicKey = await crypto.subtle.exportKey("raw", importedPublicKey);
const sharedInfo = new Uint8Array(rawPublicKey);
// Use SHA-256 to hash the shared secret
const symmetricKey = await crypto.subtle.digest("SHA-256", new Uint8Array(await crypto.subtle.exportKey("raw", sharedSecret)));
// Use the first 16 bytes as an AES-GCM key
const aesGcmKey = await crypto.subtle.importKey(
"raw",
symmetricKey.slice(0, 16),
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
// Use the second 16 bytes as the initialization vector (IV)
const iv = symmetricKey.slice(16, 32);
// Use AES/GCM/NoPadding to encrypt the plaintext and generate a 16-byte GCM tag
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
aesGcmKey,
plaintextBytes
);
// Concatenate sharedInfo, IV, and encrypted data
const combinedData = concatBuffers(concatBuffers(sharedInfo, iv), encryptedData);
console.log("combinedData", combinedData)
// Convert combinedData to base64
const base64CombinedData = btoa(String.fromCharCode.apply(null, new Uint8Array(combinedData)));
console.log("Encrypted Data (Base64):", base64CombinedData);
}
// Utility function to concatenate two ArrayBuffers
function concatBuffers(buffer1, buffer2) {
const result = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
result.set(new Uint8Array(buffer1), 0);
result.set(new Uint8Array(buffer2), buffer1.byteLength);
return result;
}
performEncryption();
Upvotes: 0
Views: 168
Reputation: 49131
Both codes contain bugs:
In the JavaScript code, the public key of the other side (i.e. importedPublicKey
) must not be sent to the other side as sharedInfo
(why should it, because the other side knows its own public key), but the public key of the ephemeral EC key pair:
...
const rawPublicKey = await crypto.subtle.exportKey("raw", ephemeralEcKeyPair.publicKey);
const sharedInfo = new Uint8Array(rawPublicKey);
...
In the C# code, only the shared secret may be hashed and not the concatenation of shared secret, counterData
and sharedInfo
, as this concatenation is not performed on the JavaScript side (alternatively, the C# side can remain unchanged and the concatenation can be added to the JavaScript side):
...
var preHashKey = symmetricKey.ToByteArrayUnsigned();
var hashedKey = SHA256.HashData(preHashKey);
...
In addition, the first 16 bytes of the ciphertext encryptedDataAndGcmTag
must be ignored in the C# code during decryption, as these contain the IV:
...
byte[] plainBytes = new byte[cipher.GetOutputSize(encryptedDataAndGcmTag.Length - 16)];
...
cipher.DoFinal(plainBytes, cipher.ProcessBytes(encryptedDataAndGcmTag, 16, encryptedDataAndGcmTag.Length - 16, plainBytes, 0));
...
Notes:
sharedInfo
and encryptedData
to the other side (concatenated), which would not be necessary as it could also be derived there.Upvotes: 1