Algoritmo Criptografia AES Java compatível com C#

2021/02/11

Recentemente tive de implementar um método de criptografia AES no Java de forma a ser compatível com a criptografia implementada em C# no back-end do cliente.

O código implementado em C# pelo cliente era análogo à este:

    public static string AesEncryptString(string toEncrypt)
    {
        try
        {
            string encrypted = null;
            AesManaged aes = null;
            try
            {
                var password = "hswPJNdopw5as0NJqweVCdA1FKsNYIlkdoVlk/poifW=";
                byte[] salt = Encoding.ASCII.GetBytes("ps85hjk98ng23ga");
                Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(password, salt);
                aes = new AesManaged();
                aes.Key = key.GetBytes(aes.KeySize / 8);
                aes.IV = key.GetBytes(aes.BlockSize / 8);
                ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
                using (MemoryStream memory = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(memory, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {
                            swEncrypt.Write(toEncrypt);
                        }
                    }
                    encrypted = Convert.ToBase64String(memory.ToArray());
                }
            }
            finally
            {
                if (aes != null) aes.Clear();
            }
            return encrypted;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

Pois bem tentei usar as classes de criptografia do Java mas nunca conseguia obter os mesmos resultados que o algoritmo escrito em C#.

O problema é que a implementação do PBEKeySpec do Java só suporta vetores de inicialização aleatórios e não pseudo-aleatórios como o C#, dessa forma nunca era possível obter a mesma encriptação como na implementação feita em C#.

A solução foi criar uma implementação do Rfc2898DeriveBytes em Java. Essa implementação eu fiz como inner class de uma classe chamada AesCipher, a qual segue o código:

    import java.io.UnsupportedEncodingException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.util.Base64;
    
    import javax.crypto.Cipher;
    import javax.crypto.Mac;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    
    /**
     * Algoritmo de enciptação/decriptação AES compatível com C# .Net.
     * 
     * @author ricardo.staroski
     */
    public final class AesCipher {
    
        /**
         * Derivação de senha RFC 2898.<br>
         * Compatível com a classe <code>Rfc2898DeriveBytes</code> do C# .Net.
         */
        private static final class Rfc2898DeriveBytes {
    
            /**
             * Converte um <code>int</code> em um array de 4 <code>byte</code>s.
             * 
             * @param value O valor <code>int</code>.
             * @return Os 4 <code>byte</code>s que compõe o <code>int</code> informado.
             */
            private static byte[] intToBytes(int value) {
                return new byte[] { (byte) (value >>> 24), (byte) (value >>> 16), (byte) (value >>> 8), (byte) (value >>> 0) };
            }
    
            private Mac hmacSha1;
            private byte[] salt;
            private byte[] buffer = new byte[20];
    
            private int iterationCount;
            private int bufferStartIndex = 0;
            private int bufferEndIndex = 0;
            private int block = 1;
    
            /**
             * @param password   A senha utilizada para derivar a chave.
             * @param salt       O salto utilizado para derivar a chave.
             * @param iterations O número de iterações da operação.
             * @throws NoSuchAlgorithmException Se o algoritmo HmacSHA1 núo estiver disponúvel.
             * @throws InvalidKeyException      Se o salto tiver menos de 8 bytes ou a senha for <code>null</code>.
             */
            public Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeyException {
                if ((salt == null) || (salt.length < 8)) {
                    throw new InvalidKeyException("Salt must be 8 bytes or more.");
                }
                if (password == null) {
                    throw new InvalidKeyException("Password cannot be null.");
                }
                this.salt = salt;
                this.iterationCount = iterations;
                this.hmacSha1 = Mac.getInstance("HmacSHA1");
                this.hmacSha1.init(new SecretKeySpec(password, "HmacSHA1"));
            }
    
            /**
             * @param password A senha utilizada para derivar a chave.
             * @param salt     O salto utilizado para derivar a chave.
             * @throws NoSuchAlgorithmException     Se o algoritmo HmacSHA1 não estiver disponúvel.
             * @throws InvalidKeyException          Se o salto tiver menos de 8 bytes ou a senha for <code>null</code>.
             * @throws UnsupportedEncodingException Se o encoding UTF-8 não for suportado.
             */
            public Rfc2898DeriveBytes(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
                this(password, salt, 1000);
            }
    
            /**
             * @param password   A senha utilizada para derivar a chave.
             * @param salt       O salto utilizado para derivar a chave.
             * @param iterations O número de iteraçães da operação.
             * @throws NoSuchAlgorithmException     Se o algoritmo HmacSHA1 não estiver disponível.
             * @throws InvalidKeyException          Se o salto tiver menos de 8 bytes ou a senha for <code>null</code>.
             * @throws UnsupportedEncodingException Se o encoding UTF-8 não for suportado.
             */
            public Rfc2898DeriveBytes(String password, byte[] salt, int iterations) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
                this(password.getBytes("UTF8"), salt, iterations);
            }
    
            /**
             * Retorna uma chave pseudo-aleatória de uma senha, salto e iterações.
             * 
             * @param count Número de bytes para retornar.
             * @return O array de bytes.
             */
            public byte[] getBytes(int count) {
                byte[] result = new byte[count];
                int resultOffset = 0;
                int bufferCount = this.bufferEndIndex - this.bufferStartIndex;
    
                if (bufferCount > 0) {
                    if (count < bufferCount) {
                        System.arraycopy(this.buffer, this.bufferStartIndex, result, 0, count);
                        this.bufferStartIndex += count;
                        return result;
                    }
                    System.arraycopy(this.buffer, this.bufferStartIndex, result, 0, bufferCount);
                    this.bufferStartIndex = this.bufferEndIndex = 0;
                    resultOffset += bufferCount;
                }
    
                while (resultOffset < count) {
                    int needCount = count - resultOffset;
                    this.buffer = this.func();
                    if (needCount > 20) {
                        System.arraycopy(this.buffer, 0, result, resultOffset, 20);
                        resultOffset += 20;
                    } else {
                        System.arraycopy(this.buffer, 0, result, resultOffset, needCount);
                        this.bufferStartIndex = needCount;
                        this.bufferEndIndex = 20;
                        return result;
                    }
                }
                return result;
            }
    
            private byte[] func() {
                this.hmacSha1.update(this.salt, 0, this.salt.length);
                byte[] tempHash = this.hmacSha1.doFinal(intToBytes(this.block));
    
                this.hmacSha1.reset();
                byte[] finalHash = tempHash;
                for (int i = 2; i <= this.iterationCount; i++) {
                    tempHash = this.hmacSha1.doFinal(tempHash);
                    for (int j = 0; j < 20; j++) {
                        finalHash[j] = (byte) (finalHash[j] ^ tempHash[j]);
                    }
                }
                if (this.block == 2147483647) {
                    this.block = -2147483648;
                } else {
                    this.block += 1;
                }
                return finalHash;
            }
        }
    
        private final SecretKeySpec key;
        private final IvParameterSpec iv;
        private final Cipher aesAlgorithm;
    
        /**
         * Construtor parametrizado.
         * 
         * @param password A senha a ser utilizada.
         * @param salt     O salto a ser utilizado.
         */
        public AesCipher(String password, String salt) {
            this(password, salt.getBytes());
        }
    
        /**
         * Construtor parametrizado.
         * 
         * @param password A senha a ser utilizada.
         * @param salt     O salto a ser utilizado.
         */
        public AesCipher(String password, byte[] salt) {
            try {
                aesAlgorithm = Cipher.getInstance("AES/CBC/PKCS5Padding");
                Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, salt);
                key = new SecretKeySpec(deriveBytes.getBytes(256 / 8), "AES");
                iv = new IvParameterSpec(deriveBytes.getBytes(128 / 8));
            } catch (Exception e) {
                throw new RuntimeException(e.getLocalizedMessage(), e);
            }
        }
    
        /**
         * Decripta a {@link String} informada.
         * 
         * @param toDecrypt {@link String} a ser decriptada.
         * @return A {@link String} decriptada.
         */
        public String decrypt(String toDecrypt) {
            try {
                aesAlgorithm.init(Cipher.DECRYPT_MODE, key, iv);
                byte[] encoded = toDecrypt.getBytes();
                byte[] encrypted = Base64.getDecoder().decode(encoded);
                byte[] decrypted = aesAlgorithm.doFinal(encrypted);
                return new String(decrypted);
            } catch (Exception e) {
                throw new RuntimeException(e.getLocalizedMessage(), e);
            }
        }
    
        /**
         * Encripta a {@link String} informada.
         * 
         * @param toEncrypt {@link String} a ser encriptada.
         * @return A {@link String} encriptada.
         */
        public String encrypt(String toEncrypt) {
            try {
                aesAlgorithm.init(Cipher.ENCRYPT_MODE, key, iv);
                byte[] decrypted = toEncrypt.getBytes();
                byte[] encrypted = aesAlgorithm.doFinal(decrypted);
                byte[] encoded = Base64.getEncoder().encode(encrypted);
                return new String(encoded);
            } catch (Exception e) {
                throw new RuntimeException(e.getLocalizedMessage(), e);
            }
        }
    }

Com a classe classe AesCipher consegui reproduzir a funcionalidade do código C# da seguinte forma no Java:

    public static String aesEncryptString(String toEncrypt) {
        String password = "hswPJNdopw5as0NJqweVCdA1FKsNYIlkdoVlk/poifW=";
        byte[] salt = "ps85hjk98ng23ga".getBytes();
        AesCipher aes = new AesCipher(password, salt);
        String encrypted = aes.encrypt(toEncrypt);
        return encrypted;
    }

Como essa implementação me trouxe uma certa dor de cabeça, resolvi compartilhá-la com a comunidade, caso alguém passe pelo mesmo problema futuramente.