pakito
pakito

Reputation: 387

Encrypt in JS and Decrypt in C# with ECIES

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

Answers (1)

Topaco
Topaco

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:

  • The handling of the IV is inconsistent. On the one hand, the IV is derived together with the key from the SHA256 hash of the shared secret: The first 16 bytes are the AES key, the second 16 bytes are the IV. On the other hand, the IV is sent redundantly with sharedInfo and encryptedData to the other side (concatenated), which would not be necessary as it could also be derived there.
    Alternatively, if it is sent to the other side, the other side can apply this IV and does not have to derive it.
  • The implementation uses a 16 bytes IV for GCM, which is allowed, but the recommended IV size for GCM is 12 bytes (16 bytes reduces performance and can also lead to compatibility problems, as most libraries follow the recommendation and some only support the recommended IV size).

Upvotes: 1

Related Questions