terça-feira, 10 de janeiro de 2023

Java AES/CBC criptografia e descriptografia

 

Nesse artigo vou mostrar como criptografar e descriptografar um texto usando criptografia bidirecional assimétrica, ou seja, de chave única e privada. Esse tipo de criptografia é útil por exemplo para enviar uma URL de recuperação de senha para um usuário ou mesmo proteger o token de acesso.

 

Vamos usar o algoritmo AES (Advanced Encryption Standard) que possui algumas variações, da qual vamos usar o CBC - Cipher Block Chaining (Encadeamento de blocos cifrados). O modo CBC usa um vetor de inicialização (IV) para aumentar a criptografia. Primeiro, o CBC usa o bloco de texto simples XOR com o IV. Em seguida, ele criptografa o resultado para o bloco de texto cifrado. No próximo bloco, ele usa o resultado da criptografia para XOR com o bloco de texto simples até o último bloco. Uma das principais características do CBC é que ele usa um processo de encadeamento que faz com que a descriptografia de um bloco de texto cifrado dependa de todos os blocos de texto cifrado anteriores.

 

*XOR = Ou exclusivo ou disjunção exclusiva é um mecanismo de codificação usado para combinar diferentes entradas.

 

Vamos aos códigos. Serão 2 classes e um Teste Unitário para garantirmos que tudo está certo.

 

import java.security.spec.KeySpec;

 

import javax.crypto.Cipher;

import javax.crypto.SecretKey;

import javax.crypto.SecretKeyFactory;

import javax.crypto.spec.IvParameterSpec;

import javax.crypto.spec.PBEKeySpec;

import javax.crypto.spec.SecretKeySpec;

 

/**

 * Advanced Encryption Standard ( AES )

 *

 * @author Daniel Oliveira

 *

 * IV = Initialization Vector ou Vetor de inicialização

 *  é uma entrada para uma primitiva criptográfica sendo usada para fornecer

 *  o estado inicial. O IV geralmente precisa ser aleatório ou pseudo -aleatório,

 *  mas às vezes um IV só precisa ser imprevisível ou único.

 * 

 *  o AES tem um tamanho de bloco de 128 bits ou 16 bytes. O AES não altera o

 *  tamanho, e o tamanho do texto cifrado é igual ao tamanho do texto não

 *  criptografado. Além disso, nos modos ECB e CBC, devemos usar um algoritmo

 *  de preenchimento como PKCS 5. Portanto, o tamanho dos dados após a criptografia é: 256

 */

public class SymmetricCripto {     

      private static final String AES = "AES";

      private static final int ITERATION_COUNT = 65536;

      private static final int KEY_LENGTH = 256;

      private static final String ALGORIHM = "AES/CBC/PKCS5Padding";

      private static final String SECRET_KEY_FACTORY_INSTANCE = "PBKDF2WithHmacSHA256";

      private static final String ENCODING = "UTF-8";

// crie seu IV com 16 Bytes.

      private static final String IV = "8P7A6S5S4W3O2R1D";

 

      public static byte[] cripto(String text, SecretKey password )   {

            try {

                  return cripto(password, text.getBytes(ENCODING));

            } catch (Exception exception) {

                  exception.printStackTrace();

                  return new byte[0];

            }

      }

     

      private static byte[] cripto(SecretKey secretKey, byte[] text )   {

            try {

                  Cipher cipher = Cipher.getInstance(ALGORIHM);

                  cipher.init(Cipher.ENCRYPT_MODE, secretKey, generateIv());

                  return cipher.doFinal(text);

            } catch (Exception exception) {

                  exception.printStackTrace();

                  return new byte[0];

            }

      }

     

      public static byte[] descripto(SecretKey secretKey, byte[] text )   {

            try{

                  Cipher cipher = Cipher.getInstance(ALGORIHM);

                  cipher.init(Cipher.DECRYPT_MODE, secretKey, generateIv());

            return cipher.doFinal(text);

            } catch (Exception exception) {

                  exception.printStackTrace();

                  return new byte[0];

            }

      }

     

      public static SecretKey getSecretKey(String password, String salt){

            SecretKey secret = null;

            try {

                  SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_FACTORY_INSTANCE);                 

                  KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), ITERATION_COUNT, KEY_LENGTH);

                  secret = new SecretKeySpec(factory.generateSecret(keySpec)

                      .getEncoded(), AES);

            } catch (Exception e) {

                  e.printStackTrace();

            }

          return secret;

      }

     

      public static IvParameterSpec generateIv() {

        byte[] iv = IV.getBytes();

        return new IvParameterSpec(iv);

    }

}

 

