Advertisement
  1. Code
  2. JavaScript
  3. Web APIs

Stockage des données en toute sécurité sur Android

Scroll to top

() translation by (you can also view the original English article)

La crédibilité d'une application dépend aujourd'hui fortement de la gestion des données privées de l'utilisateur. La pile Android comporte de nombreuses API puissantes entourant les informations d'identification et le stockage des clés, avec des fonctionnalités spécifiques uniquement disponibles dans certaines versions. Cette courte série commencera avec une approche simple pour bien démarrer en parlant du système de stockage et comment crypter et stocker des données sensibles via un code d'accès fourni par l'utilisateur. Dans le deuxième tutoriel, nous verrons des méthodes plus complexes de protéger les clés et les informations d'identification.

Les bases

La première question à laquelle vous devez réfléchir est la quantité de données que vous devez réellement acquérir. Une bonne approche consiste à éviter de stocker des données privées si vous n'y êtes pas obligé.

Pour les données que vous devez stocker, l'architecture Android est prête à vous aider. Depuis la version 6.0 de Marshmellow, le chiffrement intégral du disque est activé par défaut pour les périphériques disposant de cette fonctionnalité. Les fichiers et SharedPreferences enregistrés par l'application sont automatiquement définis avec la constante MODE_PRIVATE. Cela signifie que les données ne sont accessibles que par votre propre application.

C'est une bonne idée de s'en tenir à ce défaut. Vous pouvez le définir explicitement lors de l'enregistrement d'une préférence partagée.

1
SharedPreferences.Editor editor = getSharedPreferences("preferenceName", MODE_PRIVATE).edit();
2
editor.putString("key", "value");
3
editor.commit();

Ou lors de l'enregistrement d'un fichier.

1
FileOutputStream fos = openFileOutput(filenameString, Context.MODE_PRIVATE);
2
fos.write(data);
3
fos.close();

Évitez de stocker des données sur un support de stockage externe, car les données sont ainsi visibles par d'autres applications et utilisateurs. En fait, pour éviter que les utilisateurs ne puissent copier le binaire et les données de votre application, vous pouvez interdire aux utilisateurs d'installer l'application sur un support de stockage externe. Ajouter android:installLocation avec une valeur d' internalOnly au fichier manifest va accomplir cette tâche.

Vous pouvez également empêcher de faire un backup de l'application et de ses données. Cela empêche également le téléchargement du contenu du répertoire de données privé d'une application à l'aide d'adb backup. Pour ce faire, définissez l'attribut android:allowBackup sur false dans le fichier manifest. Par défaut, cet attribut est défini sur true.

Ce sont les meilleures pratiques, mais elles ne fonctionneront pas pour un périphérique compromis ou rooté, et le chiffrement de disque n'est utile que lorsque le périphérique est sécurisé avec un écran de verrouillage. C'est là qu'être doté d'un mot de passe côté application qui protège ses données avec un cryptage est bénéfique.

Sécurisation des données utilisateur avec un mot de passe

Conceal est un excellent choix pour une bibliothèque de chiffrement, car elle vous permet d'accomplir la tâche très rapidement sans avoir à vous soucier des détails sous-jacents. Cependant, un exploit qui cible pour un framework populaire affectera simultanément toutes les applications qui en dépendent.

Il est également important de bien connaître le fonctionnement des systèmes de chiffrement afin de savoir si vous utilisez un framework particulier de manière sécurisée. Donc, pour ce post, nous allons nous salir les mains en regardant directement le fournisseur de cryptographie.

AES et dérivation de clé basée sur un mot de passe

Nous utiliserons la norme AES recommandée, qui crypte les données en fonction d'une clé. La même clé utilisée pour chiffrer les données est utilisée pour déchiffrer les données, ce qui est appelé le chiffrement symétrique. Il existe différentes tailles de clé et AES256 (256 bits) est la longueur préférée pour une utilisation avec des données sensibles.

Alors que l'expérience utilisateur de votre application devrait forcer un utilisateur à utiliser un code fort, il est possible que le même code soit également choisi par un autre utilisateur. Mettre la sécurité de nos données cryptées entre les mains de l'utilisateur n'est pas conseillé. Nos données doivent être sécurisées avec une clé aléatoire et suffisamment large (c'est-à-dire qui a suffisamment d'entropie) pour être considérée comme forte. C'est pourquoi il n'est jamais recommandé d'utiliser un mot de passe directement pour chiffrer les données - c'est là que la fonction PBKDF2 (Password-Based Key Derivation Function) entre en jeu.

