O Hash é uma sequência de bits que tem como objetivo identificar um arquivo. Isso significa que se realizarmos um processamento num arquivo será gerado um Hash que é único, e dessa forma, alcançamos a garantia da integridade. Utilizando o Hash também temos a propriedade da unidirecionalidade onde o caminho de volta não é possível. Além disso, utilizando Hash não há a necessidade de chaves e, temos a garantia da consistência, pois se introduzirmos a mesma mensagem de Hash teremos exatamente o mesmo Hash sendo gerado. Por fim, o Hash também nos oferece as propriedades de randomicidade e unicidade onde nunca temos a mesma mensagem de Hash para diferentes mensagens.

Um dos problemas do Hash é que estamos vulneráveis a ataques de dicionário. Um ataque de dicionário é onde um atacante tem um banco de dados de senhas prováveis com seus respectivos Hashs. Como uma solução para este problema tem-se o SALT que é uma porção aleatória de texto que é concatenado com a senha original, e assim sendo, por exemplo, pessoas com mesma senha obterão Hashs diferentes.

A saída das mensagens criptografadas com Hash é independente da entrada, sempre temos como saída 128 bits. Por exemplo, uma letra "a" tem como saída 128 bits, um texto de 10 MB também gera como saída um Hash de 128 bits. Os Hashs também oferecem a noção de colisão que é quando duas mensagens produzem o mesmo Hash.

Entre as famílias dos Hash temos o MD (Message Digest) que é composta por MD2 (lento e com saída de 128 bits), MD3, MD4, MD5 que é o mais utilizado atualmente e por fim, a família SHA (Secure Hash Algorithm) projetada pela National Security Agency (NSA) publicadas como um padrão do governo Norte-Americano é composta pelos algoritmos SHA-1, SHA-224, SHA-256, SHA-384 e SHA-512. Cada um dos algoritmos se diferenciam pelo tamanho da mensagem de entrada suportada, tamanho do bloco, tamanho da palavra, tamanho do Message Digest e a segurança do algoritmo. A função mais utilizada nesta família, a SHA-1 sucessora do MD5, é usada numa grande variedade de aplicações e protocolos de segurança, incluindo TLS, SSL, PGP, SSH, S/MIME e IPSec. Tanto o SHA-1 quanto o MD5 têm vulnerabilidades comprovadas. Em algumas correntes, é sugerido que o SHA-256 ou superior a este seja utilizado para tecnologia crítica. A primeira função desta família foi publicada em 1993 e foi oficialmente chamada de SHA. Dois anos mais tarde foi publicado o SHA-1 que é o primeiro sucessor do SHA. Após isso foram lançadas mais quatro variantes com capacidades de saída aumentadas e um design diferente, são elas: SHA-224, SHA-256, SHA-384, e SHA-512 (ou SHA-2). Até o momento não foram reportados ataques às variantes SHA-2. O SHA tem tamanho de saída de 160 bits, tamanho dos blocos de 512 bits, comprimento de 64 bits, tamanho das palavras de 32 bits. O SHA-1 é semelhante ao SHA, porém não tem tantas colisões como o SHA. O SHA-256/224 tem tamanho de saída de 256/224 bits, tamanho dos blocos de 512 bits, comprimento de 64 bits, tamanho das palavras de 32 bits. O SHA-512/384 tem tamanho de saída de 512/384 bits, tamanho dos blocos de 1024 bits, comprimento de 128 bits, tamanho das palavras de 64 bits. Tanto o SHA-256/224 quanto o SHA-512/384 são livres colisões.

Utilizando Hash basicamente temos uma mensagem de entrada, essa mensagem será fatiada em pedaços de 512 bits que será submetida ao algoritmo e como saída temos 160 bits (dependendo do algoritmo isso muda) que nunca serão repetidos para diferentes mensagens. Com isso temos como garantia o controle de integridade, em que caso um único bit seja modificado teremos um Hash completamente diferente.

