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

 

 

 

quinta-feira, 3 de novembro de 2022

Pegando um arquivo de uma URL com Java usando Proxy

      As vezes nem tudo é tão simples. No artigo anterior eu mostrei como pegar um arquivo com Java a partir de uma URL, porém na maioria das empresas para se ter acesso a internet é preciso usar um proxy devido a configurações de segurança e tal. Então para acessar arquivos de URLs sem usar proxy provavelmente só vai rolar se for de URLs internas da empresa. Mas calma, usar o proxy muda pouca coisa no nosso código e vou seguir com os mesmos exemplos. Como no artigo anterior isso pode ser aplicado a um PDF, uma imagem, etc. Vou mostrar uma forma apenas com Java puro e outra que usa o org.apache.commons.io.IOUtils para copiar do InputStream para o OutputStream.

      No problema em questão a necessidade original era buscar um arquivo e retorná-lo em memória para um método que irá guardá-lo no banco de dados.

       Segue o exemplo de código apenas com o Java puro:

try {
    URL url = new URL("http://www.pdf995.com/samples/pdf.pdf");
    
    //cria o proxy com ip e porta do proxy
    URLConnection openConnection = url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("ip_do_proxy", 8080)));
    
    // muda a chamado do stream
    InputStream in = new BufferedInputStream(openConnection.getInputStream());
    ByteArrayOutputStream out = new ByteArrayOutputStream();
   
    byte[] buf = new byte[121072]; //quantidade de bytes lida por vez
    int i = 0;
   
    while (-1 != (i = in.read(buf))){
         out.write(buf, 0, i);
    }
   
    out.close();
    in.close();
   
    byte[] dadosRecebidos = out.toByteArray();
           
    File pdf = File.createTempFile("nomedoc", ".pdf"); //cria arquivo temporário
    FileOutputStream fos = new FileOutputStream(pdf); // passa o arquivo para o fos
    fos.write(dadosRecebidos); //escreve os dados no arquivo criado
    fos.close();
                 
    return pdf;
                 
           
} catch (Exception e) {
    e.printStackTrace();
}

       Segue o exemplo usando o apache commons:

try {
    URL url = new URL("http://www.pdf995.com/samples/pdf.pdf");

    //cria o proxy com ip e porta do proxy
   URLConnection openConnection = url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("ip_do_proxy", 8080)));

    InputStream in = new BufferedInputStream(
openConnection.getInputStream());
   
    File pdf = File.createTempFile("nomedoc", ".pdf"); //cria arquivo temporário
    FileOutputStream fos = new FileOutputStream(pdf); // passa o arquivo para o fos
       
    IOUtils.copy(in, fos); // usando org.apache.commons.io.IOUtils para passar os dados para o arquivo
   
    in.close();
    fos.close();
    return pdf;
 
} catch (Exception e) {
    e.printStackTrace();
}

       Para este artigo é isso. Se quiser saber como fazer sem o proxy está aqui o artigo:

Pegando um arquivo de uma URL com Java

Pegando um arquivo de uma URL com Java

     Nesse artigo vou mostrar como ler um arquivo de uma URL. Isso pode ser aplicado a um PDF, uma imagem, etc. Vou mostrar uma forma apenas com Java puro e outra que usa o org.apache.commons.io.IOUtils para copiar do InputStream para o OutputStream.

      No problema em questão a necessidade original era buscar um arquivo e retorná-lo em memória para um método que irá guardá-lo no banco de dados.

       Segue o exemplo de código apenas com o Java puro:

try {
    URL url = new URL("http://www.pdf995.com/samples/pdf.pdf");
    InputStream in = new BufferedInputStream(url.openStream());
    ByteArrayOutputStream out = new ByteArrayOutputStream();
   
    byte[] buf = new byte[121072]; //quantidade de bytes lida por vez
    int i = 0;
   
    while (-1 != (i = in.read(buf))){
         out.write(buf, 0, i);
    }
   
    out.close();
    in.close();
   
    byte[] dadosRecebidos = out.toByteArray();
           
    File pdf = File.createTempFile("nomedoc", ".pdf"); //cria arquivo temporário
    FileOutputStream fos = new FileOutputStream(pdf); // passa o arquivo para o fos
    fos.write(dadosRecebidos); //escreve os dados no arquivo criado
    fos.close();
                 
    return pdf;
                 
           
} catch (Exception e) {
    e.printStackTrace();
}

       Segue o exemplo usando o apache commons:

try {
    URL url = new URL("http://www.pdf995.com/samples/pdf.pdf");
    InputStream in = new BufferedInputStream(url.openStream());
   
    File pdf = File.createTempFile("nomedoc", ".pdf"); //cria arquivo temporário
    FileOutputStream fos = new FileOutputStream(pdf); // passa o arquivo para o fos
       
    IOUtils.copy(in, fos); // usando org.apache.commons.io.IOUtils para passar os dados para o arquivo
   
    in.close();
    fos.close();
    return pdf;
 
} catch (Exception e) {
    e.printStackTrace();
}

       Para este artigo é isso.

sábado, 23 de julho de 2022

Como e porquê configurar o cache do Hibernate

Nesse artigo vou trazer algo que vi sobre a importância de fazer uso correto de cache do Hibernate em um artigo do Vlad Mihalcea que deixo no final para que possam acessar e ver outros detalhes como o gráfico que mostra a diferença de tempo entre a consulta que usa e a que não usa o cache.

 

Não vou me prender a demonstração da melhora de performance pois isto está no artigo dele. Aqui quero apenas mostrar a configuração necessária e dizer o porquê configurar o cache de acordo com o tamanho da sua aplicação.

 

As consultas JPQL são compiladas antes da execução, e como sabemos, há um custo para se compilar um código. Então, caso o cache não esteja configurado corretamente, todas as vezes que as consultas forem executas elas serão compiladas, gerando sempre esse custo adicional. As consultas usando Criteria API também passam por esse impacto porque são transformadas em JPQL;

 

Por padrão o Hibernate coloca 2048 consultas no cache, mas se seu sistema for muito grande isso pode não ser suficiente.

 

A propriedade que trata dessa configuração é a hibernate.query.plan_cache_max_size. Basta calcular quantas consultas seu sistema tem e configurar essa propriedade para atender essa demanda. Lembrando que o cache não armazena apenas JPQL, então o valor precisa ser a soma de todas as consultas do sistema.

 

No caso que Native Queries, embora não haja esse processo de compilação, o hibernate guarda metadados sobre os parâmetros passados, e a configuração desse cache é feita na propriedade hibernate.query.plan_parameter_metadata_max_size. O valor default é 128, mas como mostrado no artigo do Mihalcea, a diferença para Native Queries não é tão grande com ou sem o cache configurado.


Você pode configurar essas propriedades no persistence.xml como abaixo:


<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <! – consultas - default: 2048 – >
            <property name="hibernate.query.plan_cache_max_size" value="4096"/>
            <! – metadados - default: 128 – >
            <property name="hibernate.query.plan_parameter_metadata_max_size"
                value="256"/>
        </properties>
    </persistence-unit>

</persistence>


Fonte: https://vladmihalcea.com/hibernate-query-plan-cache/