Checklist de armazenamento seguro em Kotlin para tokens, chaves e PII
Checklist de armazenamento seguro em Kotlin para escolher entre Android Keystore, EncryptedSharedPreferences e criptografia de banco para tokens, chaves e PII.

O que você está tentando proteger (em termos simples)
Armazenamento seguro em um app empresarial significa uma coisa: se alguém pegar o telefone (ou os arquivos do seu app), ainda assim não deve conseguir ler ou reutilizar o que você salvou. Isso inclui dados em repouso (no disco) e também segredos que vazam por backups, logs, relatórios de crash ou ferramentas de depuração.
Um teste mental simples: o que um estranho poderia fazer se abrisse a pasta de armazenamento do seu app? Em muitos apps, os itens mais valiosos não são fotos ou configurações. São pequenas strings que abrem acessos.
O armazenamento no dispositivo frequentemente inclui tokens de sessão (para manter o usuário logado), tokens de refresh, chaves de API, chaves de criptografia, dados pessoais (PII) como nomes e e‑mails, e registros do negócio em cache usados offline (pedidos, tickets, notas de cliente).
Aqui estão modos reais comuns de falha:
- Um dispositivo perdido ou roubado é examinado, e tokens são copiados para personificar um usuário.
- Malware ou um app “auxiliar” lê arquivos locais em um dispositivo com root ou via truques de acessibilidade.
- Backups automáticos transferem os dados do seu app para um lugar que você não planejou.
- Builds de debug registram tokens, os enviam em relatórios de crash ou desabilitam verificações de segurança.
Por isso “só guarde em SharedPreferences” não é aceitável para qualquer coisa que conceda acesso (tokens) ou que possa prejudicar usuários e sua empresa (PII). SharedPreferences em texto plano é como anotar segredos em um post‑it dentro do app: conveniente e fácil de ler se alguém tiver chance.
O ponto de partida mais útil é nomear cada item salvo e fazer duas perguntas: isso destrava algo? e seria um problema se ficasse público? O resto (Keystore, preferências criptografadas, banco criptografado) segue dessas respostas.
Classifique seus dados: tokens, chaves e PII
O armazenamento seguro fica mais fácil quando você para de tratar todos os “dados sensíveis” igualmente. Comece listando o que o app salva e o que aconteceria se vazasse.
Tokens não são a mesma coisa que senhas. Access tokens e refresh tokens devem ser armazenados para manter a sessão, mas continuam sendo segredos de alto valor. Senhas não devem ser armazenadas. Se precisar de login, armazene apenas o que é necessário para manter a sessão (normalmente tokens) e deixe o servidor verificar a senha.
Chaves são uma classe diferente. Chaves de API, chaves de assinatura e chaves de criptografia podem abrir sistemas inteiros, não apenas uma conta de usuário. Se alguém extrai‑las de um dispositivo, pode automatizar abusos em escala. Uma boa regra: se um valor pode ser usado fora do app para personificar o app ou descriptografar dados, trate‑o como maior risco que um token de usuário.
PII é qualquer coisa que possa identificar uma pessoa: e‑mail, telefone, endereço residencial, notas de cliente, documentos governamentais, dados de saúde. Campos que parecem inofensivos se tornam sensíveis quando combinados.
Um sistema rápido de rotulagem que funciona bem na prática:
- Segredos de sessão: access token, refresh token, cookie de sessão
- Segredos do app: chaves de API, chaves de assinatura, chaves de criptografia (evite colocá‑las em dispositivos quando possível)
- Dados do usuário (PII): detalhes de perfil, identificadores, documentos, informações médicas ou financeiras
- IDs de dispositivo e analytics: advertising ID, device ID, install ID (ainda sensíveis sob muitas políticas)
Android Keystore: quando usá‑lo
Android Keystore é melhor quando você precisa proteger segredos que nunca devem sair do dispositivo em forma legível. É um cofre para chaves criptográficas, não um banco para seus dados reais.
Para que serve bem: gerar e manter chaves usadas para criptografia, descriptografia, assinatura ou verificação. Normalmente você criptografa um token ou dados offline em outro lugar, e a chave do Keystore é o que o desbloqueia.
Chaves com suporte a hardware: o que isso significa de verdade
Em muitos dispositivos, as chaves do Keystore podem ser hardware‑backed. Isso significa que operações com a chave acontecem em um ambiente protegido e o material da chave não pode ser extraído. Reduz o risco de malware que lê arquivos do app.
Hardware‑backed não é garantido em todo dispositivo, e o comportamento varia por modelo e versão do Android. Construa assumindo que operações com a chave podem falhar.
Gatilhos de autenticação do usuário
Keystore pode exigir presença do usuário antes de uma chave ser usada. É assim que você amarra acesso a biometria ou credenciais do dispositivo. Por exemplo, você pode criptografar um token de exportação e só descriptografá‑lo após a confirmação por impressão digital ou PIN.
Keystore é uma boa escolha quando você quer uma chave não exportável, quando quer aprovação biométrica ou por credencial do dispositivo para ações sensíveis, e quando quer segredos por dispositivo que não devem sincronizar ou viajar com backups.
Planeje armadilhas: chaves podem ser invalidadas após mudanças na tela de bloqueio, alterações biométricas ou eventos de segurança. Espere falhas e implemente um fallback limpo: detectar chaves inválidas, apagar blobs criptografados e pedir que o usuário faça login novamente.
EncryptedSharedPreferences: quando é suficiente
EncryptedSharedPreferences é um bom padrão para um pequeno conjunto de segredos em formato chave‑valor. É “SharedPreferences, mas criptografado”, então alguém não pode simplesmente abrir um arquivo e ler os valores.
Por baixo, usa uma chave mestre para criptografar e descriptografar valores. Essa chave mestre é protegida pelo Android Keystore, então seu app não armazena a chave de criptografia em texto simples.
Normalmente é suficiente para alguns itens pequenos lidos com frequência, como access e refresh tokens, IDs de sessão, IDs de dispositivo, flags de ambiente ou pequenos estados como último horário de sincronização. Também serve para pequenos pedaços de dados do usuário, se realmente precisar armazená‑los, mas não deve virar um depósito para PII.
Não é adequado para nada grande ou estruturado. Se você precisa de listas offline, busca ou consultas por campos (clientes, tickets, pedidos), EncryptedSharedPreferences vira lento e desconfortável. Nesse ponto você quer um banco de dados criptografado.
Uma regra simples: se você consegue listar toda chave armazenada em uma tela, EncryptedSharedPreferences provavelmente é suficiente. Se você precisa de linhas e consultas, vá além.
Criptografia de banco: quando você precisa
Criptografia de banco faz sentido quando você armazena mais que uma configuração pequena ou um token. Se seu app guarda dados do negócio no dispositivo, assuma que podem ser extraídos de um telefone perdido, a menos que os proteja.
Um banco de dados faz sentido quando você precisa de acesso offline a registros, cache local por performance, histórico/trilhas de auditoria, ou notas e anexos longos.
Duas abordagens comuns de criptografia
Criptografia do banco inteiro (frequentemente estilo SQLCipher) criptografa o arquivo inteiro em repouso. Seu app o abre com uma chave. Isso é fácil de raciocinar porque você não precisa lembrar quais colunas estão protegidas.
Criptografia na camada do app por campo criptografa apenas certos campos antes de escrever, e depois descriptografa ao ler. Isso funciona se a maior parte dos registros não é sensível, ou se você quer manter uma estrutura de banco sem mudar o formato do arquivo.
Trocas: confidencialidade vs busca e ordenação
A criptografia do banco inteiro esconde tudo no disco, mas uma vez desbloqueado o app pode consultar normalmente.
A criptografia por campo protege colunas específicas, mas você perde busca e ordenação fáceis sobre valores criptografados. Ordenar por sobrenome criptografado não funciona de forma confiável, e buscar vira “buscar após descriptografar” (lento) ou “armazenar índices extras” (mais complexidade e possíveis vazamentos).
Noções básicas de gerenciamento de chaves
A chave do banco não deve ser hardcoded nem enviada com o app. Um padrão comum é gerar uma chave aleatória para o banco (DEK), então armazená‑la “wrapped” (criptografada) usando uma chave mantida no Android Keystore. No logout, você pode apagar a chave wrapped e tratar o banco local como descartável, ou mantê‑la se o app precisar funcionar offline entre sessões.
Como escolher: uma comparação prática
Você não está escolhendo “a opção mais segura” em geral. Está escolhendo a opção mais segura que se ajusta ao modo como seu app usa os dados.
Perguntas que realmente guiam a escolha certa:
- Com que frequência os dados são lidos (a cada abertura ou raramente)?
- Quanto dado é (alguns bytes ou milhares de registros)?
- O que acontece se vazar (inconveniente, custoso, passível de notificação legal)?
- Você precisa de acesso offline, busca ou ordenação?
- Há requisitos de conformidade (retenção, auditoria, regras de criptografia)?
Um mapeamento prático:
- Tokens (tokens OAuth de acesso e refresh) geralmente pertencem a
EncryptedSharedPreferencesporque são pequenos e lidos com frequência. - Material de chave deve viver preferencialmente no Android Keystore para reduzir a chance de cópia do dispositivo.
- PII e dados comerciais offline normalmente precisam de criptografia de banco quando você armazena mais que alguns campos ou precisa de listas e filtros offline.
Dados mistos são normais em apps empresariais. Um padrão prático é gerar uma chave de criptografia de dados (DEK) aleatória para seu banco local ou arquivo, armazenar apenas o DEK wrapped usando uma chave protegida pelo Keystore e rotacioná‑la quando necessário.
Se estiver em dúvida, escolha o caminho mais simples e seguro: armazene menos. Evite PII offline salvo quando realmente necessário e mantenha chaves no Keystore.
Passo a passo: implementar armazenamento seguro em um app Kotlin
Comece anotando cada valor que pretende guardar no dispositivo e a razão exata para que ele precise estar lá. Isso evita o armazenamento “só por precaução”.
Antes de escrever código, defina suas regras: por quanto tempo cada item deve viver, quando deve ser substituído e o que significa “logout”. Um access token pode expirar em 15 minutos, um refresh token pode durar mais, e PII offline pode ter uma regra de “apagar após 30 dias”.
Implementação que permanece sustentável:
- Crie um único wrapper “SecureStorage” para que o resto do app nunca toque diretamente SharedPreferences, Keystore ou o banco.
- Coloque cada item no lugar certo: tokens em
EncryptedSharedPreferences, chaves de criptografia protegidas pelo Android Keystore, e conjuntos offline maiores em um banco criptografado. - Trate falhas de propósito. Se o armazenamento seguro falhar, falhe fechado. Não caia silenciosamente para armazenamento em texto simples.
- Adicione diagnósticos sem vazar dados: registre tipos de evento e códigos de erro, nunca tokens, chaves ou detalhes do usuário.
- Encaminhe caminhos de exclusão: logout, remoção de conta e “limpar dados do app” devem convergir para a mesma rotina de wipe.
Depois, teste os casos chatos que quebram armazenamento seguro em produção: restauração de backup, atualização de versão do app, mudança de configurações de bloqueio do dispositivo, migração para um novo telefone. Garanta que usuários não fiquem presos em loop onde os dados não podem ser descriptografados mas o app fica tentando.
Por fim, documente as decisões em uma página que toda a equipe possa seguir: o que é armazenado, onde, prazos de retenção e o que acontece quando a descriptografia falha.
Erros comuns que quebram o armazenamento seguro
A maioria das falhas não é escolher a biblioteca errada. Acontece quando um atalho copia segredos para lugares que você não pretendia.
O maior sinal vermelho é um refresh token (ou token de longa duração) salvo em texto simples em qualquer lugar: SharedPreferences, um arquivo, um cache “temporário” ou uma coluna do banco local. Se alguém tiver um backup, um dump de dispositivo com root ou um artefato de build de debug, esse token pode sobreviver à senha.
Segredos também vazam pela visibilidade, não só pelo armazenamento. Logar cabeçalhos completos de requisição, imprimir tokens durante depuração ou anexar contexto “útil” a relatórios de crash e analytics pode expor credenciais fora do dispositivo. Trate logs como públicos.
O manuseio de chaves é outra lacuna comum. Usar uma chave para tudo aumenta o raio de dano. Não rotacionar chaves significa que compromissos antigos permanecem válidos. Inclua um plano de versionamento de chaves, rotação e o que acontece com dados criptografados antigos.
Não esqueça os caminhos “fora do cofre”
Criptografia não impede backups na nuvem de copiarem dados locais. Não impede screenshots ou gravações de tela capturarem PII. Não resolve builds de debug com configurações relaxadas, nem recursos de exportação (CSV/compartilhar) que vazem campos sensíveis. O uso da área de transferência também pode vazar códigos únicos ou números de conta.
Além disso, criptografia não corrige autorização. Se seu app mostra PII após logout, ou mantém cache acessível sem reauth, isso é um bug de controle de acesso. Tranque a interface, limpe caches sensíveis no logout e verifique permissões antes de exibir dados protegidos.
Detalhes operacionais: ciclo de vida, logout e casos de borda
Armazenamento seguro não é só onde você guarda segredos. É como eles se comportam ao longo do tempo: quando o app dorme, quando um usuário faz logout e quando o dispositivo é bloqueado.
Para tokens, planeje o ciclo de vida completo. Access tokens devem ser de curta duração. Refresh tokens devem ser tratados como senhas. Se um token expirar, renove‑o silenciosamente. Se refresh falhar (revogado, senha alterada, dispositivo removido), pare tentativas em loop e force um novo login limpo. Também dê suporte a revogação no lado servidor. Armazenamento local perfeito não ajuda se você nunca invalidar credenciais roubadas.
Use biometria para re‑autenticação, não para tudo. Solicite quando a ação tiver risco real (ver PII, exportar dados, mudar dados de pagamento, mostrar uma chave de uso único). Não peça biometria a cada abertura do app.
No logout, seja estrito e previsível:
- Limpe cópias em memória primeiro (tokens cacheados em singletons, interceptors ou ViewModels).
- Apague tokens armazenados e estado de sessão (incluindo refresh tokens).
- Remova ou invalide chaves de criptografia locais se seu design permitir.
- Delete PII offline e respostas de API em cache.
- Desative jobs em background que possam refazer fetch de dados.
Casos de borda importam em apps empresariais: múltiplas contas no mesmo dispositivo, perfis de trabalho, backup/restore, transferência entre dispositivos e logouts parciais (trocar workspace em vez de sair completamente). Teste force stop, upgrades do OS e mudanças de relógio, já que derivações de tempo podem quebrar lógica de expiração.
Detecção de adulteração é um tradeoff. Checks básicos (builds debuggable, flags de emulador, sinais simples de root, verdicts do Play Integrity) reduzem abuso casual, mas atacantes determinados podem contorná‑los. Trate sinais de tamper como entradas de risco: limite acesso offline, exija reauth e registre o evento.
Checklist rápido antes de enviar
Use isto antes do release. Ele mira nos pontos onde armazenamento seguro falha em apps empresariais reais.
- Assuma que o dispositivo pode ser hostil. Se um atacante tem um dispositivo com root ou uma imagem completa do aparelho, ele consegue ler tokens, chaves ou PII de arquivos do app, preferências, logs ou screenshots? Se a resposta for “talvez”, mova segredos para proteção com Keystore e mantenha o payload criptografado.
- Cheque backups e transferências de dispositivo. Mantenha arquivos sensíveis fora do Android Auto Backup, backups na nuvem e transferências device‑to‑device. Se perder uma chave na restauração quebrará a descriptografia, planeje um fluxo de recuperação (re‑auth e re‑download em vez de tentar descriptografar).
- Procure plaintext acidental no disco. Busque arquivos temporários, caches HTTP, relatórios de crash, eventos de analytics e caches de imagem que possam conter PII ou tokens. Verifique logs de debug e dumps JSON.
- Expire e rotacione. Tokens de acesso devem ser curtos, refresh tokens protegidos e sessões servidor‑lado revogáveis. Defina rotação de chaves e o que o app faz quando um token é rejeitado (apagar, reauth, tentar uma vez).
- Comportamento em reinstalação e troca de dispositivo. Teste desinstalar e reinstalar, abrir offline. Se chaves do Keystore sumirem, o app deve falhar de forma segura (apagar dados criptografados, mostrar login, evitar leituras parciais que corrompam o estado).
Uma validação rápida é o teste do “pior dia”: um usuário faz logout, muda a senha, restaura um backup em um novo telefone e abre o app offline. O resultado deve ser previsível: ou os dados descriptografam para o usuário certo, ou são apagados e re‑baixados após o login.
Cenário de exemplo: um app empresarial que guarda PII offline
Imagine um app de campo usado onde o sinal é ruim. Representantes fazem login uma vez pela manhã, consultam clientes atribuídos offline, adicionam notas de reunião e sincronizam depois. É aí que uma checklist de armazenamento deixa de ser teoria e começa a prevenir vazamentos reais.
Uma divisão prática:
- Access token: mantenha de curta duração e em
EncryptedSharedPreferences. - Refresh token: proteja mais e controle acesso via Android Keystore.
- PII do cliente (nomes, telefones, endereços): armazene em um banco local criptografado.
- Notas offline e anexos: guarde no banco criptografado, com cuidado extra para exportações e compartilhamento.
Agora adicione duas features e o risco muda.
Se você adicionar “lembrar de mim”, o refresh token vira a porta principal de volta à conta. Trate‑o como uma senha. Dependendo dos usuários, você pode exigir desbloqueio do dispositivo (PIN/padrão/biometria) antes de descriptografá‑lo.
Se adicionar modo offline, você não protege mais só uma sessão. Protege uma lista inteira de clientes que pode ser valiosa por conta própria. Isso normalmente te empurra para criptografia de banco e regras claras de logout: limpar PII local, manter só o necessário para o próximo login e cancelar sincronização em background.
Teste em dispositivos reais, não só em emuladores. No mínimo, verifique comportamento de bloqueio/desbloqueio, reinstalação, backup/restore e separação entre múltiplos usuários ou perfis de trabalho.
Próximos passos: transforme isso em hábito de equipe
Armazenamento seguro só funciona quando vira hábito. Escreva uma política curta de armazenamento que sua equipe siga: o que vai onde (Keystore, EncryptedSharedPreferences, banco criptografado), o que nunca é armazenado e o que deve ser apagado no logout.
Torne isso parte da entrega diária: definition of done, revisão de código e checagens de release.
Uma checklist leve para reviewers:
- Cada item armazenado está rotulado (token, material de chave ou PII).
- A escolha de armazenamento está justificada em comentários do código.
- Logout e troca de conta removem os dados corretos (e somente esses dados).
- Erros e logs nunca imprimem segredos ou PII completo.
- Alguém é responsável pela política e a mantém atualizada.
Se sua equipe usa AppMaster (appmaster.io) para construir apps empresariais e exporta código Kotlin para o cliente Android, mantenha a mesma abordagem do wrapper SecureStorage para que código gerado e customizado siga uma política consistente.
Comece com um pequeno proof‑of‑concept
Construa um POC pequeno que armazene um token de autenticação e um registro de PII (por exemplo, o telefone de um cliente necessário offline). Depois teste instalação limpa, upgrade, logout, mudanças na tela de bloqueio e limpar dados do app. Expanda só depois que o comportamento de wipe estiver correto e repetível.
FAQ
Comece listando exatamente o que você armazena e por quê. Coloque segredos de sessão pequenos, como tokens de acesso e refresh, em EncryptedSharedPreferences, mantenha chaves criptográficas no Android Keystore e use um banco de dados criptografado para registros comerciais offline e dados pessoais (PII) quando houver mais de alguns campos ou necessidade de consultas.
SharedPreferences armazena valores em um arquivo que pode, em muitos casos, ser lido a partir de backups do dispositivo, acesso em dispositivos com root ou artefatos de depuração. Se o valor for um token ou qualquer PII, tratá‑lo como uma configuração normal facilita que alguém copie e reutilize fora do app.
Use Android Keystore para gerar e manter chaves criptográficas que não devem ser extraídas. Normalmente você usa essas chaves para criptografar outros dados (tokens, chaves de banco de dados, arquivos) e pode exigir autenticação do usuário (biometria ou credenciais do dispositivo) antes de usar a chave.
Significa que operações com a chave podem ocorrer em hardware protegido, tornando o material da chave mais difícil de extrair, mesmo que um atacante consiga ler arquivos do app. Não presuma que esteja sempre disponível ou que se comporte igual em todos os dispositivos; projete para falhas e tenha um fluxo de recuperação quando a chave não estiver disponível ou for invalidada.
Normalmente sim, para um pequeno conjunto de segredos em formato chave-valor lidos com frequência, como tokens de acesso/refresh, IDs de sessão e pequenos estados. Não é adequado para dados grandes, registros estruturados offline ou qualquer coisa que você precise consultar e filtrar, como listas de clientes ou pedidos.
Escolha um banco de dados criptografado quando você armazenar dados comerciais offline ou PII em escala, precisar de consultas/buscas/ordenações ou manter histórico para uso offline. Ele reduz o risco de um dispositivo perdido expor listas inteiras de clientes ou notas, enquanto permite que o app funcione offline com uma estratégia de chaves clara.
A criptografia de banco inteira protege o arquivo todo em repouso e é mais simples de raciocinar porque você não precisa rastrear quais colunas são sensíveis. A criptografia por campo pode funcionar para algumas colunas, mas dificulta busca e ordenação, e é fácil vazar dados por índices ou campos derivados.
Gere uma chave de banco de dados aleatória e armazene‑a somente em forma “wrapped” (criptografada) usando uma chave protegida pelo Keystore. Nunca deixe chaves hardcoded no app ou embarque‑as no binário, e decida o que fazer no logout ou quando uma chave for invalidada (frequentemente: apagar a chave wrapped e tratar os dados locais como descartáveis).
Keys podem ser invalidadas por mudanças na tela de bloqueio, alterações biométricas, eventos de segurança do SO ou cenários de restauração/migração. Trate isso explicitamente: detecte falhas de descriptografia, limpe os blobs ou o banco local de forma segura e solicite que o usuário entre novamente em vez de tentar repetidamente ou cair para armazenamento em texto simples.
A maior parte dos vazamentos acontece “fora do cofre”: logs, relatórios de crash, eventos de analytics, prints de depuração, caches HTTP, capturas de tela, uso da área de transferência e caminhos de backup/restore. Trate logs como públicos, nunca registre tokens ou PII completo, desative caminhos de exportação acidental e faça o logout apagar tanto os dados em disco quanto as cópias em memória.


