Sécuriser une base de donnée SQLite par chiffrement, et la déchiffrer dans une application mobile iOS et Android

| 3 Comments | 15 minutes read

Cet article présente comment sécuriser une base de donnée SQLite lorsqu’elle est embarquée dans une application mobile, pour iOS et Android.

Nous verrons :

  • comment chiffrer une base de donnée SQLite
  •  le code source nécessaire pour déchiffrer à la volée la base SQLite dans une application mobile (Étape 2 : iOS, et Étape 3 : Android)
  • comment automatiser le chiffrement de votre base dans votre processus de build (sections Intégration dans le processus de build par un script pour chaque plateforme mobile)

Le chiffrage est fait via SQLCipher, une extension open source de SQLite.

Téléchargements :

Chiffrage de la base

L’outil en ligne de commande pour Mac OS X pour chiffrer la base est disponible sur le github ekito (source et binaire).

Déchiffrage de la base

Le code pour le déchiffrage dans les applications iOS et Android est présenté aux étapes 2 et 3 de cet article.
Un projet de démo est présent pour chaque plateforme sur le github ekito (chiffrage sous Mac OS X, déchiffrage sous iOS et Android).

Au cours d’un récent exercice de reverse engineering d’une application mobile, j’ai fini par m’intéresser au contenu d’une base de donnée SQLite présente dans les ressources de l’application.
J’ai pu accéder à l’intégralité du contenu de la base, à l’exception d’une des colonnes de la base. Evidemment, c’est de celle ci dont j’avais besoin.

Ayant eu la confirmation que le fichier que j’avais trouvé était une base de donnée SQLite, j’ai pu par tentatives successives obtenir l’information dont j’avais besoin. Cette expérience passionnante, qui commence avec l’interception de quelques requêtes réseau, puis mêle du jailbreaking, un détournement inattendu des fonctions usuelles de gdb, quelques class-dump, de l’assembleur, accompagnés de scripts pour lier le tout, sera publié prochainement ici.

Avant de publier mon article sur le reverse engineering, je me suis dit que j’allais chercher et partager comment sécuriser proprement (et facilement) une base de donnée SQLite embarquée dans une application mobile.

Pourquoi chiffrer une base de donnée SQLite ?

Préambule sur la nomenclature

En français correct, on parle de chiffrement pour protéger un message, et déchiffrement pour l’opération inverse.
Les termes crypter et décrypter sont des anglicismes, et cryptage et décryptage des utilisations communément utilisées, mais par abus de langage et sans justification étymologique. Un langage vivant étant par définition en évolution, libre à vous de choisir le terme qui vous plaît.

A noter, d’après wikipedia, le terme décryptage est l’opération de récupérer un message chiffré sans connaître la clé.

Pourquoi vouloir protéger une base de donnée ?

Le cas le plus courant : vous avez besoin que vos utilisateurs puissent accéder au contenu de la base de donnée via l’application ; mais vous ne voulez pas qu’ils puissent récupérer la base pour l’étudier / et ou la manipuler.
Et vous voulez encore moins que vos concurrents aient accès à votre base de donnée, hautement confidentielle et stratégique, cela va de soi 😉 !

Quelques exemple de contexte :

  • voilà la liste des coordonnées GPS de nos magasins avec les horaires d’ouverture.
    On veut que les clients y aient accès dans la section “Localiser mon Magasin” de notre superbe future appli.
    Par contre, on ne veut surtout pas que ces informations soient accessibles à quelqu’un d’autre.
  • empêcher les joueurs de votre jeu de consulter et modifier le tableau des meilleurs scores

Avantages et limitations de SQLCypher

Ceci n’est pas une base de donnée. C’est peut-être une base de donnée cryptée.

Ceci n’est pas une base de donnée. C’est peut-être une base de donnée cryptée.

Image sous licence Creative Commons (Attribution-Noncommercial-Share Alike 3.0 Unported).
Icône de la base de donnée par Barry Mieny, également sous licence Creative Commons (Attribution-Noncommercial-Share Alike 3.0 Unported).

L’avantage du choix de ce composant est qu’il est totalement transparent dans le code de l’application :

  • Pas besoin de modifier les requêtes
  • La base de donnée reste bien évidemment accessible en lecture comme en écriture

Une fois la clef de déchiffrement indiquée, toutes les requêtes de sélection ou mise à jour du contenu sont automatiquement interceptées par SQLCipher, qui se charge de gérer le chiffrement / déchiffrement.

L’autre avantage est que le fichier de la base de donnée cryptée est non distinguable d’un fichier contenant des données aléatoires. Aucun élément ne laisse transparaitre la nature du fichier.