PDKDF2 dérive une clé d'un mot de passe en le hachant plusieurs fois avec un salt. C'est ce qu'on appelle l'étirement des touches. Le salt est juste une séquence aléatoire de données et rend la clé dérivée unique même si le même mot de passe a été utilisé par quelqu'un d'autre. Commençons par générer ce salt.

1
SecureRandom random = new SecureRandom();
2
byte salt[] = new byte[256];
3
random.nextBytes(salt);

La classe SecureRandom garantit que la sortie générée sera difficile à prévoir - c'est un "générateur de nombres aléatoires fort cryptographiquement". Nous pouvons maintenant mettre le salt et le mot de passe dans un objet de chiffrement basé sur un mot de passe : PBEKeySpec. Le constructeur de l'objet prend également une forme de compteur d'itération rendant la clé plus forte. C'est parce que l'augmentation du nombre d'itérations augmente le temps qu'il faudrait pour fonctionner sur un jeu de clés pendant une attaque en force brute. PBEKeySpec est ensuite passé dans SecretKeyFactory, qui génère finalement la clé sous la forme d'un tableau byte[]. Nous allons envelopper ce tableau byte[] brut dans un objet SecretKeySpec.

1
char[] passwordChar = passwordString.toCharArray(); //Turn password into char[] array

2
PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); //1324 iterations

3
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
4
byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded();
5
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

Notez que le mot de passe est passé en tant que tableau char[] et la classe PBEKeySpec le stocke également en tant que tableau char[]. Les tableaux char[] sont généralement utilisés pour les fonctions de chiffrement, car si la classe String est immuable, un tableau char[] contenant des informations sensibles peut être écrasé, supprimant ainsi entièrement les données sensibles de la RAM du périphérique.

Vecteurs d'initialisation

Nous sommes maintenant prêts à crypter les données, mais nous avons encore une chose à faire. Il existe différents modes de cryptage avec AES, mais nous utiliserons le mode recommandé : CBC (Cipher Block Chaining). Cela fonctionne sur nos données un bloc à la fois. L'avantage de ce mode est que chaque bloc de données non crypté suivant est XORé avec le bloc crypté précédent pour renforcer le cryptage. Cependant, cela signifie que le premier bloc n'est jamais aussi unique que tous les autres !

Si un message à chiffrer devait être le même qu'un autre message à chiffrer, la sortie chiffrée du début serait la même, ce qui donnerait à un attaquant une idée de ce que pourrait être le message. La solution consiste à utiliser un vecteur d'initialisation (IV).

Un IV est juste un bloc d'octets aléatoires qui seront XORés avec le premier bloc de données utilisateur. Comme chaque bloc dépend de tous les blocs traités jusqu'à ce point, le message entier sera crypté de façon unique; des messages identiques cryptés avec la même clé ne produiront pas des résultats identiques. Créons un IV maintenant.

1
SecureRandom ivRandom = new SecureRandom(); //not caching previous seeded instance of SecureRandom

2
byte[] iv = new byte[16];
3
ivRandom.nextBytes(iv);
4
IvParameterSpec ivSpec = new IvParameterSpec(iv);

Une note à propos de SecureRandom. Sur les versions 4.3 et inférieures, l'architecture de cryptographie Java présentait une vulnérabilité due à une mauvaise initialisation du générateur de nombres pseudo-aléatoires (PRNG) sous-jacent. Si vous ciblez les versions 4.3 et inférieures, un correctif est disponible.

Chiffrer les données

Armé d'un IvParameterSpec, nous pouvons maintenant faire réellement le chiffrement.

1
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
2
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
3
byte[] encrypted = cipher.doFinal(plainTextBytes);

Ici, nous passons dans la chaîne "AES/CBC/PKCS7Padding". Ceci spécifie le cryptage AES avec chaînage de blocs de chiffrement. La dernière partie de cette chaîne fait référence à PKCS7, qui est une norme établie pour le remplissage des données qui ne correspond pas parfaitement à la taille du bloc. (Les blocs ont 128 bits et le remplissage est effectué avant le cryptage.)

