使用C#加密AES以匹配Java加密

10

我收到了一个用Java实现的加密程序,但不幸的是我们是一家.NET公司,我无法将Java融入我们的解决方案中。可悲的是,我也不是Java专业人员,所以我已经为此奋斗了几天,想最终在这里求助。

我已经四处寻找一种匹配Java加密方式的方法,并得出结论,我需要在C#中使用RijndaelManaged。我离成功很近了,但是我返回的字符串只匹配到前半部分,后半部分却不同。

这里是Java实现的一小段代码:

private static String EncryptBy16( String str, String theKey) throws Exception
{

    if ( str == null || str.length() > 16)
    {
        throw new NullPointerException();
    }
    int len = str.length();
    byte[] pidBytes = str.getBytes();
    byte[] pidPaddedBytes = new byte[16];

    for ( int x=0; x<16; x++ )
    {
        if ( x<len )
        {
            pidPaddedBytes[x] = pidBytes[x];
        }
        else
        {
            pidPaddedBytes[x] = (byte) 0x0;
        }

    }

    byte[] raw = asBinary( theKey );
    SecretKeySpec myKeySpec = new SecretKeySpec( raw, "AES" );
    Cipher myCipher = Cipher.getInstance( "AES/ECB/NoPadding" );
    cipher.init( Cipher.ENCRYPT_MODE, myKeySpec );
    byte[] encrypted = myCipher.doFinal( pidPaddedBytes );
    return( ByteToString( encrypted ) );
}

public static String Encrypt(String stringToEncrypt, String key) throws Exception
{

    if ( stringToEncrypt == null ){
        throw new NullPointerException();
    }
    String str = stringToEncrypt;

    StringBuffer result = new StringBuffer();
    do{
        String s = str;
        if(s.length() > 16){
            str = s.substring(16);
            s = s.substring(0,16);
        }else {
            str = null;
        }
        result.append(EncryptBy16(s,key));
    }while(str != null);

    return result.toString();
}

我不确定为什么他们一次只传递16个字符,但无所谓。我使用字符串构建器尝试了与我的c#实现相同的方法,每次只发送16个字符,并在一次性传入整个字符串时获得了与发送16个字符相同的结果。

下面是我c#实现的代码片段,大部分都是从微软的RijndaelManaged站点复制和粘贴过来的:

public static string Encrypt(string stringToEncrypt, string key)
        {
            using (RijndaelManaged myRijndael = new RijndaelManaged())
            {
                myRijndael.Key = StringToByte(key);
                myRijndael.IV = new byte[16];
                return EncryptStringToBytes(stringToEncrypt, myRijndael.Key, myRijndael.IV);
            }
        }

static string EncryptStringToBytes(string plainText, byte[] Key, byte[] IV)
        {
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException("plainText");
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException("Key");
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException("Key");
            byte[] encrypted;
            using (RijndaelManaged rijAlg = new RijndaelManaged())
            {
                rijAlg.Key = Key;
                rijAlg.IV = IV;
                ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {
                            swEncrypt.Write(plainText);
                        }
                        encrypted = msEncrypt.ToArray();
                    }
                }
            }
            return ByteToString(encrypted);
        }

如上所述,加密字符串的前半部分相同(请参见下面的示例),但后半部分不同。 我在下面的输出中添加了空格,以更好地说明差异所在。 我对加密和Java的了解不足,不知道该往哪里转。 任何指导都将不胜感激

Java输出:

49a85367ec8bc387bb44963b54528c97 8026d7eaeff9e4cb7cf74f8227f80752

C#输出:

49a85367ec8bc387bb44963b54528c97 718f574341593be65034627a6505f13c

根据下面Chris的建议更新:

static string EncryptStringToBytes(string plainText, byte[] Key, byte[] IV)
{
    if (plainText == null || plainText.Length <= 0)
        throw new ArgumentNullException("plainText");
    if (Key == null || Key.Length <= 0)
        throw new ArgumentNullException("Key");
    if (IV == null || IV.Length <= 0)
        throw new ArgumentNullException("Key");
    byte[] encrypted;
    using (RijndaelManaged rijAlg = new RijndaelManaged())
    {
        rijAlg.Key = Key;
        rijAlg.IV = IV;
        rijAlg.Padding = PaddingMode.None;
        rijAlg.Mode = CipherMode.ECB;
        ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);
        using (MemoryStream msEncrypt = new MemoryStream())
        {
            using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
            {
                using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                {

                    swEncrypt.Write(plainText);
                    if (plainText.Length < 16)
                    {
                        for (int i = plainText.Length; i < 16; i++)
                        {
                            swEncrypt.Write((byte)0x0);
                        }
                    }
                }
                encrypted = msEncrypt.ToArray();
            }
        }
    }
    return ByteToString(encrypted);
}

你考虑过将IKVM作为一个选项吗? - chrylis -cautiouslyoptimistic-
1
那段Java代码使用了非常糟糕的加密方式。:-(通常情况下,你不会想用零来填充(就像那段代码所做的那样),因为你不会知道原始数据的大小(在填充之前)。填充的标准方法是使用PKCS #5填充。如果你能让Java代码使用它,你的C#代码将更容易与之互操作。 - C. K. Young
@chrylis 我最初尝试使用IKVM转换jar文件,但没有成功。这就是为什么我开始自己进行翻译的原因。 - Christopher Johnson
@ChrisJester-Young 很遗憾,这段 Java 代码不在我的控制范围之内。就像我在帖子中提到的那样,这是我拿到的东西,我需要用 C# 编写一段代码来加密一个字符串,以匹配他们发送给我的内容。 - Christopher Johnson
@ChrisJester-Young,这给了我与我上面发布的相同的结果。 - Christopher Johnson
显示剩余2条评论
2个回答

18

很好的问题,这是在不同语言中使用相同加密算法时经常出现的错误。要注意算法细节的实现。虽然我没有测试过代码,但在您的情况下,两个实现的填充选项不同,请尝试为c#和Java实现使用相同的填充选项。 您可以从这里阅读有关实现的注释和更多信息。 请注意填充默认值。

  • Padding = PaddingMode.PKCS7,
  • private final String cipherTransformation = "AES/CBC/PKCS5Padding";

c# 实现:

public RijndaelManaged GetRijndaelManaged(String secretKey)
    {
        var keyBytes = new byte[16];
        var secretKeyBytes = Encoding.UTF8.GetBytes(secretKey);
        Array.Copy(secretKeyBytes, keyBytes, Math.Min(keyBytes.Length, secretKeyBytes.Length));
        return new RijndaelManaged
        {
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7,
            KeySize = 128,
            BlockSize = 128,
            Key = keyBytes,
            IV = keyBytes
        };
    }

    public byte[] Encrypt(byte[] plainBytes, RijndaelManaged rijndaelManaged)
    {
        return rijndaelManaged.CreateEncryptor()
            .TransformFinalBlock(plainBytes, 0, plainBytes.Length);
    }

    public byte[] Decrypt(byte[] encryptedData, RijndaelManaged rijndaelManaged)
    {
        return rijndaelManaged.CreateDecryptor()
            .TransformFinalBlock(encryptedData, 0, encryptedData.Length);
    }


    // Encrypts plaintext using AES 128bit key and a Chain Block Cipher and returns a base64 encoded string

    public String Encrypt(String plainText, String key)
    {
        var plainBytes = Encoding.UTF8.GetBytes(plainText);
        return Convert.ToBase64String(Encrypt(plainBytes, GetRijndaelManaged(key)));
    }


    public String Decrypt(String encryptedText, String key)
    {
        var encryptedBytes = Convert.FromBase64String(encryptedText);
        return Encoding.UTF8.GetString(Decrypt(encryptedBytes, GetRijndaelManaged(key)));
    }

Java 实现:

private final String characterEncoding = "UTF-8";
private final String cipherTransformation = "AES/CBC/PKCS5Padding";
private final String aesEncryptionAlgorithm = "AES";

public  byte[] decrypt(byte[] cipherText, byte[] key, byte [] initialVector) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
{
    Cipher cipher = Cipher.getInstance(cipherTransformation);
    SecretKeySpec secretKeySpecy = new SecretKeySpec(key, aesEncryptionAlgorithm);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(initialVector);
    cipher.init(Cipher.DECRYPT_MODE, secretKeySpecy, ivParameterSpec);
    cipherText = cipher.doFinal(cipherText);
    return cipherText;
}

public byte[] encrypt(byte[] plainText, byte[] key, byte [] initialVector) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
{
    Cipher cipher = Cipher.getInstance(cipherTransformation);
    SecretKeySpec secretKeySpec = new SecretKeySpec(key, aesEncryptionAlgorithm);
    IvParameterSpec ivParameterSpec = new IvParameterSpec(initialVector);
    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
    plainText = cipher.doFinal(plainText);
    return plainText;
}

private byte[] getKeyBytes(String key) throws UnsupportedEncodingException{
    byte[] keyBytes= new byte[16];
    byte[] parameterKeyBytes= key.getBytes(characterEncoding);
    System.arraycopy(parameterKeyBytes, 0, keyBytes, 0, Math.min(parameterKeyBytes.length, keyBytes.length));
    return keyBytes;
}


public String encrypt(String plainText, String key) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException{
    byte[] plainTextbytes = plainText.getBytes(characterEncoding);
    byte[] keyBytes = getKeyBytes(key);
    return Base64.encodeToString(encrypt(plainTextbytes,keyBytes, keyBytes), Base64.DEFAULT);
}


public String decrypt(String encryptedText, String key) throws KeyException, GeneralSecurityException, GeneralSecurityException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException{
    byte[] cipheredBytes = Base64.decode(encryptedText, Base64.DEFAULT);
    byte[] keyBytes = getKeyBytes(key);
    return new String(decrypt(cipheredBytes, keyBytes, keyBytes), characterEncoding);
}

感谢您的回复,Berkay。但正如我所说,我无法控制Java代码。本质上,我正在使用C#编写此代码,以便在通过Web服务传递加密字符串时,他们将能够解密它(可能使用他们的Java实现)。因此,我需要确保C#版本以与他们当前Java版本相同的方式进行加密。 - Christopher Johnson
我明白了,尝试深入研究填充选项吧 :) Chris Jester-Young的回答也很有教学意义。 - berkay
好的,我已经根据他们的要求进行了一些更改(我认为是这样),现在我收到YSOD告诉我“加密数据的长度无效。”根据您的建议,我添加了以下内容:rijAlg.Padding = PaddingMode.None; rijAlg.Mode = CipherMode.ECB; 我添加了后面一项,因为看起来这是他们用于“模式”的方法。 - Christopher Johnson
@ChristopherJohnson 是的,看起来他们正在使用 ECB(呃)。ECB 不使用 IV,所以不要设置一个。(显然,写 Java 代码的人对如何编写安全的加密代码一无所知。请参见我在“手写”加密代码主题上的帖子:http://programmers.stackexchange.com/a/51528/5167。) - C. K. Young
1
谢谢你的帮助Berkay,我遇到了类似的问题,你的答案帮我找到了解决方案。至于Navin的问题,是的,PaddingMode.PKCS7与Java中的PKCS5Padding相同。 - Jonathan Vukadinovic
显示剩余2条评论

4
您的 C# 代码翻译在大部分情况下看起来是正确的,因为第一个块匹配。不匹配的是最后一个块,这是因为 Java 代码用零填充最后一个块以填满它,而您的 C# 代码没有这样做,所以默认使用 PKCS #5 填充。
当然,PKCS #5 填充要比零填充好得多,但由于 Java 代码使用的是后者,因此您需要做同样的事情。(也就是说,调用 swEncrypt.Write((byte) 0) 几次,直到字节计数是 16 的倍数。)
还有一个微妙之处。Java 代码使用 String.getBytes() 将字符串转换为字节,它会使用 Java 运行时的 "默认编码"。这意味着如果您的字符串包含非 ASCII 字符,您将遇到互操作性问题。最佳实践是使用 UTF-8,但由于您无法更改 Java 代码,所以我猜您对此无能为力。

世界上我最不想成为的就是负担 :) 但正如我在我的原始帖子中所说,我对加密方面并不擅长,因此我真的不知道如何添加0填充(无论是最差的做法还是不是)。你能给我提供一个小的代码示例吗?非常感谢。 - Christopher Johnson
@ChristopherJohnson 我添加了一个关于多次调用 swEncrypt.Write((byte) 0) 的注释。希望你能从中解决问题。 :-) - C. K. Young
好的,我已经尝试在using语句中添加swEncrypt.Write((byte) 0),但是对于“第二个块”,它给我不同的结果,但我无法使其匹配。我不确定如何检查字节数是否是16的倍数。 - Christopher Johnson
1
我最终不得不在开始加密之前填充数组,然后加密字节数组而不是字符串。我会将这标记为答案,因为它让我最接近解决问题。也感谢berkay提供有关RijndaelManaged的帮助。 - Christopher Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接