As assinaturas digitais usam basicamente os Hashs, visto que, dada uma mensagem que pode ter centenas de megabytes ela é processada por um Hash que terá um tamanho muito menor. Após isso, esse Hash é criptografado, assim ciframos bits de informação e não megabytes de informação que é então enviado ao receptor. Portanto, utilizando a assinatura digital temos a integridade e a autenticidade garantidas.

Nas próximas seções veremos como podemos criptografar senhas usando Hash em Java.

Criptografando Senhas Usando Hash em Java

O maior erro que um site pode cometer é guardar as senhas dos usuários em texto puro, o que faz com que um possível roubo de dados acarrete problemas ainda maiores.

Existem basicamente dois tipos de algoritmos de criptografia:

  • Os algoritmos de duas vias: é aquele em que é possível descriptografar a mensagem criptografada;
  • Os de algoritmos uma via: é aquele em que não há como descriptografar a mensagem, pois o cálculo matemático efetuado não permite, a partir do valor fim, retornar ao valor inicial.

Os algoritmos de uma via podem ser chamados de Message Digests, e são extremamente eficientes para armazenamento de senhas.

A Segurança da informação do computador muitas vezes exige que as Strings sejam criptografadas e isso pode ser feito através do Hash. O Hashing é semelhante a criptografia, exceto que um Hash não pode ser revertido, enquanto que a criptografia pode ser descriptografada. O uso mais comum para o Hash de uma String é para proteger as senhas. Normalmente, um sistema de computador não vai armazenar a senha atual. Ao invés disso, ele irá armazenar o Hash da senha, de modo que um atacante que venha a ganhar acesso ilegal ao banco de dados ainda não vai conseguir adquirir as senhas de usuários. As Strings são transformadas através de algoritmos de Hash. Um algoritmo de Hash muito popular é o SHA-256.

A API do Java implementa dois algoritmos de Message Digest, são elas:

  • MD5 (Message-Digest algorithm 5)
  • SHA (Secure Hash Algorithm)

Basicamente devemos guardar um Hash ou um "digest" da senha, usando algum algoritmo de Hash unidirecional. Podemos realizar esta operação utilizando a classe MessageDigest do pacote javax.security. Através desta classe podemos gerar o Hash de uma senha. Segue o exemplo da Listagem 1.

  package teste;
   
  import java.io.UnsupportedEncodingException;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
   
  public class TesteAlgoritmo {
   
           public static void main(String args []) throws NoSuchAlgorithmException, 
           UnsupportedEncodingException {
                     
                     String senha = "admin";
   
                     MessageDigest algorithm = MessageDigest.getInstance("MD5");
                     byte messageDigest[] = algorithm.digest(senha.getBytes("UTF-8"));
                     
                     System.out.println(messageDigest);
           }
           
  }
Listagem 1. Utilizando a API java para gerar o Hash das senhas

Agora esse Array de bytes gerado pode ser armazenado num banco de dados, arquivo, ou algum outro meio de armazenamento.

Quando o usuário fizer o login podemos fazer um Hash da senha digitada e comparar com a senha em Hash que está armazenada.

Para guardar esse array de bytes como uma String basta utilizarmos o código "new String(bytes, encoding)".

No exemplo acima para termos uma instância da classe MessageDigest, devemos proceder da seguinte forma:

//message digest para MD5
  MessageDigest md = MessageDigest.getInstance("MD5");

Após a chamada à getInstance() teremos uma referência a um objeto pronto para criptografar os dados utilizando o algoritmo especificado.

Também podemos utilizar outros algoritmos como o SHA.

Não podemos esquecer-nos de tratar devidamente as exceções, como por exemplo, NoSuchAlgorithmException.

O método update() recebe o que deve ser criptografado (em Array de bytes) e o método digest() efetua a criptografia, retornando um Array de bytes.