Pour compléter notre exemple, nous allons mettre ce code dans une méthode de chiffrement qui va empaqueter le résultat dans un HashMap contenant les données chiffrées, avec le salt et le vecteur d'initialisation nécessaires au décryptage.

1
private HashMap<String, byte[]> encryptBytes(byte[] plainTextBytes, String passwordString)
2
{
3
    HashMap<String, byte[]> map = new HashMap<String, byte[]>();
4
    
5
    try
6
    {
7
        //Random salt for next step

8
        SecureRandom random = new SecureRandom();
9
        byte salt[] = new byte[256];
10
        random.nextBytes(salt);
11
12
        //PBKDF2 - derive the key from the password, don't use passwords directly

13
        char[] passwordChar = passwordString.toCharArray(); //Turn password into char[] array

14
        PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); //1324 iterations

15
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
16
        byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded();
17
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
18
19
        //Create initialization vector for AES

20
        SecureRandom ivRandom = new SecureRandom(); //not caching previous seeded instance of SecureRandom

21
        byte[] iv = new byte[16];
22
        ivRandom.nextBytes(iv);
23
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
24
25
        //Encrypt

26
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
27
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
28
        byte[] encrypted = cipher.doFinal(plainTextBytes);
29
30
        map.put("salt", salt);
31
        map.put("iv", iv);
32
        map.put("encrypted", encrypted);
33
    }
34
    catch(Exception e)
35
    {
36
        Log.e("MYAPP", "encryption exception", e);
37
    }
38
39
    return map;
40
}

La méthode de déchiffrage

Vous avez seulement besoin de stocker l'IV et le salt avec vos données. Alors que les salts et IVs sont considérés comme publics, assurez-vous qu'ils ne sont pas séquentiellement incrémentés ou réutilisés. Pour déchiffrer les données, tout ce que nous devons faire est de changer le mode dans le constructeur Cipher de ENCRYPT_MODE à DECRYPT_MODE. La méthode de déchiffrement prendra un HashMap qui contient les mêmes informations requises (données cryptées, salt et IV) et retourne un tableau byte[] décrypté, avec le mot de passe correct. La méthode de déchiffrement régénère la clé de chiffrement à partir du mot de passe. La clé ne devrait jamais être stockée !

1
private byte[] decryptData(HashMap<String, byte[]> map, String passwordString)
2
{
3
    byte[] decrypted = null;
4
    try
5
    {
6
        byte salt[] = map.get("salt");
7
        byte iv[] = map.get("iv");
8
        byte encrypted[] = map.get("encrypted");
9
10
        //regenerate key from password

11
        char[] passwordChar = passwordString.toCharArray();
12
        PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256);
13
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
14
        byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded();
15
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
16
17
        //Decrypt

18
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
19
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
20
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
21
        decrypted = cipher.doFinal(encrypted);
22
    }
23
    catch(Exception e)
24
    {
25
        Log.e("MYAPP", "decryption exception", e);
26
    }
27
28
    return decrypted;
29
}

Test du chiffrement et du déchiffrement

Pour garder l'exemple simple, nous omettons la vérification d'erreur qui ferait en sorte que le HashMap contienne la clé requise, les paires de valeurs. Nous pouvons maintenant tester nos méthodes pour nous assurer que les données sont déchiffrées correctement après le cryptage.

1
//Encryption test

2
String string = "My sensitive string that I want to encrypt";
3
byte[] bytes = string.getBytes();
4
HashMap<String, byte[]> map = encryptBytes(bytes, "UserSuppliedPassword");
5
6
//Decryption test

7
byte[] decrypted = decryptData(map, "UserSuppliedPassword");
8
if (decrypted != null)
9
{
10
    String decryptedString = new String(decrypted);
11
    Log.e("MYAPP", "Decrypted String is : " + decryptedString);
12
}

Les méthodes utilisent un tableau byte[] pour que vous puissiez chiffrer des données arbitraires au lieu de chiffrer seulement des objets String.

Enregistrement des données chiffrées

Maintenant que nous avons un tableau byte[] crypté, nous pouvons l'enregistrer dans le stockage.

1
FileOutputStream fos = openFileOutput("test.dat", Context.MODE_PRIVATE);
2
fos.write(encrypted);
3
fos.close();

