Categorias

Introdução ao suporte pela API Java

Introdução

Este tutorial tem por finalidade apresentar uma introdução ao suporte provido pela API Java para a criptografia e a verificação de dados através de assinaturas digitais.

Ele está dividido em duas partes:

  • Message Digest e
  • Assinaturas Digitais

No decorrer deste documento, será apresentado a importância da criptografia de dados e a utilização de assinaturas digitais para a validação dos mesmos.

Todo o suporte para a criptografia que será usado nesse tutorial está disponível no Java desde a versão 1.2 do JSDK. Então, tudo o que você precisa é uma versão igual ou superior a esta.

Você pode encontrar a versão mais nova do JSDK em https://java.sun.com/j2se/

A preocupação com a segurança dos dados é antiga. Os romanos já utilizavam códigos secretos para comunicar planos de batalha. A criptografia foi largamente utilizada na Segunda Guerra Mundial, para comunicar planos de ação, posicionamento de tropas e trocar mensagens entre altas patentes.

Mas foi só com a invenção do computador que a criptografia mostrou todo seu poder, utilizando-se de complexos algoritmos matemáticos para geração de seus códigos.

O Java, que possui como um de seus lemas a segurança, nos fornece uma API poderosa para se trabalhar com criptografia de dados. Os dois métodos abordados neste tutorial são apresentados abaixo.

Message digests são funções hash que geram código de tamanho fixo, em uma única direção, a partir de dados de tamanho arbitrário.

“Em uma única direção” significa que a código não pode ser descriptografado. Qual é então sua utilidade?

Esses códigos hash são extremamente úteis para segurança de senhas. Como ele não pode ser descriptografado, o código hash precisa ser regerado e comparado com a sequência disponível anteriormente. Se ambos se igualarem, o acesso é liberado.

A API Java implementa dois algoritmos de Message Digest: o MD5 e o SHA-1. Para aqueles mais entusiasmados, a especificação técnica do MD5 pode ser ancontrada aqui e as especificações do SHA-1 em www.faqs.org/rfcs/rfc1321.html.

As assinaturas digitais servem para autenticar o remetente da informação e se ter a garantia de que o dado é confiável. As assinaturas digitais trabalham com uma tríade: a assinatura em si, uma chave pública e uma chave privada.

É de responsabilidade do remetente gerar a tríade e fornecer a chave pública aos destinatários de suas mensagens. No ato do envio, o remetente gera uma assinatura para o dado que deseja enviar usando a chave privada. O destinatário recebe o dado e a assinatura, e valida a informação usando sua chave pública.

Se a validação for efetuada com sucesso, o destinatário tem a garantia de que a mensagem foi enviada por um remetente confiável, possuidor da chave privada.

Será apresentado agora a API do Message Digest e um exemplo de como utulizá-lo.

Para que se possa gerar códigos cripografados, é necessário seguir os seguintes passos.

  • 1 – Obter uma instância do algoritmo a ser usado.
  • 2 – Passar a informação que se deseja criptografar para o algoritmo.
  • 3 – Realizar a criptografia.

Estes passos são abordados com detalhes abaixo.

Para se obter uma instância de um algoritmo de criptografia message digest, utiliza-se o método getInstance() da classe MessageDigest.

Como dito anteriormente, Java possui dois algoritmos embutidos, o MD5 e o SHA-1.

1 MessageDigest md5 = MessageDigest.getInstance("MD5");
2 MessageDigest sha1 = MessageDigest.getInstance("SHA-1");  

A API javadoc lista ainda dois outros métodos getInstance(), no qual você passa o algoritmo a ser utilizado e um provider onde o algoritmo pode ser encontrado. A utilização de providers, entretanto, foge ao escopo deste tutorial.

1 MessageDigest.getInstance(String algorithm, Provider provider);
2 MessageDigest.getInstance(String algorithm, String provider);  

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

Quando você obtém uma instancia de um algoritmo, você recebe esta instância pronta para uso. A partir daí, você deve chamar o método update() para passar os dados a serem criptografados.

As assinaturas existentes são (repare que o código é apenas de demonstração, isso não compila):

