03.09.2015

Protegendo dados de usuários em apps Android

Imagem via imgur.com

Já pensou que louco seria se existissem usuários que realmente sabem o que estão fazendo? Pois é, embora raros, eles existem. Nem todos os usuários de smartphones são que nem o cara na imagem do lado, e alguns conseguem encontrar e abusar da menor das falhas de segurança de um sistema. Por isso devemos desenvolver aplicativos mobile com segurança sempre em mente.

Segurança no Android sempre foi e sempre será uma questão complicada, já que a plataforma é open source e qualquer um pode revirar o seu código fonte procurando falhas de segurança. Por isso, ao desenvolver um aplicativo para o sistema operacional Android, deve-se ter cuidados em cada ponto e vírgula do código. O primeiro cuidado a se tomar é, obviamente, com os inputs que o usuário envia para o aplicativo através da interface. SQL injection é um problema gravíssimo, e pode ser fácil esquecer de tratar um input. Porém, as ameaças vêm de fora também, e usuários podem tentar acessar seus dados de for a do aplicativo.

A preocupação principal que se deve ter em relação a segurança é, obviamente, como armazenar de forma segura os dados do usuário. As formas mais comuns (documentadas pelo guia do desenvolvedor do Android) de se armazenar dados são:

  • Shared Preferences: Salvar os dados como pares chave/valor como um arquivo XML em uma área protegida da memória interna do aparelho. É uma forma relativamente segura de guardar dados de usuário, já que o acesso é restrito apenas à aplicação que cria os dados. Porém o XML não é encriptado, e qualquer pessoa que “rootear” o celular terá livre acesso a todos os dados salvos dessa forma.
  • Internal/External Storage: Salvar os dados como um arquivo na memória interna ou externa do aparelho. É a forma menos segura de se guardar dados sigilosos, já que os arquivos podem ser acessados de forma trivial. Porém é bastante útil para guardar informações menos importantes, como arquivos de mídia, fotos, etc.
  • SQLite Database: Salvar os dados em um banco de dados SQLite. O banco é único para cada aplicação e também tem acesso restrito, assim como SharedPreferences. Embora pareça a melhor solução entre as 3 já mencionadas, esta sofre do mesmo problema de “rooteamento”.
  • Network Connection: • Potencialmente a forma mais segura de armazenamento. A ideia aqui é bem simples: não armazenar nada no aparelho e pegar e salvar todos os dados “on the fly” pela rede. Se feito corretamente pode realmente ser bem seguro, porém pode ser vulnerável a redes wifi inseguras, além de ser bem ineficiente nos quesitos bateria, performance e uso da rede de dados, fazendo deste o método menos prático entre os mencionados.

Com exceção da última, todas essas formas de armazenamento falham em um ponto principal: é sempre possível acessar fisicamente o arquivo aonde os dados são salvos. Devemos então aceitar isso como fato e garantir que mesmo que alguém mal intencionado tenha acesso aos arquivos que guardam os dados do usuário, não será possível ter acesso aos dados em si. Mas como fazemos isso?

Criptografia com SQLCipher

O ponto chave de uma aplicação que protege bem os dados dos seus usuários é conseguir garantir que mesmo que alguém tenha acesso aos dados, eles serão incompreensíveis sem antes descriptografá-los.

Uma boa solução é criptografar o banco de dados, usando uma extensão para o SQLite chamada SQLCipher:

Logo do SQL Cipher

Essa extensão consiste em um .jar que é adicionado ao projeto do aplicativo, e oferece encriptação de 256 bits para a base, de uma forma transparente para o desenvolvedor. Veja a diferença entre um BD simples com e sem criptografia:

Criando o banco sem criptografia:

rodcastro:~/workspace$ sqlite sem_sqlcipher.db
SQLite version 2.8.17
Enter ".help" for instructions
sqlite> create table pessoas (nome, telefone);
sqlite> insert into pessoas values ("Rodrigo", "2555-1234");
sqlite> .exit

