2

We have a target Java codebase responsible for decrypting an incoming payload. The payload is being encrypted within a .NET Core isolated Azure Function before being sent to the endpoint where the Java code attempts decryption. The encryption logic in .NET has been implemented to mirror the approach used in the Java code. However, during decryption on the Java side, we are encountering an "Invalid MAC" error.

The Java code for encryption:

package encrypt;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;

import javax.crypto.Cipher;

import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.IESParameterSpec;

public class Encrypt {

    public static void main(String[] args) throws Exception {
        // plain-text request
        String input = "Encryption with ECIES";
        String encryptedpayload = encryptECIES(input);
        System.out.println(encryptedpayload);
    }

    public static String encryptECIES(String payLoad) throws Exception {
        Security.addProvider(new BouncyCastleProvider());

        String key = {{PUBLIC KEY}};

        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode((base64PublicKey)));
        PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
        Cipher cipher = Cipher.getInstance("ECIESwithSHA512/NONE/NoPadding");
        IESParameterSpec iesParamSpec = new IESParameterSpec(null, null, 256);
        cipher.init(1, publicKey, iesParamSpec);
        return Base64.getEncoder().encodeToString(cipher.doFinal(payLoad.getBytes()));
    }
}

The Java code snippet for decryption:

package encrypt;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.IESParameterSpec;
import org.json.JSONObject;

public class Decrypt {

    public static void main(String[] args) throws Exception{
      // encrypted payload
      String input = "BEXNm796h82FKz79//6spJ/u9Aq7W+972N+vdx6dBnjiFibP+2XXHuHOzsLKqgKslj6NW8gIKk3//LPOckKnsWyETUtTKLF7LXRWzOp1jQbQfBHXqOu1J81N85r2PvHiL9vQG/oKxJMXXPLlSfJ0m1GTfG7FKzrELqhxAUoG3jUpIujjgycOHIw+iw9oQDcCGNZtpm7D";
        String payload = createDecryptionCipher(input);
    }

    private static String createDecryptionCipher(String input) throws NoSuchPaddingException, NoSuchAlgorithmException, InterruptedException,
            InvalidKeySpecException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        Security.addProvider(new BouncyCastleProvider());
        Cipher cipher = Cipher.getInstance("ECIESwithSHA512/NONE/NoPadding");

        String privateKey = "{{PRIVATE KEY}}";
        IESParameterSpec iesParamSpec = new IESParameterSpec(null, null,256);
        cipher.init(2, getDecryptedPrivateKey(privateKey), iesParamSpec);
        return new String(cipher.doFinal(Base64.getDecoder().decode(input.getBytes())));
    }
}

I've used similar encryption logic in C# that is used in the Java code snippet.

The C# encryption logic that is used:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using NSec.Cryptography;
using System.Text;
using System.Text.Json;
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.OpenSsl;
using System.Text.RegularExpressions;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto.EC;
using Org.BouncyCastle.Asn1.Anssi;
using Org.BouncyCastle.Asn1.Nist;
using Org.BouncyCastle.Asn1.TeleTrust;
using Org.BouncyCastle.Math.EC;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Asn1.X509;
using System.Security.Cryptography.X509Certificates;
using Org.BouncyCastle.Bcpg.OpenPgp;

namespace BharatConnectEncryption;

public class Encryption_Demo
{
    private readonly ILogger<Encryption_Demo> _logger;

    public Encryption_Demo(ILogger<Encryption_Demo> logger)
    {
        _logger = logger;
    }

    [Function("Encryption_Demo")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        try
        {
            string input = "Encryption with ECIES";
            Console.WriteLine("Original: " + input);
           
            string pemPublicKey = "{{PUBLIC KEY}}"
            ECPublicKeyParameters pubKey = LoadPublicKeyFromPem(pemPublicKey);         

            string encrypted = ECIESEncryptionUtil.Encrypt(input, pubKey);
            Console.WriteLine("Encrypted: " + encrypted);

            var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
            await response.WriteStringAsync(encrypted);
            return response;
        }
        catch (Exception ex)
        {
            var response = req.CreateResponse(System.Net.HttpStatusCode.InternalServerError);
            await response.WriteStringAsync($"There is some error. The reason is : {ex.Message}");
            return response;
        }
    }