Portanto, para gerar a chave criptografada chamamos o método digest(). Segue abaixo as assinaturas existentes:

  byte[] digest();  
  byte[] digest(byte[] input);  
  int digest(byte[] buf, int offset, int len) throws DigestException;  

O primeiro método, realiza a operação nos bytes que foram fornecidos até o momento, isso se dá através do método update(). O segundo método, realiza um update() final, utilizando o array de bytes em "input" que é fornecido para o método, e por fim completa a operação. O terceiro e último método armazena no "buf" o resultado do hashing. As variáveis "offset" e "length" especificam onde, no array de destino, o hashing deve ser colocado. Este método retorna a quantidade de bytes escrita em "buf". Após a computação ser totalmente concluída, o método "digest()" chama o método "reset()" para devolver o algoritmo à seu estado inicial.

Apesar do algoritmo MD5 ter sido bastante utilizado, atualmente ele já é "quebrável" por força bruta, onde se descobre uma String que gere esse mesmo Hash.

Dessa forma, o SHA-2 atualmente é um dos mais utilizados. O código da Listagem 2 exemplifica como podemos utilizá-lo.

  package teste;
   
  import java.io.UnsupportedEncodingException;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
   
  public class TesteAlgoritmo {
   
           public static void main(String args []) throws NoSuchAlgorithmException, 
           UnsupportedEncodingException {
                     
                     String senha = "admin";
                     
                     MessageDigest algorithm = MessageDigest.getInstance("SHA-256");
                     byte messageDigest[] = algorithm.digest(senha.getBytes("UTF-8"));
                     
                     System.out.println(messageDigest);
           }
           
  }
Listagem 2. Utilizando a API java para gerar o Hash das senhas utilizando o algoritmo SHA-2

Outra boa prática é gravarmos a senha em formato hexadecimal ao invés do Hash puro conforme exemplificamos na Listagem 3.

  package teste;
   
  import java.io.UnsupportedEncodingException;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
   
  public class TesteAlgoritmo {
   
     public static void main(String args []) throws NoSuchAlgorithmException, 
     UnsupportedEncodingException {
               
         String senha = "admin";
         
         MessageDigest algorithm = MessageDigest.getInstance("SHA-256");
         byte messageDigest[] = algorithm.digest(senha.getBytes("UTF-8"));
          
         StringBuilder hexString = new StringBuilder();
         for (byte b : messageDigest) {
           hexString.append(String.format("%02X", 0xFF & b));
         }
         String senhahex = hexString.toString();
         
         System.out.println(senhahex);
     }
           
  }
Listagem 3. Utilizando a API java para gerar o Hash das senhas utilizando o algoritmo SHA-2 e transformando em formato hexadecimal

O loop for no código acima percorre os bytes e vai pegando os seus valores e convertendo para String – concatenando os valores com a classe StringBuilder.

Assim, teríamos uma senha como:

  "8C6976E5B5410415BDE908BD4DEE15DFB167A9C873FC4BB8A81F6F2AB448A918".

A máscara de bits em "hexString.append(Integer.toHexString(0xFF & b));" é utilizada para evitar números negativos.

Uma observação importante, como já foi dito antes, é que não é possível descriptografar essa senha, por isso as senhas estão teoricamente seguras.