01 //Faz o update do digest utilizando o byte especificado
02 //Este método é util quando não se tem controle do tamanho da mensagem a ser criptografada
03 //(por exemplo, vinda de um stream)
04 
05 void update(byte input);
06     //Exemplo
07     int i;
08 
09     while ((i = inputStream.read()) != -1) {
10         md5.update((byte)i);
11     }
12 
13     //Esta outra versão, faz o update a partir de um array de bytes
14     void update(byte[] input);
15 
16     //Exemplo
17     String str = "Elvis is live!!!";
18     sha1.update(str.getBytes());
19 
20     //Finalmente, ainda é possível especificar uma porção do array
21     void update(byte[] input, int offset, int len);  

A qualquer momento, você pode chamar o método reset() para que seu algoritmo volte ao estado original (antes de ter sido chamado qualquer update()).

Finalmente, para gerar a chave criptografada, você chama o método digest().

Eis a assinaturas existentes:

1 byte[] digest();
2 byte[] digest(byte[] input);
3 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 (através do método update()).

O segundo método, realiza um update() final, utilizando o array de bytes fornecido, e completa a operação.

O terceiro método armazena em buf o resultado do hashing. 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 concluida, o método digest() chama reset() para devolver o algoritmo à seu estado inicial.

Será construída neste tutorial uma classe que nos ajudará nos processos de criptografia. Ela será chamada de CriptoUtils.

Nota: Esta classe possui dois métodos utilitários: byteArrayToHexString() e hexStringToByteArray(). Esses métodos criam uma representação haxadecimal do código hash gerado, para poder ser facilmente gravado em bancos de dados e transportado via HTTP.

01 public final class CriptoUtils {
02     private static final String hexDigits = "0123456789abcdef";
03     /**
04     * Realiza um digest em um array de bytes através do algoritmo especificado
05     * @param input - O array de bytes a ser criptografado
06     * @param algoritmo - O algoritmo a ser utilizado
07     * @return byte[] - O resultado da criptografia
08     * @throws NoSuchAlgorithmException - Caso o algoritmo fornecido não seja
09     * válido
10     */
11     public static byte[] digest(byte[] input, String algoritmo)
12         throws NoSuchAlgorithmException {
13         MessageDigest md = MessageDigest.getInstance(algoritmo);
14         md.reset();
15         return md.digest(input);
16     }
17   
18     /**
19      * Converte o array de bytes em uma representação hexadecimal.
20      * @param input - O array de bytes a ser convertido.
21      * @return Uma String com a representação hexa do array
22      */
23     public static String byteArrayToHexString(byte[] b) {
24         StringBuffer buf = new StringBuffer();
25     
26         for (int i = 0; i < b.length; i++) {
27             int j = ((int) b[i]) & 0xFF; 
28             buf.append(hexDigits.charAt(j / 16)); 
29             buf.append(hexDigits.charAt(j % 16)); 
30         }
31         
32         return buf.toString();
33     }
34   
35     /**
36      * Converte uma String hexa no array de bytes correspondente.
37      * @param hexa - A String hexa
38      * @return O vetor de bytes
39      * @throws IllegalArgumentException - Caso a String não sej auma
40      * representação haxadecimal válida
41      */
42     public static byte[] hexStringToByteArray(String hexa)
43         throws IllegalArgumentException {
44       
45         //verifica se a String possui uma quantidade par de elementos
46         if (hexa.length() % 2 != 0) {
47             throw new IllegalArgumentException("String hexa inválida");  
48         }
49       
50         byte[] b = new byte[hexa.length() / 2];
51       
52         for (int i = 0; i < hexa.length(); i+=2) {
53             b[i / 2] = (byte) ((hexDigits.indexOf(hexa.charAt(i)) << 4) |
54                 (hexDigits.indexOf(hexa.charAt(i + 1))));          
55         }
56         return b;
57     }
58 }  

Eis aqui um exemplo para utilizar esta classe de utilitário: a senha digitada por um usuário deve ser comparada com a senha criptografada pelo algoritmo md5 gravada no banco.

01 stmt = con.prepareStatement("select * from users where login = ?");
02 stmt.setString(1, user.getLogin());
03 ResultSet rs = stmt.executeQuery();
04       
05 if (rs.next()) {
06     senhaNoBanco = rs.getString("senha");
07 } else {
08     throw new MinhaException("Usuário " + user.getLogin() + " não encontrado");
09 }
10   
11 try {
12     byte[] b = CriptoUtils.digest(user.getSenha().getBytes(), "md5");
13 } catch (NoSuchAlgorithmException e) {
14     e.printStackTrace();
15     return false;
16 }
17 
18 String senhaCriptografada = CriptoUtils.byteArrayToHexString(b);
19 
20 if (senhaNoBanco.equalsIgnoreCase(senhaCriptografada )) {
21     return true;
22 } else {
23     return false;
24 }  

