Search code examples
javac#encryptioncryptographyrijndael

Decrypt C# RIJNDAEL encoded text


I'm implementing in Java the communication to a third party application. As part of the login process, the third party application is sending an encrypted string that I have to decode and send back. I have spent nearly 2 days googeling and reading posts but I can't find the right way to implement this.

I have a test case where the encrypted string is "c1W2YO1vYQzu6czteEidrG0U4g5gT4h57vAlP7tdjcY=" that decrypted with the password "GAT" must return "101714994".

The documentation I have states this: Authorization string was encrypted with the following settings:

  • Padding of input data: PKCS*7
  • Password byte array is 32 bytes long. Password string is converted to UTF-16 encoded byte array and byte array is then padded with zeroes up to length of 32 bytes. Longer passwords are truncated.

This is the C# example of how to decrypt the authorization string:

/// <summary> 
/// Decrypts a string. 
/// </summary> 
/// <param name="content">The string to decrypt.</param> 
/// <param name="password">The password to use.</param> 
/// <returns>The decrypted string.</returns> 
private static string DecryptString(string content, string password) { 
    Rijndael aes; 
    byte[] retVal = null; 
    byte[] contentBytes; 
    byte[] passwordBytes; 
    byte[] ivBytes;
    try { 
        //Get the content as byte[] 
        contentBytes = Convert.FromBase64String(content);

        //Create the password and initial vector bytes 
        passwordBytes = new byte[32]; 
        ivBytes = new byte[16]; 
        Array.Copy(Encoding.Unicode.GetBytes(password), passwordBytes, Encoding.Unicode.GetBytes(password).Length); 
        Array.Copy(passwordBytes, ivBytes, 16);

        //Create the cryptograpy 
        object aes = Rijndael.Create(); 
        aes.Key = passwordBytes; 
        aes.IV = ivBytes; 
        aes.Padding = PaddingMode.PKCS7;

        //Decrypt 
        retVal = aes.CreateDecryptor().TransformFinalBlock(contentBytes, 0, contentBytes.Length); 
    } 
    catch { 
    }
    aes = null; 
    contentBytes = null; 
    passwordBytes = null; 
    ivBytes = null;
    return Encoding.Unicode.GetString(retVal) 
}

This is my Java procedure to decrypt the string:

private String decryptAuthorizationString(String authString, String password) {
  try {
    //Force the test string
    authString = "c1W2YO1vYQzu6czteEidrG0U4g5gT4h57vAlP7tdjcY=";
    //Force the test password
    password = "GAT";

    //Create the password and initial vector bytes
    byte[] passwordBytes= new byte[32];      
    byte[] b= password.getBytes("UTF-8");      
    int len= b.length;
    if (len > passwordBytes.length) len = passwordBytes.length;
      System.arraycopy(b, 0, passwordBytes, 0, len);

    byte[] ivBytes= new byte[16];
    System.arraycopy(passwordBytes, 0, ivBytes, 0, 16);

    //Get the authString as byte[]
    byte[] authBytes = new BASE64Decoder().decodeBuffer(authString);

    InputStream inputStream = new ByteArrayInputStream(authBytes);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

    // If you have Bouncycastle library installed, you can use
    // Rijndael/CBC/PKCS7PADDING directly.
    Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    Cipher cipher = Cipher.getInstance("Rijndael/CBC/PKCS7PADDING", "BC");

    // convertedSecureString and initVector must be byte[] with correct length
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(passwordBytes, "AES"), new IvParameterSpec(ivBytes));

    CipherInputStream cryptoStream = new CipherInputStream(inputStream, cipher);
    byte[] buffer = new byte[1024];
    len = cryptoStream.read(buffer, 0, buffer.length);
    while (len > 0) {
      outputStream.write(buffer, 0, len);
      len = cryptoStream.read(buffer, 0, buffer.length);
    }

    outputStream.flush();
    cryptoStream.close();
    String resStr = outputStream.toString("UTF-8");       
    return resStr; //<<--- resStr must be "101714994"
  } catch (Throwable t) {

  }
  return null;
}

The procedure runs without any error but the result is not what I need it to be. Any help would be very appreciated.


Solution

  • You don't need the BouncyCastleProvider for this, as AES is already included in Java. However PKCS#7 padding is incorrectly indicated by "PKCS5Padding", so "AES/CBC/PKCS7Padding" cannot be indicated without Bouncy Castle.

    The default Unicode encoding of .NET is actually more compatible with UTF-16LE. Leave it to Microsoft to not keep to standard names (although they may have preceded it).

    The Java JCE is not really build around streaming as the C# classes are, so it is better to avoid streaming altogether.

    I've rewritten your sample code to show how to properly code this in Java (you'll need to be Java 7 compatible though). Don't shove exceptions under the table, turn them into AssertError or RuntimeExceptions.

    I've use the Bouncy Castle Base 64 decoder, as that one was the one available both for you and for me (but otherwise this is independent of Bouncy). Java 8 has a base 64 class included.


    So without further ado:

    import static java.nio.charset.StandardCharsets.UTF_16LE;
    
    import java.security.GeneralSecurityException;
    import java.util.Arrays;
    
    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    
    import org.bouncycastle.util.encoders.Base64;
    
    public class AuthenticationStringDecrypter {
    
        private static final String AES_CBC_PKCS5PADDING = "AES/CBC/PKCS5PADDING";
        private static final int KEY_SIZE = 256;
    
        public static void main(final String[] args) throws Exception {
            System.out.println(decryptAuthorizationString(
                    "c1W2YO1vYQzu6czteEidrG0U4g5gT4h57vAlP7tdjcY=", "GAT"));
        }
    
        private static String decryptAuthorizationString(final String authString,
                final String password) {
            try {
                // --- check if AES-256 is available
                if (Cipher.getMaxAllowedKeyLength(AES_CBC_PKCS5PADDING) < KEY_SIZE) {
                    throw new IllegalStateException("Unlimited crypto files not present in this JRE");
                }
    
                // --- create cipher
                final Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5PADDING);
    
                // --- create the key and initial vector bytes
                final byte[] passwordEncoded = password.getBytes(UTF_16LE);
                final byte[] keyData = Arrays.copyOf(passwordEncoded, KEY_SIZE
                        / Byte.SIZE);
                final byte[] ivBytes = Arrays.copyOf(keyData, cipher.getBlockSize());
    
                // --- init cipher
                cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyData, "AES"),
                        new IvParameterSpec(ivBytes));
    
                // --- decode & decrypt authentication string
                final byte[] authBytes = Base64.decode(authString);
                final byte[] decryptedData = cipher.doFinal(authBytes);
    
                // WARNING: may still decrypt to wrong string if
                // authString or password are incorrect - 
                // BadPaddingException may *not* be thrown
                return new String(decryptedData, UTF_16LE);
            } catch (BadPaddingException | IllegalBlockSizeException e) {
                // failure to authenticate
                return null;
            } catch (final GeneralSecurityException e) {
                throw new IllegalStateException(
                        "Algorithms or unlimited crypto files not available", e);
            }
        }
    }