    public static ECPublicKeyParameters LoadPublicKeyFromPem(string pem)
    {
        using var reader = new StringReader(pem);
        var pemReader = new PemReader(reader);
        var keyObject = pemReader.ReadObject();

        if (keyObject is ECPublicKeyParameters pubKey)
            return pubKey;

        if (keyObject is SubjectPublicKeyInfo info)
            return (ECPublicKeyParameters)PublicKeyFactory.CreateKey(info);

        throw new Exception("Unsupported public key format");
    }

public static class ECIESEncryptionUtil
{
    // Encrypt using ECIESwithSHA512/NONE/NoPadding
    public static string Encrypt(string plainText, ECPublicKeyParameters recipientPublicKey)
    {
        var ecParams = recipientPublicKey.Parameters;
        var random = new SecureRandom();

        // Generate ephemeral key pair
        var keyGen = new ECKeyPairGenerator();
        keyGen.Init(new ECKeyGenerationParameters(ecParams, random));
        AsymmetricCipherKeyPair ephKp = keyGen.GenerateKeyPair();

        var iesEngine = new IesEngine(
            new ECDHBasicAgreement(),
            new Kdf2BytesGenerator(new Sha512Digest()),
            new HMac(new Sha512Digest())
        );

        var derivation = new byte[0];
        var encoding = new byte[0];
        
        var iesParams = new IesWithCipherParameters(derivation, encoding, 256, 512);
        iesEngine.Init(true, ephKp.Private, recipientPublicKey, iesParams);

        byte[] input = Encoding.UTF8.GetBytes(plainText);
        byte[] ciphertext = iesEngine.ProcessBlock(input, 0, input.Length);

        // Prepend ephemeral public key
        byte[] ephPublicBytes = ((ECPublicKeyParameters)ephKp.Public).Q.GetEncoded(false);
        byte[] result = new byte[ephPublicBytes.Length + ciphertext.Length];
        Array.Copy(ephPublicBytes, 0, result, 0, ephPublicBytes.Length);
        Array.Copy(ciphertext, 0, result, ephPublicBytes.Length, ciphertext.Length);

        return Convert.ToBase64String(result);
    }
}

After generating the encrypted string from C# when we're using the string for decryption from the Java code, we're getting the error as "Invalid MAC".

4
  • 2
    Do both encryption codes generate the same encrypted result, given the same input? Commented Jun 10 at 14:38
  • 6
    @cliff2310 :D You do realize that that MAC (Media Access Control) has nothing to do with the MAC (Message Authentication Code) in the question? Commented Jun 10 at 15:19
  • The Java code is minimal but the C# code contains much code that isn't necessary to demonstrating the problem. Commented Jun 10 at 22:51
  • @PresidentJamesK.Polk, I've not found any minimal code in C# which is equivalent to the Java Commented Jun 11 at 14:35

1 Answer 1

3

Java/BouncyCastle implements (among others) the classes ECIESwithSHAXXX, ECIES and IESCipher. I can't find any comparable classes in C#/BouncyCastle. The closest is BufferedIesCipher, but this class seems to cover only part of the required functionality.

Another relevant class in Java/BouncyCastle is IESEngine, in which (among other things) the ephemeral key pair is generated (in processBlock()). I can't find this functionality in C#/BouncyCastle either (s. processBlock()).

There may be other classes that I have overlooked and that provide the required functionality. If not, the C#/BouncyCastle library can be extended by the missing classes, e.g. by yourself, if you are willing to invest the corresponding effort.


A faster solution is to debug Java/BouncyCastle in order to determine the details of the logic and implement it with C#/BouncyCastle. However, this C# implementation only works for the specific ECIES configuration of the posted Java code and is not a general solution that covers all possible ECIES variants of the Java/BouncyCastle library (e.g. this code will be incompatible with an ECIES configuration that applies ciphers like AES).

When the Java code is debugged, the following logic is revealed:

  • Importing the public key.
  • Generating an ephemeral key pair.
  • Generating the key agreement.
  • Generating the key using the KDF: As input for the key derivation, the concatenation of the ephemeral public uncompressed key and the key agreement is used. The optional parameter derivation (first parameter passed in IESParameterSpec, null in the sample code) is taken into account if provided. The length of the key KEnc for encryption corresponds to the length of the plaintext, and the length of the key KMac for integrity is set to 256 bits. Both are determined from the key derivation as KMac|KEnc.
  • Encrypting the plaintext with the plaintext and KEnc are XORed bit by bit (in particular no AES is involved).
  • Generating a MAC for the ciphertext. The optional parameter encoding (second parameter passed in IESParameterSpec, null in the sample code) is taken into account if provided.

The following is a possible C# implementation (without values for derivation and encoding) whose ciphertext can be decrypted with the Java code:

using System;
using System.IO;
using System.Buffers.Binary;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
...
public static string EncryptECIES(string payLoad)
{
    byte[] plaintext = Encoding.UTF8.GetBytes(payLoad);
    byte[] derivation = new byte[0]; // corresponds to 1st param in IESParameterSpec
    byte[] encoding = new byte[0]; // corresponds to 2nd param in IESParameterSpec

    // import public key and create ephemeral key pair
    ECPublicKeyParameters publicKey = ImportPublicKey();
    AsymmetricCipherKeyPair ephKeyPair = CreateEphemeralKeyPair(publicKey.Parameters);
    ECPrivateKeyParameters ephemeralPrivateKey = (ECPrivateKeyParameters)ephKeyPair.Private;
    byte[] ephemeralPublicKey = ((ECPublicKeyParameters)ephKeyPair.Public).Q.GetEncoded();

    // create ECDH key agreement
    ECDHBasicAgreement ecdhBasicAgreement = new ECDHBasicAgreement();
    ecdhBasicAgreement.Init(ephemeralPrivateKey);
    BigInteger agreementBI = ecdhBasicAgreement.CalculateAgreement(publicKey);
    byte[] agreement = Arrays.Concatenate(
            ephemeralPublicKey,
            BigIntegers.AsUnsignedByteArray(ecdhBasicAgreement.GetFieldSize(), agreementBI));

    // key derivation
    Kdf2BytesGenerator kdf2BytesGenerator = new Kdf2BytesGenerator(new Sha512Digest());
    KdfParameters kdfParam = new KdfParameters(agreement, derivation); 
    kdf2BytesGenerator.Init(kdfParam);
    byte[] KEnc = new byte[plaintext.Length];         
    byte[] KMac = new byte[256/8];                  
    byte[] K = new byte[KEnc.Length + KMac.Length];
    kdf2BytesGenerator.GenerateBytes(K, 0, K.Length);
    Buffer.BlockCopy(K, 0, KMac, 0, KMac.Length);
    Buffer.BlockCopy(K, KMac.Length, KEnc, 0, KEnc.Length);

    // encryption
    byte[] ct = new byte[plaintext.Length];
    for (int i = 0; i != plaintext.Length; ++i)
    {
        ct[i] = (byte)(plaintext[i] ^ KEnc[i]);
    }

    // MAC
    byte[] encodingLength = GetLengthTag(encoding);  
    HMac mac = new HMac(new Sha512Digest());
    byte[] tag = new byte[mac.GetMacSize()];
    mac.Init(new KeyParameter(KMac));
    mac.BlockUpdate(ct, 0, ct.Length);
    if (encoding != null)
    {
        mac.BlockUpdate(encoding, 0, encoding.Length);
    }
    mac.BlockUpdate(encodingLength, 0, encodingLength.Length);
    mac.DoFinal(tag, 0);

    // concatenate
    return Convert.ToBase64String(Arrays.Concatenate(ephemeralPublicKey, ct, tag));
}

private static ECPublicKeyParameters ImportPublicKey()
{
    string spkiPem = @"-----BEGIN PUBLIC KEY-----
                    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0
                    xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
                    -----END PUBLIC KEY-----";
    PemReader pemReader = new PemReader(new StringReader(spkiPem));
    return (ECPublicKeyParameters)pemReader.ReadObject();
}

private static AsymmetricCipherKeyPair CreateEphemeralKeyPair(ECDomainParameters ecParams)
{
    var keyGen = new ECKeyPairGenerator();
    keyGen.Init(new ECKeyGenerationParameters(ecParams, new SecureRandom()));
    return keyGen.GenerateKeyPair();
}

protected static byte[] GetLengthTag(byte[] p)
{
    byte[] L = new byte[8];
    if (p != null)
    {
        BinaryPrimitives.WriteInt64BigEndian(L, (long)p.Length * 8L);
    }
    return L;
}  

Alternatively, the C# code posted in the question can be retained and the required functions/changes implemented in a modified IesEngine class.

If the ephemeral key pair is still to be generated in ECIESEncryptionUtil.Encrypt(), the ephemeral public key must be passed to the modified IesEngine class, e.g. with an Init() overload (this is necessary because the ephemeral public key is not only required for decryption, but is also used for key derivation, where it is concatenated with the key agreement).


Test:

string ct = EncryptECIES("The quick brown fox jumps over the lazy dog");
Console.WriteLine(ct);

Sample Output:

BAexCkJ6AtfjRIEXF/ytMyLXvLDZZ5og94ZeVMA7B1XtofYPZ+XjTld1QO4UFkHc8SbwCOgYnPE/hSijWBvQZp8rGGnKdF0CT5HF/AJWX+VdcDafHzAww/Wx7WruXlNKaeAA8qDHPSBdF03OAfgN5RpzccCKIHGIsp9r/kEejwyTOlWoRJzqkOkQdL/DixsONDsrx9iEts8vmuHz6kCQwE/9b5LXBPj1EcnCoQ==

The ciphertext generated in this way can be decrypted using the posted Java code from the question.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the update. The thing is we can't modify the Java code as you mentioned. The Java logic will remain same.
@AyanPanjal - This solution requires no change to the Java code! derivation and encoding can have any value including null (as in the sample Java code posted). I only used the sample values to demonstrate that it also works for arbitrary values for derivation and encoding.
OK, revised the answer and used null (Java side) and new byte[0] (C# side) for both optional parameters. This should be less confusing. Decryption is possible using the unchanged Java method as demonstrated in the following online example: See .NET Fiddle for encryption with C# and jdoodle for decryption with the unchanged Java code.
@Topaco-Thank you very much for the solution. It's actually worked. Thank you once again

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.