Android

6 jul, 2017

Java Cryptography Extension no Android

Publicidade

O Java quase sempre foi desenhado pensando na extensão de suas funcionalidades. Por exemplo, definindo interfaces de APIs como JAX-RS (Java API for RESTful Web Services) e as anotações de JAXB (Java Architecture for XML Binding). Assim, surgiram frameworks que proveem diversos recursos extras sobre o conjunto padrão de requisitos das especificações.

Um dos pontos de extensão das funcionalidades do Java SE é uma especificação de provedores de algoritmos de criptografia chamada JCE (Java Cryptography Extension) – esta extensão nos dá uma forma de adicionar provedores distintos de criptografia. Com isso, conseguimos usar algoritmos que não estão no padrão do JRE (Java Runtime Environment) e também podemos, por meio desta arquitetura, alterar as configurações de limite de força dos algoritmos.

Talvez aqui valha a pergunta: mas por que faríamos isso? Um motivo é simplesmente usar criptografia mais forte do que aquela que a plataforma fornece. Um bom exemplo é o algoritmo de AES (Advanced Encryption Standard) de criptografia de bloco simétrica. Por padrão, o Java possui um limite para o tamanho das chaves, de 128 bits. É possível estender esse mínimo usando o provedor de criptografia padrão, mas para isso é necessário customizar a JRE instalando um arquivo extra de “policy”, também conhecido como “unlimited strenght” (força ilimitada). Há diversos motivos para isso: o limite não é algo que a Sun ou a Oracle criaram de suas cabeças, mas um limite sobre a força da criptografia de produtos para consumidores imposta pelas agências americanas. Não pretendo entrar na discussão política deste assunto, mas como esta limitação não se faz presente em todos os lugares do mundo, há a opção de sobrescrever este limite.

Independentemente dos motivos, poder adicionar outro provedor de criptografia, e customizar os limites do JCA (Java Cryptography Architecture), é sempre bom. Um dos motivos é para que seja possível comparar as implementações destes algoritmos tão sensíveis, ou seja, para que seja possível testar algoritmos menos convencionais que não fazem parte da JRE.

Nessa linha, o Android decidiu usar um provedor diferente por padrão (além de, claro, não poder usar o padrão da JRE por ser proprietário). O projeto escolhido para ser o padrão é o Bouncy Castle, que atualmente tem 15 anos e é financiado por uma organização sem lucros chamada Legion of the Bouncy Castle Inc.

Essa solução traz vantagens e desvantagens para o desenvolvedor Android. A maior vantagem nesta abordagem, já citada, é a presença de mais algoritmos mantendo a compatibilidade com a API do Java por meio do JCE, ou seja, quando queremos gerar uma chave AES, podemos inicializar uma instância de KeyGenerator da seguinte forma:

try {
   final KeyGenerator keyGen = KeyGenerator.getInstance("AES", "BC");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
   // do something
}

Neste trecho do código, pedimos uma instância de KeyGenerator para o algoritmo AES e passamos o provedor que queremos – no caso, “BC” para Bouncy Castle. Assim, usamos a API do pacote javax.crypto, mas utilizamos a implementação do Bouncy Castle.

Nesse caso, poderíamos em seguida usar a variável “keyGen” para inicializar seu estado interno de acordo com o tamanho da chave que queremos:

keyGen.init(256);

É nesse ponto que podemos começar a ter problemas. Lembra do limite padrão que a JRE traz? O código acima irá executar tranquilamente em um emulador Android (que não possui esse limite), porém não irá executar em uma JRE padrão da Oracle. E aí todos os tipos de problemas podem aparecer… Como você irá testar seu código de criptografia, em geral um teste local, já que usa mais lógica de negócio do que lógica de interação? Se for tentar usar Robolectric ou algo do tipo, continuará tendo problemas por causa da diferença do provedor padrão de JCE da JVM Oracle e da plataforma Android.

Outra desvantagem problemática do uso de um provider padrão na plataforma é sua atualização. Por exemplo, na versão 4.0 do Android (API 14), o Bouncy Castle que está embarcado é o da versão 1.46 (de acordo com esta issue), mas a versão mais atual é a 1.56 no momento da escrita deste artigo. Se dermos uma olhada no changelog, veremos que várias correções foram implantadas em diversas versões. Assim, por mais que utilizemos o Bouncy Castle nos exemplos a seguir, vale a pena lembrar que bugs em produção podem estar relacionados a versões do Bouncy Castle.