E isso é tudo.

Como dito anteriormente, assinaturas digitais server para autenticar o dado sendo transmitido. Junto com o dado, uma assinatura digital é enviada. Comparada a uma chave pública, a assinatura é validada ou rejeitada.

A assinatura possui duas propriedades:

Dada a chave pública correspondente a chave privada usada para gerar a assinatura, é possível verificar a autenticidade e integridade dos dados sendo transmitidos.

A assinatura e a chave pública nada revelam sobre a chave privada.

A classe responsável no java por gerar as assinaturas digitais é chamada, não coincidentemente, de Signature.

Objetos signature são criados atavés da chamada ao método da classe Signature chamado

1 static Signature getInstance(String algorithm)
2 //Exemplo
3 Signature sig = Signature.getInstance("DSA");  

Nota: durante o desenvolvimento deste tutorial, apenas o algoritmo DSA será utilizado para geração das assinaturas digitais. Consulte a API Java para obter conhecimento dos demais algoritmos.
Antes de ser usada, a instância de um Objeto Signature precisa ser inicializada com um achave privada.

A geração das chaves é possível através da classe KeyPairGenerator. Assim como a classe Signature, a KeyPairGenerator necessita de um algoritmo para gerar as chaves.

1 static KeyPairGenerator getInstance(String algorithm)
2 //Exemplo
3 KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");  

Note que tanto as chaves quanto a assinatura devem ser geradas utilizando-se o mesmo algoritmo de criptografia.
Após a obtenção de um KeyPairGenerator, devemos inicializa-lo através do método initialize(). Existem quatro versões deste método, mas por questões de simplicidade, usaremos uma bastante comum:

1 void initialize(int keysize, SecureRandom random)  

Este método requer dois parâmetros: um tamanho de chave e um número aleatório.

A classe responsável por nos fornecer esse numero aleatório é chamada de SecureRandom. Ela gera númeors aleatórios que dificilmente se repetirão.

O tamanho da chave deve ser compatível com o algoritmo sendo usado. no caso do DSA, podemos usar um tamanho de 512.

Após inicializada, a KeyPairGenerator está pronta para gerar nossas chaves pública e privada.

1 KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA");
2 SecureRandom secRan = new SecureRandom();
3 keyGen.initialize(512, secRan);
4 KeyPair keyP = keyGen.generateKeyPair();
5 PublicKey pubKey = keyP.getPublic();
6 PrivateKey priKey = keyP.getPrivate();  

De posse de nossa chave privada, podemos utilizá-la para inicializar nosso objeto signature:

1 sig.initSign(priKey);  

A partir de agora, podemos utilizar o método update() para passar ao algoritmo os dados a serem criptografados. Após o dado ser fornecido, o método sign deve ser chamado para geração da assinatura.

[code]1 String mensagem = "Elvis is Live!!!";
2 //Gerar assinatura
3 sign.update(mensagem.getBytes());
4 byte[ ] assinatura = sign.sign();[/code]

Nota: o método sign reseta o status do algoritmo ao seu estado inicial.

Aqui terminam as responsabilidades do remetente. Tudo o que ele precisa fazer agora é fornecer a chave pública juntamente com o dado a ser enviado e a assinatura correspondente.

O destinatário dos dados sendo enviados deverá receber, juntamente, a assinatura digital e ter acesso a chave pública.

Ele então poderá validar a assinatura junto ao dado recebido utilizando sua chave pública.

1 Signature clientSig = Signature.getInstance("DSA");
2 clientSig.initVerify(pubKey);
3 clientSig.update(mensagem.getBytes());
4 
5 if (clientSig.verify(assinatura)) {
6     //Mensagem assinada corretamente
7 } else {
8     //Mensagem não pode ser validada
9 }  

Existe um pequeno exemplo de utilização de assinaturas digitais, que acompanha este tutorial. O remetente criará uma String, as chaves públicas e privadas e a assinatura correspondente. Ele serializará a chave pública (para que possa ser lida pelo destinatério) e gravará a String, juntamente com a assinatura digital em um arquivo de propriedades.

O destinatário lerá então deste arquivo e validará o dado com sua chave pública.