Uma forma de validar a senha é comparar se os dois códigos hexadecimais são iguais conforme exemplificado no código da Listagem 4.

  package teste;
   
  import java.io.UnsupportedEncodingException;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
   
  public class TesteHexa {
   
  public static void main(String args []) throws NoSuchAlgorithmException, 
  UnsupportedEncodingException {
                     
     //-------------- Senha Admin
     String senhaAdmin = "admin";
     
     MessageDigest algorithm = MessageDigest.getInstance("SHA-256");
     byte messageDigestSenhaAdmin[] = algorithm.digest(senhaAdmin.getBytes("UTF-8"));
      
     StringBuilder hexStringSenhaAdmin = new StringBuilder();
     for (byte b : messageDigestSenhaAdmin) {
              hexStringSenhaAdmin.append(String.format("%02X", 0xFF & b));
     }
     String senhahexAdmin = hexStringSenhaAdmin.toString();
     
     System.out.println(senhahexAdmin);
     
     
     
     //-------------- Senha User
     
     String senhaUser = "user";
     
     byte messageDigestSenhaUser[] = algorithm.digest(senhaUser.getBytes("UTF-8"));
      
     StringBuilder hexStringSenhaUser = new StringBuilder();
     for (byte b : messageDigestSenhaUser) {
              hexStringSenhaUser.append(String.format("%02X", 0xFF & b));
     }
     String senhahexUser = hexStringSenhaUser.toString();
     
     System.out.println(senhahexUser);
     
     
     
     //--Comparando Senha User e Admin
     
     System.out.println(senhahexUser.equals(senhahexAdmin));
     
     
     
     //-------------- Senha Admin
     
     String senhaAdminNova = "admin";
     
     byte messageDigestSenhaAdminNova[] = algorithm.digest(senhaAdminNova.getBytes("UTF-8"));
      
     StringBuilder hexStringSenhaAdminNova = new StringBuilder();
     for (byte b : messageDigestSenhaAdminNova) {
              hexStringSenhaAdminNova.append(String.format("%02X", 0xFF & b));
     }
     String senhahexAdminNova = hexStringSenhaAdminNova.toString();
     
     System.out.println(senhahexAdminNova);
     
     
     
     //--Comparando Senha User e Admin
     
     System.out.println(senhahexAdminNova.equals(senhahexAdmin));
                        
     
}
           
  }
Listagem 4. Utilizando a API java para gerar o Hash das senhas em formato hexadecimal e compara-las

Como saída do exemplo acima teremos o seguinte resultado:

8C6976E5B5410415BDE908BD4DEE15DFB167A9C873FC4BB8A81F6F2AB448A918
04F8996DA763B7A969B1028EE3007569EAF3A635486DDAB211D512C85B9DF8FB
false
8C6976E5B5410415BDE908BD4DEE15DFB167A9C873FC4BB8A81F6F2AB448A918
true

No código acima, primeiramente instanciamos a classe MessageDigest que é responsável por fornecer a funcionalidade de mensagens seguras criptograficamente. A classe é instanciada pelo código "MessageDigest algorithm = MessageDigest.getInstance("SHA-256");".

O primeiro parâmetro do método estático getInstance() é o nome padrão do algoritmo que queremos usar. Outros algoritmos possíveis são: "MD4", "SHA-1", "SHA-224", "SHA-256", "SHA-384", "SHA-512", "RIPEMD128", "RIPEMD160", "RIPEMD256", "RIPEMD320", "Tiger", "DHA256", e "FORK256".

Após instanciarmos um MessageDigest válido podemos usar os métodos update(), digest(), entre outros.

O método update() pode ser utilizado quando quisermos atualizar uma informação no objeto MessageDigest. Ele pode ser chamado múltiplas vezes e sua assinatura é dada por "update(byte[] input)". O método update() retorna o valor desejado como um Array de bytes e reseta o objeto MessageDigest que pode ser usado para outras computações. Para fazer um reset manual podemos utiliza o método reset() a qualquer momento assim como podemos utilizar digest(byte[] input) sem precisar usar o método update().

Outra forma que, para quem preferir, é utilizar uma biblioteca pronta, largamente testada e utilizada na comunidade. Dessa forma, podemos utilizar a biblioteca Apache Commons Codec que está disponível no seu site oficial em Apache Commons Codec.

A classe DigestUtils possui métodos estáticos como sha512hex(String), que já devolve o resultado codificado.

Na próxima seção veremos mais sobre o que é essa biblioteca, como podemos utilizá-la e faremos um exemplo prático.

Codificando e Decodificando dados Utilizando a Biblioteca Apache Codec