Caso sua aplicação dependa de algoritmos de criptografia, é necessário ponderar bastante a sua decisão de arquitetura para não ter problemas de segurança. Uma saída para o problema de atualização é embarcar um terceiro provedor de JCE. Por exemplo, o Spongy Castle. Esse provider é um fork do Bouncy Castle só que com o pacote e o nome do provider alterado para não termos conflitos de classes nas aplicações Android. Se tentarmos simplesmente colocar um jar com a versão mais atualizada do Bouncy Castle, teremos problemas de ClassLoader, já que o Bouncy Castle está disponível no loadpath das aplicações. Esse cenário, no entanto, só irá funcionar no Android, pois os provedores de JCE devem ser assinados pela Oracle para serem aceitos como implementações em sua JVM.

Com isso, será que conseguimos testar nosso código diretamente na JVM? Vejamos com o seguinte exemplo:

public static void main(String... args) throws Exception {
    final KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    System.out.println("O provider que usamos é " + keyGen.getProvider().getName());
    keyGen.init(256);
    final SecretKey secretKey = keyGen.generateKey();
    System.out.println("O tamanho da chave é " + (secretKey.getEncoded().length * 8));
}

Se você não alterou nada na sua JRE, então você deve receber uma exceção assim:

java.security.InvalidKeyException: Illegal key size

Mas e agora? Instalo o Bouncy Castle como um provedor de criptografia da minha JRE? Hmmm… no Android ele já está no nosso loadpath, então só precisaria de uma dependência para o nosso teste:

// Dependência de teste local
    testCompile 'org.bouncycastle:bcprov-jdk16:1.46'

E agora podemos instalar o provider em nossos testes:

public class CryptoTest {

    static {
        Security.insertProviderAt(new BouncyCastleProvider(), 1);
    }

    @Test
    public void canSetProvider() throws Exception {
        // passando o provider como segundo parâmetro
        final KeyGenerator keyGen = KeyGenerator.getInstance("AES", "BC");
        Assert.assertEquals(keyGen.getProvider().getName(), "BC");
        keyGen.init(256);
        final SecretKey secretKey = keyGen.generateKey();
        // Tudo ok com o tamanho!
        Assert.assertEquals(secretKey.getEncoded().length * 8, 256);
    }
}

Essa é quase uma saída, mas temos alguns problemas.

Primeiro, ainda temos que instalar o “policy” de força ilimitada da JRE. Esse limite é independente do provedor de JCE.

Outro problema é que em vez de usarmos o padrão da plataforma, estamos passando o provider explicitamente no método getInstance(). Assim, se quisermos trocar o provider padrão, ou melhor, se o Android trocar o provider padrão de uma versão para outra, teremos problemas.

Voltamos à sinuca inicial. Mas vamos entender o que queremos que aconteça neste caso:

Queremos usar as APIs do pacote javax.crypto no Android de forma que seja possível testar na JVM com testes locais sem alterar nosso código.

Existe uma solução simples para o nosso exemplo: instalar a “policy” de força ilimitada. Isso porque estamos usando um tamanho de chave maior do que o permitido por padrão.

Para aumentar o limite da força de criptografia padrão, basta baixar o zip em https://goo.gl/dl1x7H (para Java 8), descompactar e mover tanto local_policy.jar e US_export_policy.jar para a pasta $JAVA_HOME/jre/lib/security (é aconselhável fazer um backup dos arquivos sobrescritos antes). E pronto!

Se executarmos novamente nosso exemplo, o teste irá passar. No entanto, o provider padrão da JVM da Oracle não se chama “BC”, e sim “SunJCE”. Assim, vamos alterar o nosso código para usar o provedor padrão:

public class CryptoTest {

    @Test
    public void canSetProvider() throws Exception {
        // passando o provider como segundo parâmetro
        final KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        Assert.assertEquals(keyGen.getProvider().getName(), "SunJCE");
        keyGen.init(256);
        final SecretKey secretKey = keyGen.generateKey();
        // Tudo ok com o tamanho!
        Assert.assertEquals(secretKey.getEncoded().length * 8, 256);
    }
}