public final class Util {

      private static final String SALT = "12345678";

// crie seu PASSWORD com 16 Bytes.

      private static final String PASSWORD = "P1A2S3S4W5O6R7D8";

      public static String createToken(String username) {

            Timestamp currentTimeStamp = new Timestamp(Calendar.getInstance().getTime().getTime());

            return username + "_" + currentTimeStamp.toString();

      }

 

      public static String encodeToken(String token) {

            SecretKey secretKey = SymmetricCripto.getSecretKey(PASSWORD, SALT);

            byte[] criptografado = SymmetricCripto.cripto(token, secretKey);

            return Base64.getUrlEncoder().encodeToString(criptografado);      

      }

 

      public static String decodeToken(String token) {

            byte[] decodedBytes = Base64.decode(token.getBytes());

            SecretKey secretKey = SymmetricCripto.getSecretKey(PASSWORD, SALT);

            token = new String(SymmetricCripto.descripto(secretKey, decodedBytes), Charset.forName("UTF-8"));

            return token;

      }

}

 

         E agora para testar a criptografia vamos ver a classe de teste:

 

public class UtilTeste {

      @Test

      public void criptografaTokenTest() {

            String token = Util.createToken("Daniel");

            String tokenCriptografado = Util.encodeToken(token);

            String tokenDescriptografado = Util.decodeToken(tokenCriptografado);

            Assert.assertTrue(token.equals(tokenDescriptografado));

      }

     

      @Test

      public void criptografaTokenTest2() {      

            String token = Util.createToken("Daniel");

            String token2 = Util.createToken("Gadiel");

            System.out.println(token);

            System.out.println(token2);

 

            String tokenCriptografado1 = Util.encodeToken(token);

            System.out.println(tokenCriptografado1);

 

            String tokenCriptografado2 = Util.encodeToken(token2);

            System.out.println(tokenCriptografado2);

 

            String tokenDescriptografado1 = Util.decodeToken(tokenCriptografado1);

            System.out.println(tokenDescriptografado1);

 

            String tokenDescriptografado2 = Util.decodeToken(tokenCriptografado2);

            System.out.println(tokenDescriptografado2);

 

             Assert.assertTrue(token.equals(tokenDescriptografado1) && token2.equals(tokenDescriptografado2));

      }

 

      @Test

      public void criptografaTokenTest3() {

            String token = "Daniel_2023-01-10 18:58:09.642";

            String token2 = "Gadiel_2023-01-10 18:58:09.642";

            System.out.println(token);

            System.out.println(token2);

 

            String tokenCriptografado1 = Util.encodeToken(token);

            System.out.println(tokenCriptografado1);

 

            String tokenCriptografado2 = Util.encodeToken(token2);

            System.out.println(tokenCriptografado2);

 

            String tokenDescriptografado1 = Util.decodeToken(tokenCriptografado1);

            System.out.println(tokenDescriptografado1);

 

            String tokenDescriptografado2 = Util.decodeToken(tokenCriptografado2);

            System.out.println(tokenDescriptografado2);

 

            Assert.assertTrue(
tokenCriptografado1.equals("KOlEIFpg8oWmgIPQlwIZ9Ibaz4DU0Fq3Fx7ZWHwmLKw="&&    tokenCriptografado2.equals("7NzCr0fzeSNy9X2Ib+mjuFPY3EmTM03gpThBW5q6A3o="));

      }

}

 

Importante: Para se ter sempre o mesmo resultado de criptografia e descriptografia para valores iguais após a reinicialização do servidor ou em servidores diferentes (para quem usa cluster ou coisa do tipo) o PassWord que gera a SecretKey e o IV devem ser criados com o mesmo valor sempre. Note que eles são constantes no código.

 

Dica: O token pode (e deve) ser melhorado com o acréscimo de um número randômico e mais dados de acordo com a necessidade. O PassWord e o IV também devem ser alterados de tempos em tempos.

 

Solução adaptada à necessidade que tive a partir das seguintes fontes:

https://www.devmedia.com.br/introducao-a-seguranca-da-informacao-em-java/28247

https://www.baeldung.com/java-aes-encryption-decryption

https://www.techtarget.com/searchsecurity/definition/cipher-block-chaining

https://stackoverflow.com/questions/54874072/aes-encryption-in-node-resulting-is-different-output-compared-to-java

 

 

 

Nenhum comentário:

Postar um comentário