O Apache Commons Codec oferece a implementação de codificadores e decodificadores como: “Base64”, “Hex”, “Phonetic” e “URLs”.

Entre os métodos de codificação acima o “Base64” é um método para codificação de dados para transferência na Internet. Ele é utilizado frequentemente para transmitir dados binários através de meios de transmissão que lidam apenas com texto, como por exemplo, o envio de arquivos anexos por e-mail. O “Base64” é constituído por 64 caracteres ([A-Za-z0-9], "/" e "+") e por isso o motivo do seu nome. Esse alfabeto com todos os símbolos que são utilizados na codificação estão presente na RFC 3548. o alfabeto contém 65 caracteres compatíveis com quase todo tipo de codificação existente como o UTF-8, ISO-8859-1, etc. Apesar do normal é dizermos 64 caracteres, temos que em algumas implementações o caractere ‘=’ (igual) é usado como padding (preenchimento).

Portanto, quando precisamos transferir e armazenar dados na forma textual, evitando que dados binários sejam manipulados diretamente, a codificação Base64 fornece um jeito mais simples de transformar essa cadeia binária em texto. Para maiores detalhes sobre o Base64 podemos acessar diretamente a RFC 3548 que está disponível através do site The Base16, Base32, and Base64 Data Encodings.

Para baixar o Apache Commons Codec através do Maven podemos utilizar a dependência da Listagem 5.

  <dependency>
      <groupId>org.apache.directory.studio</groupId>
      <artifactId>org.apache.commons.codec</artifactId>
      <version>1.9</version>
  </dependency>
Listagem 5. Dependência no Maven para baixar o Apache Commons Codec

Ou ainda podemos baixar diretamente no site Download Apache Commons Codec.

Após baixar o Apache Commons Codec devemos descompactar os arquivos que estão no zip. Se estivermos usando o Eclipse basta clicar com o botão direito do mouse em cima do nome do projeto e ir em “Propriedades”. Após isso clique na aba “Java Build Path”, conforme mostra a Figura 1.

Adicionando o JAR da biblioteca no Eclipse
Figura 1. Adicionando o JAR da biblioteca no Eclipse

Feito isso, clique em “Add External Jar” e selecione o JAR no local onde ele foi descompactado.

O Commons representa uma coleção de componentes Java reutilizáveis e, como parte de um projeto maior, o pacote Codec oferece codificadores e decodificadores para uma série de formatos de dados em texto e binários, incluindo Base64 e Hexadecimal. O projeto Codec também mantém uma coleção de utilitários para codificação fonética.

Devemos atentar para as versões mínimas do Java que são requeridas para as últimas versões do Apache Commons Codec. O Apache Commons Codec 1.9 requer Java 1.6, o Apache Commons Codec 1.8 requer Java 1.6, Apache Commons Codec 1.7 requer Java 1.6, Apache Commons Codec 1.6 requer Java 1.5, Apache Commons Codec 1.5 requer Java 1.4 e o Apache Commons Codec 1.4 requer Java 1.4.

O Apache Commons Codec 1.7 adicionou diversas novas funcionalidade na API como a adição do MD2, alteração DigestUtils.getDigest(String) para lançar IllegalArgumentException ao invés de RuntimeException, adição da classe MessageDigestAlgorithms para definir algoritmos padrão, alteração na exceção do DigestUtils.getDigest(String), adição de testes de regressão, BeiderMorseEncoder e PhoneticEngine com resultados determinísticos utilizando um LinkedHashSet ao invés de um HashSet, adicionado o método updateDigest, adicionadas as classes MD5/SHA1/SHA-512 para Unix crypt(3), método encode() não é mais thread-safe, entre outras novas características e alterações.

O Apache Commons Codec 1.8 trouxe como atualizações a adição do método "DigestUtils.updateDigest(MessageDigest, InputStream)", adição da JUnit para testes, adição do algoritmo fonético Match Rating Approach (MRA), alteração do Base64.encodeBase64URLSafeString para não adicionar padding no final.

