Dans le précédent article sur la sécurité des données utilisateur Android, nous avions examiné le chiffrement des données via un code d'accès fourni par l'utilisateur. Ce tutoriel va mettre l'accent sur le stockage des informations d'identification et des clés. Je vais commencer par introduire les informations d'identification du compte et terminer par un exemple de protection des données à l'aide de KeyStore..
Souvent, lorsque vous travaillez avec un service tiers, une forme d'authentification est requise. Cela peut être aussi simple qu'un /s'identifier
point final qui accepte un nom d'utilisateur et un mot de passe.
Au début, il semblerait qu’une solution simple consiste à créer une interface utilisateur qui demande à l’utilisateur de se connecter, puis de capturer et de stocker ses informations de connexion. Cependant, ce n'est pas la meilleure pratique car notre application ne devrait pas avoir besoin de connaître les informations d'identification pour un compte tiers. Au lieu de cela, nous pouvons utiliser le gestionnaire de compte, qui délègue pour nous les informations sensibles..
Le gestionnaire de compte est une aide centralisée pour les informations d'identification du compte utilisateur, de sorte que votre application ne doit pas gérer les mots de passe directement. Il fournit souvent un jeton à la place du nom d'utilisateur et du mot de passe réels pouvant être utilisés pour effectuer des requêtes authentifiées auprès d'un service. Un exemple est lors de la demande d'un jeton OAuth2.
Parfois, toutes les informations requises sont déjà stockées sur le périphérique et parfois, le responsable du compte devra appeler un serveur pour obtenir un jeton actualisé. Vous avez peut-être vu le Comptes section dans les paramètres de votre appareil pour diverses applications. Nous pouvons obtenir cette liste de comptes disponibles comme ceci:
AccountManager accountManager = AccountManager.get (this); Compte [] comptes = accountManager.getAccounts ();
Le code nécessitera la android.permission.GET_ACCOUNTS
autorisation. Si vous recherchez un compte spécifique, vous pouvez le trouver comme ceci:
AccountManager accountManager = AccountManager.get (this); Compte [] comptes = accountManager.getAccountsByType ("com.google");
Une fois que vous avez le compte, un jeton pour le compte peut être récupéré en appelant le getAuthToken (Compte, Chaîne, Bundle, Activité, AccountManagerCallback, Gestionnaire)
méthode. Le jeton peut ensuite être utilisé pour faire des demandes d'API authentifiées à un service. Il peut s'agir d'une API RESTful dans laquelle vous transmettez un paramètre de jeton lors d'une requête HTTPS, sans avoir à connaître les détails du compte privé de l'utilisateur..
Chaque service ayant une manière différente d'authentifier et de stocker les informations d'identification privées, le gestionnaire de comptes fournit des modules d'authentification destinés à être implémentés par un service tiers. Bien qu'Android ait des implémentations pour de nombreux services populaires, cela signifie que vous pouvez écrire votre propre authentificateur pour gérer l'authentification de compte de votre application et le stockage des informations d'identification. Cela vous permet de vous assurer que les informations d'identification sont cryptées. N'oubliez pas que cela signifie également que les informations d'identification utilisées dans le gestionnaire de comptes par d'autres services peuvent être stockées en texte clair, ce qui les rend visibles à quiconque a enraciné leur appareil..
Au lieu de simples identifiants, vous aurez parfois besoin de traiter une clé ou un certificat pour un individu ou une entité, par exemple lorsqu'un tiers vous envoie un fichier de certificat que vous devez conserver. Le scénario le plus courant est lorsqu'une application doit s'authentifier sur le serveur d'une organisation privée..
Dans le prochain didacticiel, nous examinerons l'utilisation de certificats pour l'authentification et les communications sécurisées, mais je souhaite tout de même indiquer comment stocker ces éléments entre-temps. L'API de trousseau a été créée à l'origine pour cette utilisation très spécifique: installation d'une clé privée ou d'une paire de certificats à partir d'un fichier PKCS # 12..
Introduit dans Android 4.0 (API niveau 14), l’API du trousseau traite de la gestion des clés. Plus précisément, cela fonctionne avec Clé privée
et X509Certificat
objets et fournit un conteneur plus sécurisé que d'utiliser le stockage de données de votre application. En effet, les autorisations pour les clés privées permettent uniquement à votre propre application d'accéder aux clés, et uniquement après autorisation de l'utilisateur. Cela signifie qu'un écran de verrouillage doit être configuré sur le périphérique avant de pouvoir utiliser le stockage des informations d'identification. En outre, les objets du trousseau peuvent être liés au matériel sécurisé, le cas échéant..
Le code pour installer un certificat est le suivant:
Intention Intention = KeyChain.createInstallIntent (); byte [] p12Bytes = //… lu dans un fichier, tel que example.pfx ou example.p12… intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (intention);
L'utilisateur sera invité à entrer un mot de passe pour accéder à la clé privée et une option pour nommer le certificat. Pour récupérer la clé, le code suivant présente une interface utilisateur qui permet à l'utilisateur de choisir dans la liste des clés installées..
KeyChain.choosePrivateKeyAlias (ceci, ceci, nouveau String [] "RSA", null, null, -1, null);
Une fois le choix effectué, un nom d’alias de chaîne est renvoyé dans la liste. alias (alias final de chaîne)
rappel où vous pouvez accéder directement à la clé privée ou à la chaîne de certificats.
classe publique KeychainTest s'étend Activité implémente…, KeyChainAliasCallback //… @Override alias de vide public (alias final String) Log.e ("MonApp", "Alias est" + alias); try PrivateKey privateKey = KeyChain.getPrivateKey (this, alias); X509Certificate [] certificateChain = KeyChain.getCertificateChain (this, alias); capture… //…
Forts de ces connaissances, voyons maintenant comment nous pouvons utiliser le stockage des identifiants pour sauvegarder vos propres données sensibles..
Dans le didacticiel précédent, nous avons examiné la protection des données via un code d'authentification fourni par l'utilisateur. Ce type de configuration est bon, mais les exigences des applications ne permettent souvent pas aux utilisateurs de se connecter à chaque fois et de se souvenir d'un mot de passe supplémentaire..
C'est là que l'API KeyStore peut être utilisée. Depuis l'API 1, le système a utilisé KeyStore pour stocker les informations d'identification WiFi et VPN. À partir de la version 4.3 (API 18), il vous permet de travailler avec vos propres clés asymétriques spécifiques à une application. Sous Android M (API 23), il peut stocker une clé symétrique AES. Ainsi, alors que l'API ne permet pas de stocker directement des chaînes sensibles, ces clés peuvent être stockées puis utilisées pour chiffrer des chaînes..
L’avantage de stocker une clé dans le magasin de clés est qu’il permet d’utiliser des clés sans révéler le contenu secret de cette clé; les données clés n'entrent pas dans l'espace d'application. N'oubliez pas que les clés sont protégées par des autorisations, de sorte que seule votre application puisse y accéder. De plus, elles peuvent également bénéficier d'une sauvegarde matérielle sécurisée si le périphérique en est capable. Cela crée un conteneur qui rend plus difficile l'extraction des clés d'un périphérique.
Pour cet exemple, au lieu de générer une clé AES à partir d'un code d'authentification fourni par l'utilisateur, nous pouvons générer automatiquement une clé aléatoire qui sera protégée dans le magasin de clés. Nous pouvons le faire en créant un KeyGenerator
par exemple, mis à "AndroidKeyStore"
fournisseur.
// Génère une clé et la stocke dans le KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); C’est le cas. l'écran de verrouillage est désactivé //.setUserAuthenticationValidityDurationSeconds(120) // disponible uniquement x secondes à partir de l'authentification par mot de passe. -1 requiert une empreinte digitale - à chaque fois .setRandomizedEncryptionRequired (true) // texte crypté différent pour le même texte en clair à chaque appel .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();
Les parties importantes à regarder ici sont les .setUserAuthenticationRequired (true)
et .setUserAuthenticationValidityDurationSeconds (120)
Caractéristiques. Celles-ci nécessitent la configuration d'un écran de verrouillage et le verrouillage de la clé jusqu'à l'authentification de l'utilisateur..
En regardant la documentation pour .setUserAuthenticationValidityDurationSeconds ()
, vous verrez que cela signifie que la clé n'est disponible que pendant un certain nombre de secondes à partir de l'authentification par mot de passe, et que -1
requiert une authentification par empreinte digitale chaque fois que vous souhaitez accéder à la clé. L'activation de l'exigence d'authentification a également pour effet de révoquer la clé lorsque l'utilisateur supprime ou modifie l'écran de verrouillage..
Comme stocker une clé non protégée avec les données cryptées revient à placer une clé maison sous le paillasson, ces options tentent de protéger la clé au repos en cas de compromission d'un périphérique. Un exemple pourrait être un vidage de données hors ligne du périphérique. Sans le mot de passe connu pour l'appareil, ces données sont rendues inutiles.
le .setRandomizedEncryptionRequired (true)
Cette option permet d’exiger qu’il y ait suffisamment de randomisation (un nouveau IV aléatoire à chaque fois), de sorte que si les mêmes données sont cryptées une seconde fois, la sortie cryptée sera toujours différente. Cela empêche un attaquant d'obtenir des informations sur le texte chiffré en se basant sur les mêmes données..
Une autre option à noter est setUserAuthenticationValidWhileOnBody (boolean resteValid)
, qui verrouille la clé une fois que l'appareil a détecté qu'il n'est plus sur la personne.
Maintenant que la clé est stockée dans le magasin de clés, nous pouvons créer une méthode qui chiffre les données à l’aide de la Chiffrer
objet, étant donné la Clef secrète
. Il va retourner un HashMap
contenant les données cryptées et un IV randomisé qui sera nécessaire pour décrypter les données. Les données cryptées, ainsi que le IV, peuvent ensuite être sauvegardés dans un fichier ou dans les préférences partagées..
HashMap privéeencrypt (octet final [] decryptedBytes) final HashMap map = new HashMap (); try // Récupère la clé finale KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey (); // Chiffrer les données finales Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); octet final [] ivBytes = cipher.getIV (); octet final [] encryptedBytes = cipher.doFinal (decryptedBytes); map.put ("iv", ivBytes); map.put ("crypté", cryptéBytes); catch (Throwable e) e.printStackTrace (); carte de retour;
Pour le déchiffrement, l'inverse est appliqué. le Chiffrer
l'objet est initialisé à l'aide du DECRYPT_MODE
constant et décrypté octet[]
tableau est retourné.
octet privé [] décrypter (HashMap finalmap) byte [] decryptedBytes = null; try // Récupère la clé finale KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey (); // Extraire les informations de la carte finale octet [] encryptedBytes = map.get ("encrypted"); octet final [] ivBytes = map.get ("iv"); // Décrypter les données final Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); spécification finale GCMParameterSpec = nouvelle GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal (encryptedBytes); catch (Throwable e) e.printStackTrace (); return decryptedBytes;
Nous pouvons maintenant tester notre exemple!
@TargetApi (Build.VERSION_CODES.M) private void testEncryption () try // Génère une clé et la stocke dans la clé finale KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); C’est le cas. l'écran de verrouillage est désactivé //.setUserAuthenticationValidityDurationSeconds(120) // disponible uniquement x secondes à partir de l'authentification par mot de passe. -1 requiert une empreinte digitale - à chaque fois .setRandomizedEncryptionRequired (true) // texte crypté différent pour le même texte en clair à chaque appel .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Test final HashMapmap = encrypt ("Ma chaîne très sensible!". getBytes ("UTF-8")); octet final [] decryptedBytes = décrypter (carte); final String decryptedString = new String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "La chaîne déchiffrée est" + decryptedString); catch (Throwable e) e.printStackTrace ();
C'est une bonne solution pour stocker des données pour les versions M et supérieures, mais que se passe-t-il si votre application prend en charge les versions antérieures? Les clés symétriques AES ne sont pas prises en charge sous M, les clés asymétriques RSA le sont. Cela signifie que nous pouvons utiliser les clés RSA et le cryptage pour accomplir la même chose..
La principale différence est qu’une paire de clés asymétrique contient deux clés, une clé privée et une clé publique, la clé publique chiffre les données et la clé privée les déchiffre. UNE KeyPairGeneratorSpec
est passé dans le KeyPairGenerator
qui est initialisé avec KEY_ALGORITHM_RSA
et le "AndroidKeyStore"
fournisseur.
private void testPreMEncryption () try // Génère une paire de clés et la stocke dans le magasin de clés KeyStore KeyStore = = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Calendrier début = Calendar.getInstance (); Fin du calendrier = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder (this) .setAlias ("MyKeyAlias") .setSubject (new X500Principal ("CN = MyKeyName, O = Autorité Android")) .setSerialNumber (new BigInteger (1024, nouveau Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // sur le niveau d'API 18, chiffré au repos, nécessite la configuration d'un écran de verrouillage, la modification d'un écran de verrouillage supprime la clé .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Octet final du test de chiffrement [] encryptedBytes = rsaEncrypt ("Ma chaîne secrète!". GetBytes ("UTF-8")); octet final [] decryptedBytes = rsaDecrypt (encryptedBytes); final String decryptedString = new String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "La chaîne déchiffrée est" + decryptedString); catch (Throwable e) e.printStackTrace ();
Pour chiffrer, nous obtenons le RSAPublicKey
de la paire de clés et l'utiliser avec le Chiffrer
objet.
byte public [] rsaEncrypt (octet final [] decryptedBytes) byte [] encryptedBytes = null; try final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate (). getPublicKey (); code de chiffrement final = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = nouveau ByteArrayOutputStream (); final CipherOutputStream cipherOutputStream = nouveau CipherOutputStream (outputStream, cipher); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptedBytes = outputStream.toByteArray (); catch (Throwable e) e.printStackTrace (); return encryptedBytes;
Le décryptage est effectué à l'aide du RSAPrivateKey
objet.
octet public [] rsaDecrypt (octet final [] encryptedBytes) byte [] decryptedBytes = null; try final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey (); code de chiffrement final = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privateKey); final CipherInputStream cipherInputStream = nouveau CipherInputStream (nouveau ByteArrayInputStream (encryptedBytes), cipher); liste de tableaux finalearrayList = new ArrayList <> (); int nextByte; while ((nextByte = cipherInputStream.read ())! = -1) arrayList.add ((byte) nextByte); decryptedBytes = new byte [arrayList.size ()]; pour (int i = 0; i < decryptedBytes.length; i++) decryptedBytes[i] = arrayList.get(i); catch (Throwable e) e.printStackTrace(); return decryptedBytes;
Une chose à propos de RSA est que le cryptage est plus lent que dans AES. Cela convient généralement pour de petites quantités d'informations, par exemple lorsque vous sécurisez des chaînes de préférences partagées. Si vous rencontrez un problème de performances lors du cryptage de grandes quantités de données, vous pouvez utiliser cet exemple à la place pour chiffrer et stocker uniquement une clé AES. Utilisez ensuite le cryptage AES plus rapide abordé dans le didacticiel précédent pour le reste de vos données. Vous pouvez générer une nouvelle clé AES et la convertir en une octet[]
tableau compatible avec cet exemple.
KeyGenerator keyGenerator = KeyGenerator.getInstance ("AES"); keyGenerator.init (256); // AES-256 SecretKey secretKey = keyGenerator.generateKey (); byte [] keyBytes = secretKey.getEncoded ();
Pour récupérer la clé à partir des octets, procédez comme suit:
SecretKey key = new SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");
C'était beaucoup de code! Pour que tous les exemples restent simples, j'ai omis de traiter minutieusement les exceptions. Mais rappelez-vous que pour votre code de production, il n'est pas recommandé de simplement capturer tous les Jetable
cas dans une déclaration de capture.
Ceci termine le tutoriel sur l'utilisation des informations d'identification et des clés. Une grande partie de la confusion entourant les clés et le stockage est liée à l'évolution du système d'exploitation Android, mais vous pouvez choisir la solution à utiliser en fonction du niveau d'API pris en charge par votre application..
Maintenant que nous avons couvert les meilleures pratiques pour la sécurisation des données au repos, le prochain tutoriel se concentrera sur la sécurisation des données en transit..