Assim, temos um código que será executado com o BouncyCastle nos emuladores e aparelhos Android, mas que usará o provider padrão da JVM em testes locais. Isso é aceitável? Depende.

Tanto o provedor padrão quanto o Bouncy Castle são implementações respeitadas dos algoritmos de criptografia. Assim, se sua aplicação depende apenas de algoritmos que ambas as implementações contêm, não deveria haver problemas.

No entanto, como já não deve ser surpresa para o leitor, há um pequeno grande problema nessa abordagem: a interpretação dos padrões de criptografia. Na teoria, todo mundo entende os padrões definidos em PKCS (Password-based Cryptography Specification – Especificação de criptografia baseada em senha) da mesma forma, certo? Afinal, a especificação não deveria deixar margem para interpretação. Pois é… infelizmente, não foi bem assim.

Na especificação dos algoritmos da JCA (Java Cryptography Architecture), deu-se o nome “PKCS5Padding” para o padding, que na verdade é “PKCS7Padding” (apesar de as especificações terem definições diferentes de padding, pode-se dizer que o PKCS5Padding é um subconjunto de PKCS7Padding). Se fizerem uma busca rápida na Internet por “Java PCKS7 padding”, vão encontrar inúmeras questões parecidas que esbarram nesse problema.

Então, não tem jeito mesmo de conseguirmos resolver nossos problemas com os provedores de criptografia do Java e do Android? Deadlock? Rua sem saída? Ainda não.

Recapitulando o que vimos até aqui: o Android fornece um provedor padrão de algoritmos de criptografia que difere do padrão da JVM da Oracle. Queremos usar as APIs do pacote javax.crypto e conseguir testar nosso código tanto nos emuladores quanto em testes locais na JVM. Para tanto, instalamos os arquivos de extensão da força de criptografia e fizemos nosso código não depender de um provider específico. Nosso problema agora é que há nomes diferentes para especificações de padding no Android e na JVM.

Existe uma última abordagem mais radical para resolver de vez o problema: trocar o provedor de criptografia padrão de toda a JRE. Assim, quando não passarmos um provedor em métodos como KeyGenerator.getInstance(), iremos na verdade usar o Bouncy Castle, e não o Sun JCE.

Vale lembrar que essa abordagem pode nos complicar no caminho contrário: se algum código assumir que o provedor padrão é o SunJCE e não passar o provider nos métodos, vai ter problemas com diferentes interpretações das especificações.

Para alterar os provedores que a JRE usa por padrão, temos que alterar o arquivo $JAVA_HOME/jre/lib/security/java.security, encontrar a sessão que declara os provedores (todos começam com security.provider.N, onde N é a ordem de procura dos provedores) e adicionar uma linha com o provedor que queremos. No nosso caso, pode ser:

security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider

A ordem aqui é importante. Se o provedor security.provider.N=com.sun.crypto.provider.SunJCE
tiver precedência, ele será usado antes. Na documentação do Bouncy Castle, eles não recomendam colocar o Bouncy Castle primeiro, já que isso pode quebrar a própria JVM.

Se fizermos isso, nosso código não precisará passar qual o provedor queremos e irá utilizar o Bouncy Castle. Com isso, evitamos problemas como “PKCS7Padding”, porém precisamos saber que qualquer código que assuma o contrário (que “PKCS5Padding” é a mesma coisa que “PKCS7Padding”, por exemplo) pode ter problemas.

Existem outras implementações de algoritmos de criptografia que não aderem à arquitetura do JCA e que, assim, não enfrentam esses problemas de paridade de ambientes. Um bom exemplo disso é a biblioteca Conceal do Facebook. Vale lembrar que tomar a decisão de usar uma biblioteca não padrão da plataforma para criptografia é uma decisão que deve ser bem pensada e discutida com o time de segurança. Se houver qualquer dúvida, é melhor não utilizar.

Espero que com este artigo e suas referências, o leitor entenda melhor qual é a arquitetura do Java para provedores de criptografia e quais as diferenças para o Android.

Outras referências: https://www.crypto101.io/ – ótimo livro gratuito sobre criptografia em geral.