Um acompanhamento sobre como armazenar tokens com segurança no Android

Como prólogo a este artigo, quero assinalar uma frase curta para o leitor nocional. Esta citação será importante à medida que avançamos.

Segurança absoluta não existe. A segurança é um conjunto de medidas, sendo empilhadas e combinadas, tentando desacelerar o inevitável.

Quase três anos atrás, escrevi um post dando algumas ideias para proteger tokens de String de um atacante hipotético descompilar nosso aplicativo Android. Por causa da lembrança, e a fim de afastar a morte inescapável da Internet, estou reproduzindo algumas seções aqui.

Um dos casos de uso mais comuns ocorre quando nosso aplicativo precisa se comunicar com um serviço da Web para trocar dados. Essa troca de dados pode variar de uma natureza menos a mais sensível e variar entre uma solicitação de login, petição de alteração de dados do usuário, etc.

A primeira medida absoluta a ser aplicada é usar uma conexão SSL (Secure Sockets Layer) entre o cliente e o servidor. Vá novamente para a cotação inicial. Isso não garante uma privacidade e segurança absolutas, embora faça um bom trabalho inicial.

Quando você está usando uma conexão SSL (como quando vê o bloqueador em seu navegador), isso indica que a conexão entre você e o servidor está criptografada. Em um nível teórico, nada pode acessar a informação contida neste pedido (*)

(*) Eu mencionei que a segurança absoluta não existe? Conexões SSL ainda podem ser comprometidas. Este artigo não pretende fornecer uma lista extensa de todos os possíveis ataques, mas quero que você saiba algumas possibilidades. Certificados SSL falsos podem ser usados, assim como ataques Man-in-the-Middle.

Vamos seguir em frente. Estamos assumindo que nosso cliente está se comunicando por meio de um canal SSL criptografado com nosso back-end. Eles estão trocando dados úteis, fazendo seus negócios, sendo felizes. Mas queremos fornecer uma camada de segurança adicional.

Um próximo passo lógico usado hoje em dia é fornecer um token de autenticação ou uma chave de API a ser usada na comunicação. Funciona assim. Nosso backend recebe uma petição. Como sabemos que a petição vem de um de nossos clientes verificados e não de um cara aleatório tentando obter acesso à nossa API? O back-end verificará se o cliente está fornecendo uma chave de API válida. Se a afirmação anterior for verdadeira, prosseguiremos com a solicitação. Caso contrário, nós o negamos e, dependendo da natureza de nossos negócios, tomamos algumas medidas corretivas (quando isso acontece, eu particularmente gosto de armazenar o IP e os IDs do cliente para ver com que frequência isso ocorre. Quando a frequência está aumentando mais que é desejável para o meu bom gosto, eu considero uma proibição ou observando atentamente o que o indelicado cara da internet está tentando alcançar).

Vamos construir nosso castelo do chão. Em nosso aplicativo, provavelmente adicionaremos uma variável chamada API_KEY que será automaticamente injetada em cada solicitação (se você estiver usando o Android, provavelmente em seu cliente de Retrofit).

string static static final API_KEY = “67a5af7f89ah3katf7m20fdj202”

Isso é ótimo e funciona se quisermos autenticar nosso cliente. O problema é que não fornece uma camada muito eficaz por si só.

Se você usar o apktool para descompilar o aplicativo e realizar uma pesquisa procurando por strings, você encontrará em um dos arquivos .smali resultantes o seguinte:

string const v1, “67a5af7f89ah3katf7m20fdj202”

Sim claro. Ele não diz que isso é um Token de validação, então ainda precisamos passar por uma verificação meticulosa para decidir como alcançar essa string e se ela pode ser usada para fins de autenticação ou não. Mas você sabe para onde estou indo: isso é principalmente uma questão de tempo e recursos.

Poderia Proguard nos ajudar a proteger este String, então não temos que nos preocupar com isso? Na verdade não. O Proguard declara em seu FAQ que a criptografia String não é totalmente possível.

Que tal salvar essa String em um dos outros mecanismos fornecidos pelo Android, como o SharedPreferences? Isso é apenas uma boa ideia. SharedPreferences pode ser acessado facilmente a partir do emulador ou de qualquer dispositivo com raiz. Alguns anos atrás, um cara chamado Srinivas comprovou como o placar poderia ser alterado em um videogame. Estamos ficando sem opções aqui!

Kit de Desenvolvimento Nativo (NDK)

Vou atualizar aqui o modelo inicial que propus e como podemos iterá-lo para fornecer uma alternativa mais segura. Vamos criar duas funções que podem servir para criptografar e descriptografar nossos dados:

Nada extravagante aqui. Essas duas funções terão um valor de chave e uma string para serem codificadas ou decodificadas. Eles retornarão o token criptografado ou descriptografado, respectivamente. Chamaríamos a seguinte função da seguinte maneira:

Você está adivinhando a direção? Isso está certo. Poderíamos criptografar e descriptografar nosso token sob demanda. Isso fornece uma camada adicional de segurança: quando o código fica ofuscado, não é mais tão simples quanto executar uma pesquisa de String e verificar o ambiente em torno dessa String. Mas você ainda pode descobrir um problema que precisa ser resolvido?