Si vous ne voulez pas enregistrer l'IV et le salt séparément, HashMap est sérialisable avec les classes ObjectInputStream et ObjectOutputStream.

1
FileOutputStream fos = openFileOutput("map.dat", Context.MODE_PRIVATE);
2
ObjectOutputStream oos = new ObjectOutputStream(fos);
3
oos.writeObject(map);
4
oos.close();

Enregistrement de données sécurisées dans SharedPreferences

Vous pouvez également enregistrer des données sécurisées dans les SharedPreferences de votre application.

1
SharedPreferences.Editor editor = getSharedPreferences("prefs", Context.MODE_PRIVATE).edit();
2
String keyBase64String = Base64.encodeToString(encryptedKey, Base64.NO_WRAP);
3
String valueBase64String = Base64.encodeToString(encryptedValue, Base64.NO_WRAP);
4
editor.putString(keyBase64String, valueBase64String);
5
editor.commit();

Puisque SharedPreferences est un système XML qui n'accepte que des primitives et des objets spécifiques en tant que valeurs, nous devons convertir nos données en un format compatible tel qu'un objet String. Base64 nous permet de convertir les données brutes en une représentation String qui contient uniquement les caractères autorisés par le format XML. Chiffrez à la fois la clé et la valeur afin qu'un attaquant ne puisse pas déterminer quelle valeur pourrait être. Dans l'exemple ci-dessus, encryptedKey et encryptedValue sont tous les deux des tableaux byte[] renvoyés par notre méthode encryptBytes(). L'IV et le salt peuvent être sauvegardés dans le fichier de préférences ou dans un fichier séparé. Pour récupérer les octets cryptés à partir des SharedPreferences, nous pouvons appliquer un décodage Base64 sur le String stockée.

1
SharedPreferences preferences = getSharedPreferences("prefs", Context.MODE_PRIVATE);
2
String base64EncryptedString = preferences.getString(keyBase64String, "default");
3
byte[] encryptedBytes = Base64.decode(base64EncryptedString, Base64.NO_WRAP);

Essuyage des données non sécurisées des anciennes versions

Maintenant que les données stockées sont sécurisées, il se peut que vous ayez une version précédente de l'application qui a stocké les données de manière non sécurisée. Lors d'une mise à jour, les données peuvent être essuyées et re-chiffrées. Le code suivant essuie un fichier en utilisant des données aléatoires.

En théorie, vous pouvez simplement supprimer vos préférences partagées en supprimant les fichiers /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml et your_prefs_name.bak et en effaçant les préférences en mémoire avec le code suivant :

1
getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().commit();

Cependant, au lieu de tenter d'essuyer les anciennes données et d'espérer que cela fonctionne, il est préférable de les crypter en premier lieu ! Cela est particulièrement vrai en général pour les lecteurs à semi-conducteurs qui étalent souvent l'écriture de données dans différentes régions pour éviter l'usure. Cela signifie que même si vous remplacez un fichier dans le système de fichiers, la mémoire SSD physique peut conserver vos données dans leur emplacement d'origine sur le disque.

1
public static void secureWipeFile(File file) throws IOException
2
{
3
    if (file != null && file.exists())
4
    {
5
        final long length = file.length();
6
        final SecureRandom random = new SecureRandom();
7
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rws");
8
        randomAccessFile.seek(0);
9
        randomAccessFile.getFilePointer();
10
        byte[] data = new byte[64];
11
        int position = 0;
12
        while (position < length)
13
        {
14
            random.nextBytes(data);
15
            randomAccessFile.write(data);
16
            position += data.length;
17
        }
18
        randomAccessFile.close();
19
        file.delete();
20
    }
21
}

Conclusion

Cela conclut notre tutoriel sur le stockage des données cryptées. Dans cet article, vous avez appris comment crypter et décrypter de manière sécurisée les données sensibles avec un mot de passe fourni par l'utilisateur. C'est facile à faire quand vous savez la méthode, mais il est important de suivre toutes les bonnes pratiques pour assurer la sécurité des données de vos utilisateurs.

Dans la prochaine publication, nous allons voir comment tirer parti du KeyStore et d'autres API liées aux informations d'identification pour stocker les éléments en toute sécurité. En attendant, consultez quelques-uns de nos autres articles sur le développement d'applications Android.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.