javac#aes-gcm

How can I port this Java "AES/GCM/NoPadding" encryption algorithm code to C#


I am involved in the development of two system communication projects. The Java-based system on the other side requires the transmitted content to be encrypted using AES. Our system is currently based on .NET Framework 7 (NET7). I am attempting to decrypt the content from the other party using C#. I tried implementing the algorithm using the "BouncyCastle.Cryptography" framework, attempting to mimic the Java coding style, but unfortunately, I was unsuccessful. I have been struggling with this issue for a few days and have decided to seek help here.

Here is a snippet of the java implementation:
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public class AESUtil {
    private static final String KEY_ALGORITHM_AES = "AES";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
    private static final String CHARSET = "UTF-8";
    public static String aesEncrypt(String content, String encryptPass) {
        try {
            byte[] iv = new byte[12];
            SecureRandom secureRandom = new SecureRandom();
            secureRandom.nextBytes(iv);
            byte[] contentBytes = content.getBytes(CHARSET);
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            GCMParameterSpec params = new GCMParameterSpec(128, iv);
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(encryptPass), params);
            byte[] encryptData = cipher.doFinal(contentBytes);
            assert encryptData.length == contentBytes.length + 16;
            byte[] message = new byte[12 + contentBytes.length + 16];
            System.arraycopy(iv, 0, message, 0, 12);
            System.arraycopy(encryptData, 0, message, 12, encryptData.length);
            return Base64.getEncoder().encodeToString(message);
        } catch (Exception e) {

        }
        return null;
    }

    public static String aesDecrypt(String base64Content, String encryptPass) {
        try {
            byte[] content = Base64.getDecoder().decode(base64Content);
            if (content.length < 12 + 16) {
                throw new IllegalArgumentException();
            }
            GCMParameterSpec params = new GCMParameterSpec(128, content, 0, 12);
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(encryptPass), params);
            byte[] decryptData = cipher.doFinal(content, 12, content.length - 12);
            return new String(decryptData, CHARSET);
        } catch (Exception e) {
            System.out.println("error:" + e.getMessage());
        }
        return null;
    }

    private static SecretKeySpec getSecretKey(String encryptPass) throws NoSuchAlgorithmException {
        KeyGenerator kg = KeyGenerator.getInstance(KEY_ALGORITHM_AES);
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(encryptPass.getBytes());
        kg.init(128, secureRandom);
        SecretKey secretKey = kg.generateKey();
        return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM_AES);
    }
}
The following is the code I attempted to mimic, but I haven't fully understood the Java code
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System.Text;

static string Encrypt(string content, string encryptPass)
{
    var size = 128;
    var plainTextData = Encoding.UTF8.GetBytes(content);
    CipherKeyGenerator keyGen = new CipherKeyGenerator();
    var sr = SecureRandom.GetInstance("SHA1PRNG");
    sr.SetSeed(Encoding.UTF8.GetBytes(encryptPass));
    keyGen.Init(new KeyGenerationParameters(sr, size));
    KeyParameter keyParam = keyGen.GenerateKeyParameter();

    var cipher = CipherUtilities.GetCipher("AES/GCM/NoPadding");
    byte[] iv = new byte[12];
    var secureRandom = new SecureRandom();
    secureRandom.NextBytes(iv);
    AeadParameters keyParamAead = new AeadParameters(keyParam, size, iv);
    cipher.Init(true, keyParamAead);
    int outputSize = cipher.GetOutputSize(plainTextData.Length);
    byte[] cipherTextData = new byte[outputSize];
    int result = cipher.ProcessBytes(plainTextData, 0, plainTextData.Length, cipherTextData, 0);
    cipher.DoFinal(cipherTextData, result);
    if (cipherTextData.Length != plainTextData.Length + 16)
    {
        Console.WriteLine("error!");
        return string.Empty;
    }
    else
    {
        cipher.Init(false, keyParamAead);
        outputSize = cipher.GetOutputSize(cipherTextData.Length);
        var decryptData = new byte[outputSize];
        int decryptResult = cipher.ProcessBytes(cipherTextData, 0, cipherTextData.Length, decryptData, 0);
        cipher.DoFinal(decryptData, decryptResult);
        if (Encoding.UTF8.GetString(decryptData) != content)
        {
            Console.WriteLine($"error :{Encoding.UTF8.GetString(decryptData)}");
            return string.Empty;
        }
    }

    byte[] message = new byte[12 + plainTextData.Length + 16];
    Buffer.BlockCopy(iv, 0, message, 0, 12);
    Buffer.BlockCopy(cipherTextData, 0, message, 12, cipherTextData.Length);
    return Convert.ToBase64String(message);
}