Une fois mise en place, aucune limitation ou affectation des performances n’a été rencontrée sur cette solution.

La seule limitation repose sur la protection de votre clef, garante de la sécurité générale. Comme expliqué en conclusion, il n’existe pas de moyen de totalement la protéger.

Algorithme de chiffrement

L’algorithme par défaut est AES 256-bits, en mode CBC, avec une dérivation de la clef (voir détails ici).
Le chiffrement n’est pas fait par SQLite ; il repose sur OpenSSL.

Compatibilité

La liste des plateformes supportées est assez large :

SQLCipher has broad platform support for with C/C++, Obj-C, QT, Win32/.NET, Java, Python, Ruby, Linux, Mac OS XiPhone/iOSAndroidXamarin.iOS, and Xamarin.Android.

License

La license adoptée par ce projet est BSD.

 

Étape 1 : chiffrer la base de donnée

Pré-requis : Mac OS X.

Le but est de transformer notre fichier de base de donnée SQLite en clair en une base chiffrée. Dommage, je n’ai trouvée aucune info claire à ce sujet sur le site officiel de SQLCipher.

Je me suis vite aperçu que la solution préconisée par le reste d’internet nécessitait plusieurs étapes manuelles.

ATTACH DATABASE 'YourDatabase.db' AS encrypted KEY 'yourSecretKey';
SELECT sqlcipher_export('encrypted');
DETACH DATABASE encrypted;
Test des opérations de chiffrement

Test des opérations de chiffrement

Très bien, ça fonctionne.

Mais ça ne m’a pas plu :

  •  pas de possibilité d’intégrer ça dans un processus de build ou d’intégration continue (donc impossibilité de récupérer automatiquement le dernier jeu de donnée lors de la compilation)
  •  risque d’erreur :
    • il pourrait être dramatique de faire une erreur de manip sans s’en apercevoir et publier une base non chiffrée, lisible en clair
    • chiffrer deux fois de suite la base empêcherait l’application de s’y connecter
  •  je suis feignant

C’est donc parti pour la création d’un outil utilisable en ligne de commande pour automatiser ce processus de chiffrage.

Le code de ce projet de chiffrage est disponible sur le github ekito.

Une version binaire de l’outil est disponible sur la page des releases.

L’utilisation se fait comme ceci :

sqlite-sqlcipher-crypt ./yourSQLiteDatabase.sqlite3 yourVerySecretKey4242
Capture d'écran de l'opération de chiffrage de la base de donnée SQLite avec sqlite-sqlcipher-crypt

Capture d’écran de l’opération de chiffrage de la base de donnée SQLite avec sqlite-sqlcipher-crypt

La philosophie adoptée lors du développement de cet outil est de s’arrêter avec une erreur dès que les données d’entrées sont invalides.

Cela permet de rendre impossible de pousser en production une base chiffrée plus d’une fois (donc non accessible depuis l’appli).

Entre autre, le chiffrement ne se fera que sur une base de donnée SQLite lisible (non chiffrée).

L’outil s’arrêtera et retournera un code d’erreur différent de 0 en cas de paramètres invalides, de fichier de base de donnée à chiffrer inexistant ou de toute autre erreur…

En cas de succès, la base de donnée en clair sera remplacée par la nouvelle base de donnée chiffrée.

Étape 2 : iOS – déchiffrer la base de donnée

Code pour déchiffrer la base

Nous avons chiffré notre base. Très bien !
Maintenant, il serait souhaitable de pouvoir y accéder en lecture 🙂

Pour cela, le tutorial pour iOS sur SQLCipher (ici, mais ne cliquez pas sur ce lien) est tout simplement trop compliqué et clairement plus à jour. Dommage.

Je pense que le business model de SQLCipher, qui vends dans son édition commerciale des binaires et bibliothèques compilés ne favorise pas le travail sur cet aspect du composant. Ce n’est pas toujours évident de trouver un business model qui concilie la chèvre, le choux et la survie du paysan 🙂

La bonne approche : intégrez SQLCipher en utilisant CocoaPods (introduction à CocoaPods en français ici).
Si vous avez déjà géré la connexion à une base SQLite dans votre appli, cela sera très simple : une ligne à modifier, et une ligne à rajouter.

Comme le montre la capture d’écran ci-dessous, FMDB fournit une sub-spec pour FMDB/SQLCipher.

pod search FMDB

N’oubliez pas de supprimer votre précédent pod pour FMDB.

Votre Podfile doit maintenant ressembler à :

target "MySQLCipherDemo" do 
    pod 'FMDB/SQLCipher' #  A Cocoa / Objective-C wrapper around SQLite.
end

On récupère les mises à jour avec :