Criando o banco com criptografia:

rodcastro:~/workspace$ sqlcipher com_sqlcipher.db
SQLCipher version 2.2.1 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> pragma key="chave_secreta";
sqlite> create table pessoas (nome, telefone);
sqlite> insert into pessoas values ("Rodrigo", "2555-1234");
sqlite> .exit
Agora vamos analisar o conteúdo dos arquivos para ver a diferença:
rodcastro:~/workspace$ hexdump -C sem_sqlcipher.db
00000000  2a 2a 20 54 68 69 73 20  66 69 6c 65 20 63 6f 6e  |** This file con|
00000010  74 61 69 6e 73 20 61 6e  20 53 51 4c 69 74 65 20  |tains an SQLite |
00000020  32 2e 31 20 64 61 74 61  62 61 73 65 20 2a 2a 00  |2.1 database **.|
00000030  28 75 e3 da 00 00 00 00  00 00 00 00 e5 00 00 00  |(u..............|
00000040  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000400  00 00 00 00 08 00 5c 00  00 00 00 00 04 00 00 00  |......\.........|
00000410  00 00 44 00 80 00 00 01  06 0c 14 1c 1e 44 74 61  |..D..........Dta|
00000420  62 6c 65 00 70 65 73 73  6f 61 73 00 70 65 73 73  |ble.pessoas.pess|
00000430  6f 61 73 00 33 00 63 72  65 61 74 65 20 74 61 62  |oas.3.create tab|
00000440  6c 65 20 70 65 73 73 6f  61 73 20 28 6e 6f 6d 65  |le pessoas (nome|
00000450  2c 20 74 65 6c 65 66 6f  6e 65 29 00 a4 03 00 00  |, telefone).....|
00000460  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000800  00 00 00 00 08 00 30 00  00 00 00 00 04 00 00 00  |......0.........|
00000810  00 00 15 00 80 00 00 01  03 0b 15 52 6f 64 72 69  |...........Rodri|
00000820  67 6f 00 32 35 35 35 2d  31 32 33 34 00 00 00 00  |go.2555-1234....|
00000830  d0 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000840  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000c00

Repare que além de ser possível literalmente ler tudo o que está guardado com um simples hexdump, também é possível abrir este banco com o sqlite, sem nenhuma restrição. Veja agora a diferença com o arquivo criptografado:

rodcastro:~/workspace$ hexdump -C com_sqlcipher.db
00000000  3a b2 cc 9b 94 f2 2f 26  56 ec c6 be d5 5f bd 2e  |:...../&V...._..|
00000010  ae 8d b0 e6 cc 50 e3 e0  c4 5f 1a b3 73 c1 3a 82  |.....P..._..s.:.|
00000020  45 33 bd 19 26 3e 54 d1  47 cb b0 ab 18 11 f5 01  |E3..&>T.G.......|
00000030  d0 d5 e6 65 92 cd b0 37  cc 80 5a e9 7f 52 56 49  |...e...7..Z..RVI|
00000040  cb 65 a2 bb 65 06 7a c0  92 54 05 37 24 4b 22 f6  |.e..e.z..T.7$K".|
00000050  cf 79 ea f0 bf b7 99 9b  94 d4 78 5a 79 a6 95 68  |.y........xZy..h|
00000060  3c ad 8c 3d 5a 27 ee c9  8b 08 68 c4 fc 71 c8 37  |<..=Z'....h..q.7|
00000070  51 2d 67 33 da 3e be 3f  c9 34 56 d6 a3 9f e5 c0  |Q-g3.>.?.4V.....|
00000080  2b b2 96 00 71 03 94 a1  5a 80 25 0c 62 ea c4 c4  |+...q...Z.%.b...|
00000090  af 70 b5 e7 d5 1a cc 58  3f 7b 2c 9a 49 bb bb 07  |.p.....X?{,.I...|
000000a0  77 56 2c c4 ac c9 b0 7a  83 8f 99 c0 a9 5a 0f 17  |wV,....z.....Z..|
000000b0  5a be 2a 27 84 02 b7 3e  3c 18 63 38 5c bb 81 e0  |Z.*'...><.c8\...|
000000c0  72 9a aa 70 90 02 37 b3  af 1b 2f 96 5f d5 97 3f  |r..p..7.../._..?|
000000d0  07 6b 82 93 70 4d 78 f3  7f 17 72 9c ec 97 fb 67  |.k..pMx...r....g|
000000e0  26 3b bf a8 43 ae 0e a6  7f 49 a0 70 46 86 41 1a  |&;..C....I.pF.A.|
000000f0  9e 68 ca ff 32 65 05 b6  31 e0 40 d8 29 f2 50 8e  |.h..2e..1.@.).P.|
00000100  ba 9a c0 44 6e d7 54 eb  b3 3d 28 8e 53 dc ab f1  |...Dn.T..=(.S...|
00000110  9f 79 4c 84 1b bb 5c 4f  6f a0 a3 aa 01 17 31 6e  |.yL...\Oo.....1n|
00000120  77 52 7c c4 2f b7 39 51  0e eb 44 f2 f2 c2 aa 17  |wR|./.9Q..D.....|
00000130  0c 65 63 86 20 6d 81 08  82 05 18 49 d6 e2 d7 c9  |.ec. m.....I....|
00000140  ff 50 ab 05 64 5b de 5d  6a 98 f8 49 f6 7b 18 f0  |.P..d[.]j..I.{..|
00000150  87 fa c1 90 01 c9 1c 92  14 1b 5d a9 1e 76 9c 03  |..........]..v..|
00000160  5c 0b d8 3b 65 b1 0c 6d  09 28 ab 79 93 76 0c e0  |\..;e..m.(.y.v..|
00000170  c2 65 dd 59 85 8c 56 dc  47 25 d3 54 e7 ad 13 7e  |.e.Y..V.G%.T...~|
00000180  48 6c 37 63 b5 09 d4 6f  21 e2 f6 c7 3b e3 cd c6  |Hl7c...o!...;...|
(...)

O arquivo criptografado fica maior, mas é impossível entender alguma coisa, já que está tudo protegido. Além disso, não é possível abrir mais o banco sem a chave correta:

rodcastro:~/workspace$ sqlite com_sqlcipher.db
Unable to open database "com_sqlcipher.db": file is encrypted or is not a database

Mesmo abrindo pelo SQLCipher, ainda não é possível acessar os dados sem a chave:

rodcastro:~/workspace$ sqlcipher com_sqlcipher.db 
SQLCipher version 2.2.1 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from pessoas;
Error: file is encrypted or is not a database
sqlite>

Para conseguir acessar corretamente esta base, a chave deve ser especificada assim que o arquivo é aberto. Isso dirá ao SQLCipher que essa é a chave que será usada. Caso a chave esteja incorreta ou tenha sido feito qualquer operação no banco (select, update, create table, etc) antes de informar a chave, não será possível descriptografar o banco.

rodcastro:~/workspace$ sqlcipher com_sqlcipher.db
SQLCipher version 2.2.1 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> pragma key="chave_secreta";
sqlite> select * from pessoas;
Rodrigo|2555-1234
sqlite>

Agora que vimos como o SQLCipher funciona, vamos ver como podemos utilizá-lo em um projeto Android. Para isso, basta adicionar a biblioteca Java do SQLCipher para Android, junto com alguns arquivos .so que são fornecidos junto com a biblioteca, e trocar os imports das classes responsáveis por acessar o banco de dados SQLite original:

//import android.database.sqlite.SQLiteDatabase;
//import android.database.sqlite.SQLiteOpenHelper;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLCipherOpenHelper;

Lembre-se que é importante setar a chave do banco assim que ele é aberto. No Android, isso pode ser feito, por exemplo, com o método `getWritableDatabase()`, da classe SQLCipherOpenHelper. O código abaixo mostra esse exemplo, em um adapter simples para o banco:

public class MeuBdAdapter {
    private MeuBdHelper bdHelper;
    private SQLiteDatabase bd;
    private Context context;
    private CacheWordHandler cacheWord;

    private static final String DATABASE_NAME = "bd_com_sqlcipher";
    private static final int DATABASE_VERSION = 1;
    private static final String DATABASE_CREATE = "comando de create do banco";

    public MeuBdAdapter(CacheWordHandler cacheWord, Context context) {
        this.cacheWord = cacheWord;
        this.context = context;
    }

    public MeuBdAdapter open(String senha) throws SQLException {
        bdHelper = new MeuBdHelper(cacheWord, context);
        bd = bdHelper.getWritableDatabase(senha);
        return this;
    }

    private static class MeuBdHelper extends SQLCipherOpenHelper {
        MeuBdHelper(CacheWordHandler cacheWord, Context context) {
            super(cacheWord, context, DATABASE_NAME, null, DATABA
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DATABASE_CREATE);
        }
    }
}

Armazenamento da chave de criptografia

Agora precisamos definir e guardar essa senha para o banco. Qualquer string serve como senha, mas obviamente é melhor escolher uma senha longa que não seja fácil de quebrar por brute-force. Para armazenar a chave, podemos usar a própria APK do aplicativo, salvando por exemplo como uma constante na classe descrita acima. Mas aí chegamos em outro problema: arquivos binários Java não são totalmente compilados, o que significa que é possível fazer engenharia reversa, descompilá-los e ter acesso ao código fonte completo do aplicativo (assim como todos os assets de imagens, fontes, vídeos, etc). Portanto, salvar a chave de acesso direto no código (“hardcoded”) não é uma boa solução.

Chegamos então a um impasse: Tivemos todo o trabalho de criptografar o banco de dados para ninguém ter acesso aos dados dos usuários, porém não temos um jeito fácil de guardar a chave que protege os dados. Uma possível solução é usar algumas técnicas de obfuscação de código e “esconder” a chave em algum canto do código. Dessa forma, mesmo que alguém tenha acesso a todo o código, a chave não estará “à mostra”. Para exemplificar, podemos pegar 2 cores (strings no formato hexadecimal), concatená-las e usar como a chave. Se feito da maneira certa, será quase impossível descobrir a chave. Esse método porém não é garantido de funcionar. Qualquer um que estude o código por tempo suficiente conseguirá achar a chave.

Um método mais seguro é deixar o usuário definir a senha e pedi-la toda vez que ele fizer login no aplicativo, assim não é necessário guardar a chave do banco dentro da APK. O problema é que isso limitaria o aplicativo a ter apenas um usuário, já que a chave do banco é única. Uma possível solução seria usar um banco para cada usuário, mas isso não é prático e necessita de um nível de gerenciamento bem maior no adaptador do banco de dados. Outro problema é que se o usuário esquecer sua senha, todos os dados dele serão perdidos, já que não tem como descriptografar o banco sem a senha.

Concluindo

Podemos perceber que garantir a segurança dos dados dos usuários de um aplicativo Android não é nada fácil, e necessita que várias decisões sejam tomadas sobre o design da solução final. Não há uma solução única que garante a melhor experiência ou segurança, cada solução diferente tem suas vantagens e desvantagens. Espero que este post o tenha ensinado algum truque novo, ou pelo menos ajudado a realçar a importância da segurança ao desenvolver um aplicativo que será potencialmente utilizado por milhares de pessoas e estará guardado no bolso, sempre ao alcance.

Rodrigo Castro

Estou cursando Ciência da Computação da UFF e sou Desenvolvedor Web/Líder Técnico na STI - UFF. Trabalho com Ruby on Rails e também desenvolvimento mobile com Android. Gosto também de projetos DIY e brincar com Arduino, sempre tentando criar algo novo.

< voltar a página de artigos