static string Decrypt(string encryptString, string encryptPass)
{
    var encryptedData = Encoding.UTF8.GetBytes(encryptString);
    CipherKeyGenerator keyGen = new CipherKeyGenerator();
    var sr = SecureRandom.GetInstance("SHA1PRNG");
    sr.SetSeed(Encoding.UTF8.GetBytes(encryptPass));
    keyGen.Init(new KeyGenerationParameters(sr, 128));
    KeyParameter keyParam = keyGen.GenerateKeyParameter();

    var cipher = CipherUtilities.GetCipher("AES/GCM/NoPadding");
    var iv = new byte[12];
    Buffer.BlockCopy(encryptedData, 0, iv, 0, 12);
    AeadParameters keyParamAead = new AeadParameters(keyParam, 128, iv);

    cipher.Init(true, keyParamAead);

    var realEncryptedData = new byte[encryptedData.Length - 12];
    var outputSize = cipher.GetOutputSize(realEncryptedData.Length);
    var decryptData = new byte[outputSize];
    int decryptResult = cipher.ProcessBytes(realEncryptedData, 0, realEncryptedData.Length, decryptData, 0);
    cipher.DoFinal(decryptData, decryptResult);
    return Encoding.UTF8.GetString(decryptData);
}
Key

esp4tgsltzA2fml2

The result of Java version encryption

cOYavtcA/pvUNgU1tYiNzEOOnazFVsfce/ApmHz0gAsq

The C # code I wrote cannot be decrypted properly.I would like the C# code I write to be able to achieve mutual encryption and decryption with the Java version


Solution

  • A pseudorandom number generator (PRNG) should not be used as a key derivation function (KDF), as the implementations can vary across platforms and even versions, so that even with identical seeds it is not guaranteed that the same byte sequence will be generated. This makes a PRNG unsuitable as a KDF.

    The BouncyCastle implementation you are using generates a random start seed by default and merely adds the additional seed specified with SetSeed(). The default thus generates a random byte sequence and is therefore unsuitable for your purposes.
    However, the implementation allows you to disable the generation of the start seed with SecureRandom.GetInstance("SHA1PRNG", false). Unfortunately, the generated byte sequence does not correspond to the value of the Java implementation despite the identical seed. The reason for this is the above-mentioned non-standardized implementation of the SHA1PRNG algorithm.

    To avoid such compatibility issues, the Java implementation of SHA1PRNG itself can be ported to C#. The following C# lightweight implementation only ports the part that is needed to generate a byte sequence of arbitrary length for a given seed. In particular, the porting of the seed generation, which is not required here, was omitted. The Java source code used for the porting is JDK 22:

    using System.Security.Cryptography;
    ...
    class Sha1PrngLW
    {
        private static int DIGEST_SIZE = 20;
        private byte[] state;
        private byte[] remainder;
        private int remCount;
    
        public Sha1PrngLW(byte[] seed)
        {
            state = SHA1.HashData(seed);
        }
    
        public byte[] GetNextBytes(int number)
        {
            byte[] result = new byte[number];
            int index = 0;
            int todo;
            byte[] output = remainder;
    
            int r = remCount;
            if (r > 0)
            {
                todo = Math.Min(result.Length - index, DIGEST_SIZE - r);
                for (int i = 0; i < todo; i++)
                {
                    result[i] = output[r];
                    output[r++] = 0;
                }
                remCount += todo;
                index += todo;
            }
    
            while (index < result.Length)
            {
                output = SHA1.HashData(state);
                updateState(state, output);
    
                todo = Math.Min((result.Length - index), DIGEST_SIZE);
                for (int i = 0; i < todo; i++)
                {
                    result[index++] = output[i];
                    output[i] = 0;
                }
                remCount += todo;
            }
    
            remainder = output;
            remCount %= DIGEST_SIZE;
    
            return result;
        }
    
        private static void updateState(byte[] state, byte[] output)
        {
            int last = 1;
            int v;
            byte t;
            bool zf = false;
    
            for (int i = 0; i < state.Length; i++)
            {
                v = (sbyte)state[i] + (sbyte)output[i] + last;
                t = (byte)v;
                zf = zf | (state[i] != t);
                state[i] = t;
                last = v >> 8;
            }
    
            if (!zf)
            {
                state[0]++;
            }
        }
    }
    

    A similar port can be found here. The above port differs in some respects, as it is based on a newer Java version and is adapted to .NET 7.


    Regarding the AES/GCM encryption, there is no need to use BouncyCastle on .NET 7, as this algorithm is supported with the built-in AesGcm class:

    Sha1PrngLW sha1PrngLW = new Sha1PrngLW(Encoding.UTF8.GetBytes("esp4tgsltzA2fml2"));
    byte[] key = sha1PrngLW.GetNextBytes(16);
    
    byte[] data = Convert.FromBase64String("cOYavtcA/pvUNgU1tYiNzEOOnazFVsfce/ApmHz0gAsq");
    
    AesGcm aesGcm = new AesGcm(key);
    byte[] decrypted = new byte[data.Length - 12 - 16];
    aesGcm.Decrypt(data[0..12], data[12..^16], data[^16..], decrypted);
    
    Console.WriteLine(Encoding.UTF8.GetString(decrypted)); // 12345
    

    With this, the decryption is successful and returns 12345 as decrypted data.