pod install
Capture d'écran de `pod install`

Capture d’écran de `pod install`

Dans votre code de l’appli iOS, rajoutez la ligne indiquant la clef secrète, juste après l’ouverture de la base, et avant toute requête :

if (! [self.myDatabase open]) {
    NSLog(@"Could not open the database at %@", databaseFilePath);
    self.myDatabase = nil;
 
    return NO;
}
 
// Votre seul ajout dans le code de votre projet iOS :
[self.myDatabase setKey:@“your”VerySecretKey4242];

C’est tout 🙂

Comment garder votre clef secrète… secrète !

A noter : les binaires téléchargés depuis l’AppStore sont maintenant crypté (1 point de bonus si vous savez depuis quand).
Votre clef n’est donc plus autant exposée qu’avant. Voir cependant la note en conclusion à ce sujet.

SQLCipher permet de fournir des données brutes, par exemple.

Une autre idée évoquée mais non testée : sauvegarder votre clef dans une keychain iOS, et l’inclure dans le bundle du projet.

Sur le même sujet, sachez qu’il existe un moyen de déclarer à iOS le niveau de sécurité associé à un fichier (doc Apple).
C’est un bon moyen de protéger les données de l’utilisateur en cas de vol de son téléphone, mais cela ne vous protège pas des accès non voulus de l’utilisateur à des fichiers hébergés sur son téléphone.

Cependant, si Apple décide de ne pas déchiffrer un fichier protégé par la valeur NSFileProtectionComplete pour la clef NSFileProtectionKey sur un périphérique jailbreaké, alors la situation s’améliore encore un peu… mais toujours rien d’absolu.
Je n’ai pas fait le test pour connaître le comportement de NSFileManager sur un périphérique jailbreaké.

Intégration dans le processus de build par un script

Dans cette section, nous mettons en place un script qui va chiffrer automatiquement la base de donnée SQLite.
Cela permet de copier la nouvelle base dans les resources du projet, sans devoir la chiffrer manuellement. Cela sera fait automatiquement à chaque build, ce qui évitera les oublis.

Ajoutez un script à votre processus de build :

Xcode > Editor > Add Build Phase > Add Run Script Build Phase

Xcode > Editor > Add Build Phase > Add Run Script Build Phase

Ce script sera executé à chaque fois que vous compilez votre projet.

Editez le comme suit, avec le code suivant :

"sqlite-sqlcipher-crypt" "${SRCROOT}/MyApp/resources/mydatabase.sqlite3" "MySecretKey"
 
status_code=$?
if [ "$status_code" -eq "4" ]; then
# silencing warning if database already crypted (as we most likely already crypted it in a previous run)
# Note: I'm not using the Input Files to run this only when database changes, as in the past, I discovered that it is not reliable. Things may have improved (was many Xcode versions ago), but no time to play
exit 0;
fi
exit $status_code;

La 1ère ligne crypte la base de donnée.
La ligne 4 et suivante vérifie le code de retour renvoyé par l’outil. Si c’est 4, alors on a probablement déjà chiffré la base lors d’un build précédent ; sinon, on renvoie le code reçu, qui sera affiché par Xcode.

Chiffrement de la base via le process de build d'Xcode

Chiffrement de la base via le process de build d’Xcode

Soumission de l’appli à iTunes Connect à Apple

Au moment de l’envoi de votre application sur iTunes Connect, vous devez maintenant déclarer utiliser des technologies de chiffrement à l’intérieur de votre application.
Cela impliquera de faire les déclarations et formalités administratives imposées par la loi américaine. Quelques infos sur cet article. Bonne chance !

N’oubliez pas d’envoyer à la NSA la clef utilisée pour le chiffrement – un mail envoyé à vous même devrait suffire 😉

Étape 3 : Android – déchiffrer la base de donnée

Code pour déchiffrer la base

La première chose à faire, est de regarder sur le site consacré pour la partie android et de télécharger la dernière version de sqlcypher (ici la 3.0.2) : http://sqlcipher.net/sqlcipher-for-android/

On va travailler avec intellij. Tout d’abord configurer les librairies externes :

intellij_build

Il suffit de renseigner la section dependencies avec les jar ajoutés dans le répertoire libs, et la librairie guava.

Coté binaires natifs de cryptage, il faut les placer comme suit (assets et jniLibs) :

intellij3

Le fichier icudt46l.zip doit être placé dans le répertoire assets. Les librairies natives (fichiers .so) doivent être placés dans le répertoire jniLibs.

Nous pouvons maintenant charger notre base de données embarquée dans l’application. Ici nous utilisons directement la base de données depuis notre seule activity :

sqlcipher_intellij1

