Encrypt AES with C# to match Java encryption
Asked Answered
R

2

10

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.

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.

Here is a snippet of the java implementation:

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.

Here's a snippet of my c# implementation which is mostly a copy and paste from MS' site for 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 output:

49a85367ec8bc387bb44963b54528c97 8026d7eaeff9e4cb7cf74f8227f80752

C# output:

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);
}
Reentry answered 19/2, 2014 at 19:47 Comment(7)
Have you considered IKVM as an option?Windlass
That Java code is using encryption in a really gross way. :-( Usually, you don't ever want to pad with zeroes (like that code is doing) because you won't know the size of the original data (before padding). The standard way to pad is to use PKCS #5 padding. If you can arrange for the Java code to use that, your C# code will have a much easier time interoperating with that.Interknit
@chrylis I originally tried a conversion of the jar with IKVM with no success. That's why I moved to a translation myself.Reentry
@ChrisJester-Young Sadly, the java code is out of my hands. Like I said in my OP, that's what I was handed and need to make something in C# that can encrypt a string to match what they sent me....Reentry
@ChrisJester-Young Chris, I've updated my OP with what I'm trying to do per your suggestion and it's still giving me a different result for the second block. I know it's "working" because when I change the "padding mode" to PaddingMode.None, it requires a length of 16 or else I get a YSOD. Can you spot any errors in my updated code?Reentry
@ChristopherJohnson Try this: for (int i = plainText.length % 16; i > 0 && i < 16; ++i) { swEncrypt.Write((byte) 0); } (and take out the if (plainText.Length < 16) check, of course).Interknit
@ChrisJester-Young that gives me the same results as what I have posted above.Reentry
I
4

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.

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.)

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.

Interknit answered 19/2, 2014 at 19:58 Comment(4)
The last thing in the world I want to be is a burden :) But as I said in my OP, I'm not great with Encryption stuff so I really dont' know how to add in the 0 padding (worst practice or not). Can you provide me with a small code sample to start with? Muchas.Reentry
@ChristopherJohnson I added a comment about calling swEncrypt.Write((byte) 0) a bunch of times. Hopefully you can work it out from there. :-)Interknit
Ok, I've tried adding the swEncrypt.Write((byte) 0) inside the using statement and it's giving me different results for the "2nd block" but I can't get it to match. I'm not entirely sure how to check to determine if the byte count is a multiple of 16.Reentry
I ended up having to pad the array before I started the encryption and then encrypting the byte array instead of the string. I'll mark this as the answer as it got me the closest. Thanks to berkay too for providing help on the RijndaelManaged stuff.Reentry
B
18

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 options for both c# and java implementations. You can read the comments and more about the implementation from here. Please pay attention to the padding defaults.

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

c# implementation:

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:

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);
}
Blitzkrieg answered 19/2, 2014 at 20:5 Comment(7)
Thanks for the response berkay, but as I've said, I have no control over the Java code. Essentially what I'm doing it writing this up in C# so that when I pass the encrypted string over a web service, they will be able to decrypt it (presumably with their java implementation). Therefore, I need to make the c# version encrypt it the same way their Java one currently does.Reentry
I see, try delving the padding options:) Chris Jester-Young's answer is also very instructive.Blitzkrieg
ok I've changed a few things to match what they have (I think), and now I'm getting a YSOD telling me that "Length of the data to encrypt is invalid." Here's what I added per your suggestion: rijAlg.Padding = PaddingMode.None; rijAlg.Mode = CipherMode.ECB; The latter I added because it looks like that's what they're using for the "mode"Reentry
@ChristopherJohnson Yes, it seems they're using ECB (ugh). ECB doesn't use an IV, so don't set one. (Clearly, whoever wrote the Java code doesn't know anything about how to write secure crypto code. See my post on the topic of "hand-written" crypto code.)Interknit
Do you mean PaddingMode.PKCS7 in C# is the same as PKCS5Padding in java?Worktable
thanks for the help berkay, I was having a similar issue and your answer led me to my solution. As for navins question, yes PaddingMode.PKCS7 is the same as in java PKCS5PaddingGelsemium
Isn't PKCS5 padding for 64-bit blocks only? If so, how can that padding work with 128-bit AES encryption?Cannelloni
I
4

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.

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.)

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.

Interknit answered 19/2, 2014 at 19:58 Comment(4)
The last thing in the world I want to be is a burden :) But as I said in my OP, I'm not great with Encryption stuff so I really dont' know how to add in the 0 padding (worst practice or not). Can you provide me with a small code sample to start with? Muchas.Reentry
@ChristopherJohnson I added a comment about calling swEncrypt.Write((byte) 0) a bunch of times. Hopefully you can work it out from there. :-)Interknit
Ok, I've tried adding the swEncrypt.Write((byte) 0) inside the using statement and it's giving me different results for the "2nd block" but I can't get it to match. I'm not entirely sure how to check to determine if the byte count is a multiple of 16.Reentry
I ended up having to pad the array before I started the encryption and then encrypting the byte array instead of the string. I'll mark this as the answer as it got me the closest. Thanks to berkay too for providing help on the RijndaelManaged stuff.Reentry

© 2022 - 2024 — McMap. All rights reserved.