MASTG-KNOW-0051: Memória de Processo

Todos os aplicativos no Android utilizam memória para realizar operações computacionais normais, como qualquer computador moderno comum. Não é surpresa, portanto, que em determinados momentos operações sensíveis sejam realizadas dentro da memória do processo. Por essa razão, é importante que, uma vez que os dados sensíveis relevantes tenham sido processados, eles sejam descartados da memória do processo o mais rápido possível.

A investigação da memória de um aplicativo pode ser feita por meio de despejos de memória (memory dumps) e pela análise da memória em tempo real via um debugger.

Para obter uma visão geral das possíveis fontes de exposição de dados, verifique a documentação e identifique os componentes do aplicativo antes de examinar o código-fonte. Por exemplo, dados sensíveis provenientes de um backend podem estar no cliente HTTP, no analisador XML (XML parser), etc. Você deseja que todas essas cópias sejam removidas da memória o mais breve possível.

Além disso, compreender a arquitetura do aplicativo e o papel dessa arquitetura no sistema ajudará você a identificar informações sensíveis que nem precisam ser expostas na memória. Por exemplo, suponha que seu aplicativo receba dados de um servidor e os transfira para outro sem qualquer processamento. Esses dados podem ser manipulados em formato criptografado, o que evita sua exposição na memória.

No entanto, se for necessário expor dados sensíveis na memória, você deve garantir que seu aplicativo seja projetado para expor o menor número possível de cópias de dados e pelo menor tempo possível. Em outras palavras, é desejável que o manuseio de dados sensíveis seja centralizado (ou seja, realizado pelo menor número possível de componentes) e baseado em estruturas de dados primitivas e mutáveis.

O último requisito oferece aos desenvolvedores acesso direto à memória. Certifique-se de que eles usem esse acesso para substituir os dados sensíveis por dados fictícios (normalmente zeros). Exemplos de tipos de dados preferíveis incluem byte[] e char[], mas não String ou BigInteger. Sempre que você tenta modificar um objeto imutável como String, cria e altera uma cópia do objeto.

O uso de tipos mutáveis não primitivos, como StringBuffer e StringBuilder, pode ser aceitável, mas é indicativo e requer cuidado. Tipos como StringBuffer são usados para modificar conteúdo (o que é o que você deseja fazer). No entanto, para acessar o valor de tal tipo, você usaria o método toString, que criaria uma cópia imutável dos dados. Existem várias maneiras de usar esses tipos de dados sem criar uma cópia imutável, mas elas exigem mais esforço do que usar um array primitivo. O gerenciamento seguro de memória é um benefício do uso de tipos como StringBuffer, mas isso pode ser uma faca de dois gumes. Se você tentar modificar o conteúdo de um desses tipos e a cópia exceder a capacidade do buffer, o tamanho do buffer aumentará automaticamente. O conteúdo do buffer pode ser copiado para um local diferente, deixando o conteúdo antigo sem uma referência útil para sobrescrevê-lo.

Infelizmente, poucas bibliotecas e frameworks são projetados para permitir que dados sensíveis sejam sobrescritos. Por exemplo, destruir uma chave, como mostrado abaixo, não remove a chave da memória:

val secretKey: SecretKey = SecretKeySpec("key".toByteArray(), "AES")
secretKey.destroy()

Sobrescrever o array de bytes de apoio (backing byte-array) de secretKey.getEncoded também não remove a chave; a chave baseada em SecretKeySpec retorna uma cópia do array de bytes de apoio. Consulte as seções abaixo para a maneira correta de remover uma SecretKey da memória.

O par de chaves RSA é baseado no tipo BigInteger e, portanto, permanece na memória após seu primeiro uso fora do AndroidKeyStore. Alguns cifras (como o Cipher AES no BouncyCastle) não limpam adequadamente seus arrays de bytes.

Dados fornecidos pelo usuário (credenciais, números de previdência social, informações de cartão de crédito, etc.) são outro tipo de dados que podem ser expostos na memória. Independentemente de você marcá-lo como um campo de senha, o EditText entrega o conteúdo para o aplicativo por meio da interface Editable. Se seu aplicativo não fornecer um Editable.Factory, os dados fornecidos pelo usuário provavelmente ficarão expostos na memória por mais tempo do que o necessário. A implementação padrão do Editable, o SpannableStringBuilder, causa os mesmos problemas que o StringBuilder e o StringBuffer do Java causam (discutidos acima).