Dans l’exemple ci dessus (méthode sampleGetDataFromDatabase), nous utilisons dans un premier la base non cryptée. puis nous accédons à la base cryptée. Il ne faut pas oublier de charger les librairies natives :

SQLiteDatabase.loadLibs(this)

L’API sqlcypher se base sur l’API de base de données android. Pour accéder à une base de données cryptée, il suffit de procéder avec un helper. Il vous faudra tout de même garder la clé sous le coude, afin de la donner à l’API :

sqlcipher_intellij2

Si vous avez vu des articles pour l’adoption de SQLCipher sur d’autres plateformes d’application mobile, partagez les en commentaires.

Analyse de l’impact des performances

TL;DR: cette surcouche  de chiffrement n’a pas d’impact sur la taille de la base et la vitesse d’exécution des requêtes. Le temps de chiffrage reste très faible.

Voici quelques indications de l’impact constaté sur les performances.

Temps de chiffrement :

Sur une base de donnée de 17Mb (une vingtaine de table, et un total de 180 000 lignes), le temps de chiffrement mesuré est de 1,6 seconde :

Capture d’écran de la commande `time sqtime sqlite-sqlcipher-crypt myDatabase.sqlite yourVerySecretKey4242`

Capture d’écran de la commande `time sqtime sqlite-sqlcipher-crypt myDatabase.sqlite yourVerySecretKey4242`

Sur une base de donnée de 473 Ko, le temps est de 0,03 seconde.

Taille de fichier :

Sur une base de donnée (déjà optimisée par l’opération VACUUM), la taille du fichier chiffré est très très légèrement inférieure au fichier d’entrée (de l’ordre de quelques %).

Impact sur les performances :

Aucune mesure n’a été faite à ce sujet.
Aucun ralentissement ni consommation mémoire inhabituelle n’a été constaté dans l’application l’utilisant.

Le site officiel a un article avec quelques mesures de l’impact sur les différentes requêtes (CREATE, SELECT, UPDATE, INSERT).

Conclusion

Le chiffrement de la base est maintenant automatisé. Le déchiffrement est extrêmement simple à mettre en place.
Il est désormais beaucoup plus compliquée d’accéder à la base pour la lire ou la modifier. Si vous êtes un peu créatif, il sera même difficile de savoir qu’il y a ces données.

Cependant, n’oubliez pas que vous fournissez le binaire d’une application à vos utilisateurs, qui ont un accès total au matériel.
Quelqu’un de motivé et avec du temps pourra obtenir votre clef en attachant un debugger au process de votre application. Il est à ce moment trivial de récupérer la clef secrète… et donc, de déchiffrer votre base.

Cela nécessite quelques compétences (et éventuellement un jailbreak sur iOS). Il y a des moyens de compliquer ces actions, mais rien d’incontournable.

Github

Les sources des projets sont disponibles sur notre github : https://github.com/Ekito/SQLite_crypt-for_mobile_app

Share Button

Arnaud Giuliani Author: Arnaud Giuliani

French Java Software Tech, create and run #java #server gears (distributed and other under the hood stuffs). Also like to make #android apps

Guillaume Cerquant Author: Guillaume Cerquant

Entrepreneur and geek,
focused on User Experience and Innovation.

3 Comments

  1. Pour répondre à la conclusion expliquée ici, la solution que vous présentez ne vous protégera pas du tout d’une personne malveillante.

    J’ai déjà eu ce type de problématique sur une mission, et la seule méthode que j’avais effectivement trouvée était de mêler ce mécanisme à une donnée que seul l’utilisateur était à même de connaître.

    En l’occurrence, j’ai utilisé comme clé de chiffrement de la base certaines données de l’utilisateur, dont son mot de passe de connexion. Ces données étaient bien entendu elles-même chiffrées et salées avec un algorithme de chiffrement asymétrique.

  2. Guillaume Cerquant

    Bonjour Vincent 🙂
    On est d’accord !

    Dans ton contexte, tu voulais protéger les données d’un utilisateur, en empêchant qu’une tierce personne puisse y avoir accès, c’est ça ?

    Dans notre cas (qui a mené à la rédaction de cet article), nous voulions empêcher n’importe quel utilisateur d’accéder aux données présentes dans les ressources de l’appli à lors de son téléchargement.

    L’outil proposé permet les 2 usages.

    Effectivement, malheureusement, il n’y a pas de solution parfaite.
    Néanmoins ce genre de solution augmente le niveau de complexité pour qu’une personne malveillante (ou… juste curieuse) arrive à ses fins.

  3. Bonjour et merci pour cet excellent tutoriel.

Leave a Reply

Required fields are marked *.


CommentLuv badge