如何将这个Java“AES/GCM/NoPadding”加密算法代码移植到C#

问题描述 投票:0回答:1

我参与了两个系统通信项目的开发。另一端基于Java的系统要求传输的内容使用AES进行加密。我们的系统目前基于.NET Framework 7 (NET7)。我正在尝试使用 C# 解密对方的内容。我尝试使用“BouncyCastle.Cryptography”框架来实现该算法,试图模仿Java编码风格,但不幸的是,我没有成功。我已经为这个问题苦苦挣扎了几天,并决定在这里寻求帮助。

这里是java实现的一个片段:
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);
    }
}
以下是我尝试模仿的代码,但我还没有完全理解Java代码
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);
}
钥匙

esp4tgsltzA2fml2

Java版加密结果

coYavtca/pvUNgU1tYiNzeEOOnazFVsfce/ApmHz0gaAsq

我写的C#代码无法正常解密。我希望我写的C#代码能够与Java版本实现相互加解密

java c# aes-gcm
1个回答
0
投票

PRNG 不应该用作 KDF,因为不同平台甚至版本之间的实现可能有所不同,因此即使使用相同的种子,也不能保证生成相同的字节序列。这使得 PRNG 不适合作为 KDF。

您使用的 BouncyCastle 实现默认生成随机启动种子,并且仅添加使用

SetSeed()
指定的附加种子。因此,默认值会生成随机字节序列,因此不适合您的目的。
但是,该实现允许您使用
SecureRandom.GetInstance("SHA1PRNG", false)
禁用起始种子的生成。不幸的是,尽管种子相同,但生成的字节序列与 Java 实现的值并不对应。造成这种情况的原因是上述 SHA1PRNG 算法的非标准化实现。

为了避免此类兼容性问题,SHA1PRNG 本身的 Java 实现可以移植到 C#。以下 C# 轻量级实现仅移植为给定种子生成任意长度的字节序列所需的部分。特别是,省略了此处不需要的起始种子生成的移植。用于移植的 Java 源代码是 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]++;
        }
    }
}

可以在这里找到类似的端口。上述端口在某些方面有所不同,因为它基于较新的 Java 版本并适用于 .NET 7。


关于 AES/GCM 加密,.NET 7 上无需使用 BouncyCastle,因为内置

AesGcm
类支持此算法:

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

至此,解密成功并返回

12345
作为解密数据。

© www.soinside.com 2019 - 2024. All rights reserved.