Por fim, a última versão Apache Commons Codec 1.9 lançada mais recentemente trouxe como melhorias um aumento de desempenho do Beider Morse, o Beider Morse também teve como melhoria não fechar mais o Scanner usado para ler arquivos de configurações, e melhorias no Javadoc que tinha comentários desatualizados e links quebrados em relação a última versão.

Uma lista completa de novos recursos e dos problemas corrigidos podem ser encontrados nos anúncios que são feitos através da lista de discussão do projeto e nas notas da versão. Binários e código-fonte para o Apache Commons Codec 1.7, Apache Commons Codec 1.8 e Apache Commons Codec 1.9 podem ser baixados no site do projeto.

Outro artefato muito importante que os desenvolvedores precisam é o guia do usuário. Cada uma das versões do Apache Commons Codec possui seu próprio guia, o link para cada um dos guias para as suas respectivas versões estão disponíveis abaixo:

No guia encontramos os detalhes de cada uma das interfaces, classes e métodos do Apache Commons Codec.

No exemplo da Listagem 6 demonstramos como implementar a codificação e a decodificação de uma mensagem. Segue abaixo o código:

  package teste; 
  import java.util.Arrays;
  import org.apache.commons.codec.binary.Base64;
  public class TesteApacheCodec {
   
    public static void main(String[] args) {
              
            String helloWorld = "Hello World!";
  
            helloWorld = Base64.encodeBase64String(helloWorld.getBytes());
            
         System.out.println("String codificada " + helloWorld);
   
         //
         // Decodifica uma string anteriormente codificada usando o método decodeBase64 e
         // passando o byte[] da string codificada
         //
         byte[] decoded = Base64.decodeBase64(helloWorld.getBytes());
   
         //
         // Imprime o array decodificado
         //
         System.out.println(Arrays.toString(decoded));
   
         //
         // Converte o byte[] decodificado de volta para a string original e imprime
         // o resultado.
         //
         String decodedString = new String(decoded);
         System.out.println(helloWorld + " = " + decodedString);
       }
           
  }
Listagem 6. Codificando e Decodificando dados com Apache Commons Codec

Segue abaixo a saída do código acima:

String codificada SGVsbG8gV29ybGQh
  [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]
  SGVsbG8gV29ybGQh = Hello World!

Uma prática mais aconselhada quando utilizamos o Apache Commons Codec é organizarmos a parte de codificação e decodificação em seus próprios métodos, ao invés de reutilizarmos o código inúmeras vezes no método principal. Dessa forma, segue na Listagem 7 um exemplo de como poderíamos organizar melhor esse código.

  import org.apache.commons.codec.binary.Base64;
   
  public class EncriptaDecriptaApacheCodec {
   
         /**
       * Codifica string na base 64 (Encoder)
       */
      public static String codificaBase64Encoder(String msg) {
          return new Base64().encodeToString(msg.getBytes());
      }
   
      /**
       * Decodifica string na base 64 (Decoder)
       */
      public static String decodificaBase64Decoder(String msg) {
          return new String(new Base64().decode(msg));
      }
   
      public static void main(String[] args) {
   
          String msgCodificada = codificaBase64Encoder("Exemplo de mensagem em texto puro.");
   
          String msgDecodificada = decodificaBase64Decoder(msgCodificada);
   
          System.out.println("Mensagem Codificada: " + msgCodificada);
          System.out.println("Mensagem Decodificada: " + msgDecodificada);
   
      }
         
  }
Listagem 7. Organizando de forma mais adequada a codificação e a decodificação dos dados com Apache Commons Codec

Como saída para o código acima teremos como resultado:

Mensagem Codificada: RXhlbXBsbyBkZSBtZW5zYWdlbSBlbSB0ZXh0byBwdXJvLg==
Mensagem Decodificada: Exemplo de mensagem em texto puro.
Listagem 1. NOME
Bibliografia: