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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21890805/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 11:17:10  来源:igfitidea点击:

Encrypt AES with C# to match Java encryption

c#javaencryptionaesrijndaelmanaged

提问by Christopher Johnson

I have been given a Java implementation for encryption but unfortunately we are a .net shop and I have no way of incorporating the Java into our solution. Sadly, I'm also not a Java guy so I've been fighting with this for a few days and thought I'd finally turn here for help.

我已经获得了用于加密的 Java 实现,但不幸的是,我们是一家 .net 商店,我无法将 Java 合并到我们的解决方案中。可悲的是,我也不是 Java 人,所以我已经为此奋斗了几天,并认为我终于可以求助于这里了。

I've searched high and low for a way to match the way the Java encryption is working and I've come to the resolution that I need to use RijndaelManaged in c#. I'm actually really close. The strings that I'm returning in c# are matching the first half, but the second half are different.

我已经搜索了很多方法来匹配 Java 加密的工作方式,我已经找到了需要在 c# 中使用 RijndaelManaged 的​​解决方案。我真的很接近。我在 c# 中返回的字符串与前半部分匹配,但后半部分不同。

Here is a snippet of the java implementation:

这是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();
}

I'm not entirely sure why they're only passing in 16 chars at a time, but w/e. I tried the same with my c# implementation using a string builder and only sending in 16 chars at a time and got the same result as I got when I pass the entire string in at once.

我不完全确定为什么他们一次只传递 16 个字符,但是 w/e。我使用字符串生成器在我的 c# 实现中尝试了相同的方法,并且一次只发送 16 个字符,并且得到的结果与我一次传递整个字符串时得到的结果相同。

Here's a snippet of my c# implementation which is mostly a copy and paste from MS' site for RijndaelManaged:

这是我的 c# 实现的一个片段,它主要是从 MS 站点为 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);
        }

As I said above, the first half of the encrypted string is the same (see an example below), but the second half is off. I've added spaces in the outputs below to better illustrate where the difference is. I don't know enough about encryption nor Java to know where to turn next. Any guidance would be greatly appreciated

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

Java output:

Java输出:

49a85367ec8bc387bb44963b54528c97 8026d7eaeff9e4cb7cf74f8227f80752

49a85367ec8bc387bb44963b54528c97 8026d7eaeff9e4cb7cf74f8227f80752

C# output:

C# 输出:

49a85367ec8bc387bb44963b54528c97 718f574341593be65034627a6505f13c

49a85367ec8bc387bb44963b54528c97 718f574341593be65034627a6505f13c

Update per the suggestion of Chris below:

根据下面克里斯的建议进行更新:

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);
}

采纳答案by Chris Jester-Young

Your C# translation looks like it's doing the right thing for the most part, because the first block matches. What doesn't match is the last block, and that's because the Java code is zero-padding the last block to fill it out, whereas your C# code doesn't do that, so it'd use PKCS #5 padding by default.

您的 C# 翻译看起来在大多数情况下都是正确的,因为第一个块匹配。不匹配的是最后一个块,这是因为 Java 代码对最后一个块进行零填充以填充它,而您的 C# 代码不这样做,因此默认情况下它会使用 PKCS #5 填充。

PKCS #5 padding is much better than zero-padding, of course, but since the latter is what the Java code used, you'd have to do the same thing. (That means, call swEncrypt.Write((byte) 0)a few more times until the byte count is a multiple of 16.)

PKCS #5 填充当然比零填充好得多,但由于后者是 Java 代码使用的内容,因此您必须做同样的事情。(这意味着,再调用swEncrypt.Write((byte) 0)几次,直到字节数是 16 的倍数。)

There's yet another subtlety. The Java code translates the string to bytes using String.getBytes(), which uses the "default encoding" of the Java runtime. This means that if your string contains non-ASCII characters, you'd run into interoperability issues. Best practice is to use UTF-8, but seeing as you can't change the Java code, I guess there's not much you can do about that.

还有一个微妙之处。Java 代码使用 将字符串转换为字节String.getBytes(),它使用 Java 运行时的“默认编码”。这意味着如果您的字符串包含非 ASCII 字符,您将遇到互操作性问题。最佳实践是使用 UTF-8,但鉴于您无法更改 Java 代码,我想您对此无能为力。

回答by berkay

Great question, this is a common error while working with the same encryption algorithm but in different languages. The implementation of the algorithm details requires attention. I haven't tested the code but in your case the padding options of two implementations are different, try to use the same padding optionsfor both c# and java implementations. You can read the comments and more about the implementation from here. Please pay attention to the padding defaults.

好问题,这是使用相同加密算法但使用不同语言时的常见错误。算法细节的实现需要注意。我还没有测试过代码,但在你的情况下,两种实现的填充选项不同,请尝试对c# 和 java 实现使用相同的填充选项。您可以从此处阅读有关实施的评论和更多信息。请注意填充默认值。

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

c# implementation:

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 implementation:

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);
}