La crédibilité d'une application aujourd'hui dépend fortement de la manière dont les données privées de l'utilisateur sont gérées. La pile Android possède de nombreuses API puissantes entourant le stockage des informations d'identification et des clés, avec des fonctionnalités spécifiques uniquement disponibles dans certaines versions..
Cette courte série commencera par une approche simple pour se mettre en marche en examinant le système de stockage et la manière de chiffrer et de stocker les données sensibles via un code d'authentification fourni par l'utilisateur. Dans le deuxième tutoriel, nous examinerons des moyens plus complexes de protéger les clés et les informations d'identification..
La première question à laquelle vous devez réfléchir est de savoir combien de données vous devez réellement acquérir. Une bonne approche consiste à éviter de stocker des données privées si vous n’avez pas à le faire..
Pour les données que vous devez stocker, l'architecture Android est prête à vous aider. Depuis 6.0 Marshmallow, le chiffrement intégral du disque est activé par défaut pour les périphériques dotés de cette fonctionnalité. Fichiers et Préférences partagées
qui sont enregistrés par l'application sont automatiquement définis avec le MODE_PRIVATE
constant. Cela signifie que les données ne peuvent être consultées que par votre propre application..
C'est une bonne idée de s'en tenir à ce défaut. Vous pouvez le définir explicitement lorsque vous enregistrez une préférence partagée..
SharedPreferences.Editor editor = getSharedPreferences ("preferenceName", MODE_PRIVATE) .edit (); editor.putString ("clé", "valeur"); editor.commit ();
Ou lors de la sauvegarde d'un fichier.
FileOutputStream fos = openFileOutput (filenameString, Context.MODE_PRIVATE); fos.write (données); fos.close ();
Évitez de stocker des données sur un stockage externe, car les données sont ensuite visibles par d'autres applications et utilisateurs. En fait, pour empêcher les utilisateurs de copier les données binaires et les données de votre application, vous pouvez empêcher les utilisateurs d'installer l'application sur un stockage externe. Ajouter android: installLocation
avec une valeur de interneOnly
au fichier manifeste va accomplir cela.
Vous pouvez également empêcher l'application et ses données d'être sauvegardées. Cela empêche également le téléchargement du contenu du répertoire de données privé d'une application à l'aide de sauvegarde adb
. Pour ce faire, réglez le Android: allowBackup
attribuer à faux
dans le fichier manifeste. Par défaut, cet attribut est défini sur vrai
.
Ce sont des pratiques recommandées, mais elles ne fonctionneront pas pour un périphérique compromis ou enraciné, 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'avoir un mot de passe côté application qui protège ses données avec le cryptage est bénéfique.
La dissimulation est un excellent choix pour une bibliothèque de chiffrement, car elle vous permet de fonctionner très rapidement sans vous soucier des détails sous-jacents. Cependant, un exploit destiné à un framework populaire affectera simultanément toutes les applications qui en dépendent..
Il est également important de connaître le fonctionnement des systèmes de chiffrement afin de pouvoir savoir si vous utilisez un framework particulier de manière sécurisée. Donc, pour cet article, nous allons nous salir les mains en regardant directement le fournisseur de cryptographie.
Nous utiliserons le standard AES recommandé, qui chiffre les données avec une clé. La même clé utilisée pour chiffrer les données est utilisée pour déchiffrer les données, appelée chiffrement symétrique. Il existe différentes tailles de clé et AES256 (256 bits) est la longueur recommandée pour une utilisation avec des données sensibles..
Bien que l'expérience utilisateur de votre application doive forcer un utilisateur à utiliser un code d'accès fort, il est possible que le même code d'accès 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 sûr. Nos données doivent être sécurisées à la place avec un clé qui est aléatoire et assez grand (c'est-à-dire qui a suffisamment d'entropie) pour être considéré comme fort. C’est pourquoi il n’est jamais recommandé d’utiliser un mot de passe directement pour chiffrer des données. C’est là qu’une fonction appelée Fonction de dérivation de clé basée sur un mot de passe (PBKDF2) entre en jeu.
PBKDF2 dérive un clé de mot de passe en le hachant plusieurs fois avec un sel. C'est ce qu'on appelle l'étirement des clés. Le sel 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 sel.
SecureRandom random = new SecureRandom (); sel d'octet [] = nouvel octet [256]; random.nextBytes (salt);
le SecureRandom
La classe garantit que la sortie générée sera difficile à prédire - il s'agit d'un "générateur de nombres aléatoires cryptographiquement fort". Nous pouvons maintenant mettre le sel 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 compte d'itération, ce qui renforce la clé. En effet, l'augmentation du nombre d'itérations augmente le temps nécessaire pour utiliser un jeu de clés lors d'une attaque par force brute. le PBEKeySpec
puis est passé dans le SecretKeyFactory
, qui génère finalement la clé en tant que octet[]
tableau. Nous allons emballer ça brut octet[]
tableau dans un SecretKeySpec
objet.
char [] passwordChar = passwordString.toCharArray (); // Transforme le mot de passe en tableau char [] array PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 itérations SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES");
Notez que le mot de passe est passé en tant que carboniser[]
tableau, et le PBEKeySpec
classe stocke comme un carboniser[]
tableau aussi bien. carboniser[]
Les tableaux sont généralement utilisés pour les fonctions de chiffrement, car Chaîne
la classe est immuable, un carboniser[]
un tableau contenant des informations sensibles peut être écrasé - supprimant ainsi entièrement les données sensibles de la mémoire de l'appareil.
Nous sommes maintenant prêts à chiffrer les données, mais nous avons encore une chose à faire. Il existe différents modes de cryptage avec AES, mais nous utiliserons celui recommandé: le chaînage des blocs de chiffrement (CBC). 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 traité avec 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 commençait de la même façon qu'un autre message à chiffrer, la sortie chiffrée initiale serait la même et donnerait à l'attaquant un indice lui permettant de déterminer le contenu du message. La solution consiste à utiliser un vecteur d'initialisation (IV).
Un IV est juste un bloc d'octets aléatoires qui seront XOR avec le premier bloc de données utilisateur. Étant donné que chaque bloc dépend de tous les blocs traités jusque-là, l'ensemble du message sera crypté. Des messages identiques identiques cryptés avec la même clé ne produiront pas des résultats identiques..
Créons un IV maintenant.
SecureRandom ivRandom = new SecureRandom (); // ne met pas en cache l'instance source précédente de SecureRandom byte [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv);
Une note à propos de SecureRandom
. Sur les versions 4.3 et antérieures, Java Cryptography Architecture comportait une vulnérabilité due à une initialisation incorrecte du générateur de nombre pseudo-aléatoire sous-jacent (PRNG). Si vous ciblez les versions 4.3 et inférieures, un correctif est disponible..
Armé d'un IvParameterSpec
, nous pouvons maintenant faire le cryptage réel.
Cipher Cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] crypté = cipher.doFinal (plainTextBytes);
Ici on passe dans la ficelle "AES / CBC / PKCS7Padding"
. Ceci spécifie le cryptage AES avec chaînage de blocs chiffrés. 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 s’intègrent pas parfaitement dans la taille du bloc. (Les blocs sont de 128 bits et le remplissage est effectué avant le cryptage.)
Pour compléter notre exemple, nous allons placer ce code dans une méthode de chiffrement qui compilera le résultat dans une HashMap
contenant les données chiffrées, ainsi que le sel et le vecteur d'initialisation nécessaires au déchiffrement.
HashMap privéeencryptBytes (byte [] plainTextBytes, String passwordString) HashMap map = new HashMap (); try // Sel aléatoire pour l'étape suivante SecureRandom random = new SecureRandom (); sel d'octet [] = nouvel octet [256]; random.nextBytes (salt); // PBKDF2 - dérivez la clé du mot de passe, n'utilisez pas de mot de passe directement char [] passwordChar = passwordString.toCharArray (); // Transforme le mot de passe en tableau char [] array PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 itérations SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES"); // Création du vecteur d'initialisation pour AES SecureRandom ivRandom = new SecureRandom (); // ne met pas en cache l'instance source précédente de SecureRandom byte [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv); // Encrypt Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] crypté = cipher.doFinal (plainTextBytes); map.put ("sel", sel); map.put ("iv", iv); map.put ("crypté", crypté); catch (Exception e) Log.e ("MYAPP", "exception de chiffrement", e); carte de retour;
Il vous suffit de stocker l'IV et le sel avec vos données. Bien que les sels et les solutions intraveineuses soient considérés comme publics, assurez-vous qu'ils ne sont pas incrémentés ou réutilisés de manière séquentielle. Pour déchiffrer les données, il suffit de changer le mode dans le Chiffrer
constructeur de ENCRYPT_MODE
à DECRYPT_MODE
.
La méthode de déchiffrement prendra un HashMap
contenant les mêmes informations requises (données cryptées, salt et IV) et renvoyer un message décrypté. octet[]
tableau, étant donné le mot de passe correct. La méthode de déchiffrement régénérera la clé de chiffrement à partir du mot de passe. La clé ne doit jamais être stockée!
octet privé [] decryptData (HashMapmap, String passwordString) octet [] décrypté = null; try byte salt [] = map.get ("salt"); octet iv [] = map.get ("iv"); octet crypté [] = map.get ("crypté"); // régénère la clé à partir du mot de passe char [] passwordChar = passwordString.toCharArray (); PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES"); // Décrypter Cipher Cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = new IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); déchiffré = cipher.doFinal (chiffré); catch (Exception e) Log.e ("MYAPP", "exception de décryptage", e); return decrypted;
Pour garder l’exemple simple, nous omettons la vérification des erreurs afin de nous assurer que le HashMap
contient la clé requise, paires de valeur. Nous pouvons maintenant tester nos méthodes pour nous assurer que les données sont déchiffrées correctement après le chiffrement..
// Test de chiffrement String string = "Ma chaîne sensible que je veux chiffrer"; byte [] bytes = string.getBytes (); HashMapmap = encryptBytes (bytes, "UserSuppliedPassword"); // Octet de test de déchiffrement [] decrypted = decryptData (map, "UserSuppliedPassword"); if (decrypted! = null) String decryptedString = new String (déchiffré); Log.e ("MYAPP", "Chaîne déchiffrée est:" + decryptedString);
Les méthodes utilisent un octet[]
tableau de sorte que vous pouvez chiffrer des données arbitraires au lieu de seulement Chaîne
objets.
Maintenant que nous avons un crypté octet[]
tableau, nous pouvons l'enregistrer au stockage.
FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (crypté); fos.close ();
Si vous ne voulez pas enregistrer l'IV et le sel séparément, HashMap
est sérialisable avec le ObjectInputStream
et ObjectOutputStream
Des classes.
FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject (map); oos.close ();
Préférences partagées
Vous pouvez également enregistrer des données sécurisées sur votre application. Préférences partagées
.
SharedPreferences.Editor editor = getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (); String keyBase64String = Base64.encodeToString (encryptedKey, Base64.NO_WRAP); String valueBase64String = Base64.encodeToString (encryptedValue, Base64.NO_WRAP); editor.putString (keyBase64String, valueBase64String); editor.commit ();
Depuis le Préférences partagées
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 dans un format compatible, tel qu'un fichier. Chaîne
objet. Base64 nous permet de convertir les données brutes en un Chaîne
représentation qui ne contient que les caractères autorisés par le format XML. Cryptez à la fois la clé et la valeur afin qu'un attaquant ne puisse pas déterminer quelle valeur peut être.
Dans l'exemple ci-dessus, clé cryptée
et valeur chiffrée
sont tous les deux cryptés octet[]
tableaux retournés de notre encryptBytes ()
méthode. Le vecteur d'injection et le sel peuvent être enregistrés dans le fichier de préférences ou dans un fichier séparé. Pour récupérer les octets cryptés de la Préférences partagées
, nous pouvons appliquer un décodage Base64 sur le stocké Chaîne
.
SharedPreferences preferences = getSharedPreferences ("prefs", Context.MODE_PRIVATE); String base64EncryptedString = preferences.getString (keyBase64String, "default"); byte [] encryptedBytes = Base64.decode (base64EncryptedString, Base64.NO_WRAP);
Maintenant que les données stockées sont sécurisées, il est possible que vous disposiez d'une version précédente de l'application dans laquelle les données étaient stockées de manière non sécurisée. Lors d'une mise à niveau, les données pourraient être effacées et rechiffrées. Le code suivant efface un fichier en utilisant des données aléatoires.
En théorie, vous pouvez simplement supprimer vos préférences partagées en supprimant le /data/data/com.votre.package.nom/shared_prefs/votre_nom_prefs.xml et your_prefs_name.bak fichiers et effacer les préférences en mémoire avec le code suivant:
getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (). clear (). commit ();
Cependant, au lieu d'essayer d'effacer les anciennes données et d'espérer que cela fonctionne, il est préférable de les chiffrer en premier lieu! Cela est particulièrement vrai en général pour les disques à semi-conducteurs qui répartissent souvent les écritures de données dans différentes régions pour éviter l'usure. Cela signifie que même si vous écrasez un fichier dans le système de fichiers, la mémoire SSD peut conserver vos données dans leur emplacement d'origine sur le disque..
public statique void secureWipeFile (fichier fichier) lève IOException if (fichier! = null && fichier.exists ()) final long length = fichier.length (); SecureRandom final random = new SecureRandom (); final RandomAccessFile randomAccessFile = new RandomAccessFile (fichier, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); octet [] données = nouvel octet [64]; int position = 0; while (position < length) random.nextBytes(data); randomAccessFile.write(data); position += data.length; randomAccessFile.close(); file.delete();
Cela termine notre tutoriel sur le stockage de données cryptées. Dans cet article, vous avez appris à chiffrer et déchiffrer en toute sécurité des données sensibles avec un mot de passe fourni par l'utilisateur. C'est facile à faire quand on sait comment faire, mais il est important de suivre toutes les meilleures pratiques pour assurer la sécurité des données de vos utilisateurs..
Dans le prochain article, nous verrons comment tirer parti de la Magasin de clés
et d'autres API liées aux informations d'identification pour stocker les éléments en toute sécurité. En attendant, découvrez d'autres de nos excellents articles sur le développement d'applications Android..