Você pode?

Dê mais alguns segundos se você ainda não descobriu.

Sim você está certo. Temos uma chave de criptografia que também está sendo armazenada como String. Isso está adicionando mais camadas de segurança por obscuridade, mas ainda temos um token em texto sem formatação, independentemente de esse token ser usado para criptografia ou é o token per se.

Vamos usar agora o NDK e continuar interagindo com nosso mecanismo de segurança.

O NDK nos permite acessar uma base de código C ++ do nosso código Android. Como primeira abordagem, vamos tirar um minuto para pensar no que fazer. Poderíamos ter uma função C ++ nativa que armazena uma chave de API ou quaisquer dados sensíveis que estamos tentando armazenar. Essa função pode mais tarde ser chamada a partir do código e nenhuma string será armazenada em qualquer arquivo Java. Isso forneceria uma proteção automática contra técnicas de descompilação.

Nossa função C ++ ficaria da seguinte forma:

Ele será chamado facilmente em seu código Java:

E a função de criptografia / descriptografia será chamada como no próximo trecho:

Se soubermos gerar um APK, ofuscá-lo, descompilá-lo e tentar acessar a string contida na função nativa getSecretKey (), não poderemos encontrá-lo! Vitória?

Na verdade não. O código NDK pode realmente ser desmontado e inspecionado. Isso está ficando mais difícil e você está começando a exigir ferramentas e técnicas mais avançadas. Você se livrou de 95% das crianças do roteiro, mas uma equipe com recursos e motivação suficientes ainda poderá acessar o token. Lembre-se desta frase?

Segurança absoluta não existe. A segurança é um conjunto de medidas, sendo empilhadas e combinadas, tentando desacelerar o inevitável.
Você ainda pode acessar no código desmontado String literais!

O Hex Rays, por exemplo, faz um trabalho muito bom em descompilar arquivos nativos. Tenho certeza de que há também um monte de ferramentas que poderiam desconstruir qualquer código nativo gerado com o Android (não estou associado a Hex Rays nem recebo nenhum tipo de compensação monetária deles).

Então, qual solução poderíamos usar para nos comunicarmos entre um backend e um cliente sem sermos sinalizados?

Gere a chave em tempo real no dispositivo.

Seu dispositivo não precisa armazenar nenhum tipo de chave e lidar com todo o incômodo de proteger um literal de string! Essa é uma técnica muito antiga usada por serviços, como validação de chave remota.

  1. O cliente conhece uma função () que retorna uma chave.
  2. O backend conhece a função () implementada no cliente
  3. O cliente gera uma chave através da função (), e isso é entregue ao servidor.
  4. O servidor valida e prossegue com a solicitação.

Você está conectando os pontos? Em vez de ter uma função nativa que retorna uma string (facilmente identificável), por que não ter uma função que retorna a soma de três números primos aleatórios entre 1 e 100? Ou uma função que tira o dia atual expressa em unixtime e adiciona um 1 a cada dígito diferente? Que tal tirar algumas informações contextuais do dispositivo, como a quantidade de memória usada, para fornecer um grau mais alto de entropia?

O último parágrafo inclui uma série de idéias, mas esperamos que nosso leitor hipotético tenha tomado o ponto principal.

Resumo

  1. Segurança absoluta não existe.
  2. Combinar um conjunto de medidas de proteção é a chave para alcançar um alto grau de segurança.
  3. Não armazene literais de string em seu código.
  4. Use o NDK para criar uma chave gerada automaticamente.

Lembre-se da primeira frase?

Segurança absoluta não existe. A segurança é um conjunto de medidas, sendo empilhadas e combinadas, tentando desacelerar o inevitável.

Quero salientar mais uma vez que seu objetivo é proteger o máximo possível seu código, sem perder a perspectiva de que 100% da segurança é inatingível. Mas se você for capaz de proteger seu código de uma maneira que exija uma grande quantidade de recursos para descriptografar qualquer informação sensata que tenha, você poderá dormir bem e em silêncio.

Um pequeno aviso

Eu sei. Você chegou até aqui, pensando em todo o artigo “como esse cara não está mencionando o Dexguard, e passando por todos os problemas?”. Você está certo. O Dexguard pode realmente ofuscar Strings, e eles fazem um trabalho muito bom nisso. No entanto, o preço do Dexguard pode ser proibitivo. Eu usei o Dexguard em empresas anteriores com sistemas de segurança críticos, mas isso pode não ser uma opção para todos. E, tanto no desenvolvimento de software quanto na vida, quanto mais opções você tem, mais rico e mais abundante o mundo fica.

Codificação feliz!

Eu escrevo meus pensamentos sobre engenharia de software e vida em geral na minha conta do Twitter. Se você gostou deste artigo ou se o ajudou, sinta-se à vontade para compartilhá-lo, ♥ e / ou deixar um comentário. Esta é a moeda que alimenta os escritores amadores.