Authentifiez-moi. Si tu peux ...


J'entends souvent des questions comme "Comment implémenter l'authentification dans une application Android?", "Où stocker un code PIN?", "Hé mec, serai-je en sécurité si j'implémente une fonctionnalité d'authentification de cette manière?" et beaucoup du genre. Je me suis vraiment fatigué de répondre à ces questions, alors j'ai décidé d'écrire toutes mes pensées à ce sujet une fois pour les partager avec tous les intervenants.


Table des matières




Authentification: pourquoi dois-je le faire?


Commençons par la définition. L'authentification (du grec: αὐθεντικός authentikos, "réel, authentique", de αὐθέντης authentes, "auteur") est l'acte de prouver une assertion, telle que l'identité d'un utilisateur de système informatique.


Donc, si votre application contient des informations sensibles (les informations de tout utilisateur sont sensibles à mon humble avis), vous devez ajouter un scénario d'authentification à l'application pour empêcher tout accès non autorisé à ces informations.


Les scénarios d'authentification les plus courants sont les suivants:


  • Identifiant + mot de passe
  • Mot de passe principal
  • PIN (4 chiffres ou plus)
  • Biométrie

Naturellement, l'authentification de connexion et de mot de passe vient de votre application depuis un back-end et la sécurité de ce mécanisme nous laisserons à l'équipe d'assurance de la sécurité du back-end;) N'oubliez pas d'implémenter l'épinglage de clé publique .


L' authentification par mot de passe principal est très rarement utilisée et uniquement dans les applications qui nécessitent un haut niveau de sécurité (par exemple, les gestionnaires de mots de passe).


Ainsi, nous n'avons que deux scénarios les plus populaires: un code PIN et la biométrie . Ils sont assez conviviaux et relativement faciles à mettre en œuvre (en fait ils ne le sont pas ...). Dans cet article, nous couvrirons les principaux aspects de la mise en œuvre correcte de ces fonctionnalités.



Manière simple


Imaginez, vous êtes un développeur Android et votre code vous fait gagner de l'argent. Vous ne vous inquiétez de rien et vous n'avez pas vraiment besoin d'une expertise sérieuse en matière de sécurité des applications mobiles. Mais un jour, un manager vient vous voir et vous donne pour tâche de "mettre en place une authentification supplémentaire via un code PIN et une empreinte digitale dans notre application". L'histoire commence ici ...


Pour implémenter l'authentification PIN, vous devez créer quelques écrans comme ceux-ci:


Et écrivez ce code pour créer et vérifier votre code PIN


fun savePin(pin: String) { preferences.edit().putString(StorageKey.PIN, pin).apply() } 

 fun authenticate(pin: String) { authenticationState.value = if (pinIsValid(pin)) { AuthenticationState.AUTHENTICATED } else { AuthenticationState.INVALID_AUTHENTICATION } } private fun pinIsValid(pin: String): Boolean { return preferences.getString(StorageKey.PIN, null) == pin } 

C'est tout! Maintenant, vous avez un système d'authentification cool via un code PIN. Félicitations. C'était si facile, non?


Bien sûr, vous avez déjà saisi l'ironie de mes mots. Cette façon est terriblement mauvaise, car un code PIN est stocké en texte brut. Si un malware accède d'une manière ou d'une autre au stockage interne de l'application, il obtiendra le code PIN de l'utilisateur tel quel. Vous pouvez me demander "Pourquoi est-ce si mauvais? C'est juste un code PIN d'authentification locale ...". Oui, mais les utilisateurs ont tendance à définir le même code PIN partout. Par conséquent, la connaissance d'un code PIN utilisateur permet à un intrus d'élargir la surface d'attaque.


De plus, un tel schéma d'authentification ne vous permet pas d'implémenter le cryptage des données utilisateur basé sur un code PIN de manière sécurisée (nous en parlerons plus tard).



Faisons mieux


Comment pouvons-nous améliorer notre mise en œuvre précédente? La première approche évidente consiste à extraire un hachage de votre code PIN et à le stocker.


Une fonction de hachage est une fonction qui peut être utilisée pour mapper des données de taille arbitraire à des valeurs de taille fixe. Les valeurs renvoyées par une fonction de hachage sont appelées valeurs de hachage, codes de hachage, résumés ou simplement hachages. Les valeurs sont utilisées pour indexer une table de taille fixe appelée table de hachage. L'utilisation d'une fonction de hachage pour indexer une table de hachage est appelée adressage de stockage de hachage ou de dispersion.

Il existe de nombreuses fonctions de hachage disponibles dans Android Framework (dans Java Cryptography Architecture , pour être précis), mais aujourd'hui, chacune n'est pas considérée comme sécurisée. Je ne recommande pas d'utiliser MD5 et SHA-1 en raison de collisions. SHA-256 est un bon choix pour la plupart des tâches.


 fun sha256(byteArray: ByteArray): ByteArray { val digest = try { MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { MessageDigest.getInstance("SHA") } return with(digest) { update(byteArray) digest() } } 

savePin(...) notre savePin(...) pour stocker le code PIN haché


 fun savePin(pin: String) { val hashedPin = sha256(pin.toByteArray()) val encodedHash = Base64.encodeToString(hashedPin, Base64.DEFAULT) preferences.edit().putString(StorageKey.PIN, encodedHash).apply() } 

L'utilisation du hachage est un bon début, mais le hachage nu n'est pas suffisant pour notre tâche. Dans la vie réelle, un attaquant a déjà pré-calculé tous les hachages PIN à 4 chiffres. Il sera en mesure de déchiffrer assez facilement tous ces NIP hachés volés. Il existe une approche pour y faire face - un sel .


En cryptographie, un sel est des données aléatoires qui sont utilisées comme entrée supplémentaire pour une fonction unidirectionnelle qui «hache» les données, un mot de passe ou une phrase secrète. Les sels sont utilisés pour protéger les mots de passe dans le stockage. Historiquement, un mot de passe était stocké en texte brut sur un système, mais avec le temps, des protections supplémentaires ont été développées pour protéger le mot de passe d'un utilisateur contre la lecture du système. Un sel est l'une de ces méthodes.

Pour ajouter un sel à notre mécanisme de sécurité, nous devons changer le code ci-dessus de cette manière


 fun generate(lengthByte: Int = 32): ByteArray { val random = SecureRandom() val salt = ByteArray(lengthByte) random.nextBytes(salt) return salt } 

 fun savePin(pin: String) { val salt = Salt.generate() val saltedPin = pin.toByteArray() + salt val hashedPin = Sha256.hash(saltedPin) val encodedHash = Base64.encodeToString(hashedPin, Base64.DEFAULT) val encodedSalt = Base64.encodeToString(salt, Base64.DEFAULT) preferences.edit() .putString(StorageKey.PIN, encodedHash) .putString(StorageKey.SALT, encodedSalt) .apply() } 

Remarque, vous devez stocker le sel avec le code PIN car vous devez calculer le hachage résultant (en utilisant du sel) à chaque fois lors de la vérification du code PIN à partir de la saisie utilisateur.


 private fun pinIsValid(pin: String): Boolean { val encodedSalt = preferences.getString(StorageKey.SALT, null) val encodedHashedPin = preferences.getString(StorageKey.PIN, null) val salt = Base64.decode(encodedSalt, Base64.DEFAULT) val storedHashedPin = Base64.decode(encodedHashedPin, Base64.DEFAULT) val enteredHashedPin = Sha256.hash(pin.toByteArray() + salt) return storedHashedPin contentEquals enteredHashedPin } 

Comme vous pouvez le voir, le code n'est toujours pas si difficile à comprendre, mais la sécurité de cette solution est devenue beaucoup plus forte. Je dirai même plus, cette approche est tout à fait prête à la production pour la plupart des applications qui ne nécessitent pas un haut niveau de sécurité.


"Mais si j'ai besoin d'une solution beaucoup plus sûre?", Demandez-vous. Ok, suivez-moi.



La bonne façon


Discutons de plusieurs points d'amélioration de notre approche d'authentification.


Premièrement, le principal défaut des "hachages ordinaires" (et même des "hachages ordinaires salés") est la vitesse relativement élevée d'une attaque par force brute (environ des milliards de hachages par minute ). Pour éliminer cette faille, nous devons utiliser une fonction KDF spéciale comme PBKDF2 qui est nativement prise en charge par Android Framework. Bien sûr, il existe une certaine différence entre les fonctions KDF et vous voudrez probablement choisir l'autre, mais cela sort du cadre de cet article. Je vais vous donner plusieurs liens utiles sur ce sujet à la fin de l'article.


Deuxièmement, nous n'avons pas de cryptage des données utilisateur à ce stade. Il existe de nombreuses façons de l'implémenter et je vais vous montrer la plus simple et la plus fiable. Ce sera un ensemble de deux bibliothèques et du code autour d'eux.


Écrivons une usine de création de clés PBKDF2 pour commencer.


 object Pbkdf2Factory { private const val DEFAULT_ITERATIONS = 10000 private const val DEFAULT_KEY_LENGTH = 256 private val secretKeyFactory by lazy { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { SecretKeyFactory.getInstance("PBKDF2withHmacSHA1") } else { SecretKeyFactory.getInstance("PBKDF2withHmacSHA256") } } fun createKey( passphraseOrPin: CharArray, salt: ByteArray, iterations: Int = DEFAULT_ITERATIONS, outputKeyLength: Int = DEFAULT_KEY_LENGTH ): SecretKey { val keySpec = PBEKeySpec(passphraseOrPin, salt, iterations, outputKeyLength) return secretKeyFactory.generateSecret(keySpec) } } 

Maintenant armé de cette usine, nous devons refactoriser nos savePin() et pinIsValid()


 fun savePin(pin: String) { val salt = Salt.generate() val secretKey = Pbkdf2Factory.createKey(pin.toCharArray(), salt) val encodedKey = Base64.encodeToString(secretKey.encoded, Base64.DEFAULT) val encodedSalt = Base64.encodeToString(salt, Base64.DEFAULT) preferences.edit() .putString(StorageKey.KEY, encodedKey) .putString(StorageKey.SALT, encodedSalt) .apply() pinIsCreated.value = true } 

 private fun pinIsValid(pin: String): Boolean { val encodedSalt = preferences.getString(StorageKey.SALT, null) val encodedKey = preferences.getString(StorageKey.KEY, null) val salt = Base64.decode(encodedSalt, Base64.DEFAULT) val storedKey = Base64.decode(encodedKey, Base64.DEFAULT) val enteredKey = Pbkdf2Factory.createKey(pin.toCharArray(), salt) return storedKey contentEquals enteredKey.encoded } 

Ainsi, nous venons d'atténuer le principal défaut de notre solution précédente. C'est bien, et maintenant nous devons ajouter le cryptage des données utilisateur. Pour l'implémenter, nous prendrons ces bibliothèques:


  • Tink - Une bibliothèque multi-plateforme multi-langue qui fournit des API cryptographiques qui sont sécurisées, faciles à utiliser correctement et difficiles (euh) à abuser.
  • Jetpack Security - Lisez et écrivez des fichiers chiffrés et des préférences partagées en suivant les meilleures pratiques de sécurité.

Pour obtenir un bon stockage crypté, nous devons écrire un tel code:


 class App : Application() { ... val encryptedStorage by lazy { EncryptedSharedPreferences.create( "main_storage", "main_storage_key", this, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) } ... } 

C'est tout. Plus tard, nous pouvons travailler avec lui comme s'il s'agissait de SharedPreferences régulières, mais toutes les données seront cryptées. Maintenant, nous pouvons facilement remplacer l'implémentation précédente.


 class CreatePinViewModel(application: Application) : AndroidViewModel(application) { ... private val preferences by lazy { getApplication<App>().encryptedStorage } ... } 

 class InputPinViewModel(application: Application) : AndroidViewModel(application) { ... private val preferences by lazy { getApplication<App>().encryptedStorage } ... } 

Résumons le sous-total. Nous avons une clé assez sécurisée dérivée d'un code PIN et une approche assez fiable pour la stocker. Cela a l'air cool, mais pas suffisant. Et si nous supposons que l'attaquant a eu accès à notre appareil et en a extrait toutes les données. En théorie, il a tous les composants pour décrypter les données en ce moment. Pour résoudre ce problème, nous devons réaliser deux choses:


  • un code PIN n'est pas stocké du tout
  • les opérations de chiffrement sont basées sur le code PIN

Comment pouvons-nous atteindre ces objectifs sans réécrire tout le code? C'est facile! Dans la mesure où nous utilisons Tink, nous pouvons appliquer sa fonction de cryptage nommée en tant que données associées.


Données associées à authentifier, mais non chiffrées. Les données associées sont facultatives, ce paramètre peut donc être nul. Dans ce cas, la valeur nulle équivaut à un tableau d'octets vide (de longueur nulle). Pour un décryptage réussi, les mêmes données associées doivent être fournies avec le texte chiffré.

Voilà! Nous pouvons utiliser un code PIN comme données associées pour atteindre nos objectifs désignés. Ainsi, la possibilité ou l'impossibilité de décrypter les données utilisateur agira comme un indicateur de l'exactitude du code PIN. Ce schéma fonctionne généralement comme suit:



Si un utilisateur entre un code PIN incorrect, vous recevrez GeneralSecurityException en essayant de décrypter le jeton d'accès. Ainsi, l'implémentation finale pourrait ressembler à ceci:


Afficher le code
 class CreatePinViewModel(application: Application): AndroidViewModel(application) { ... private val fakeAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXQiOiJXZSdyZSBoaXJpbmcgOykifQ.WZrEWG-l3VsJzJrbnjn2BIYO68gHIGyat6jrw7Iu-Rw" private val preferences by lazy { getApplication<App>().encryptedStorage } private val aead by lazy { getApplication<App>().pinSecuredAead } ... fun savePin(pin: String) { val salt = Salt.generate() val secretKey = Pbkdf2Factory.createKey(pin.toCharArray(), salt) val encryptedToken = aead.encrypt( fakeAccessToken.toByteArray(), secretKey.encoded ) preferences.edit { putString(StorageKey.TOKEN, Base64.encodeToString( encryptedToken, Base64.DEFAULT )) putString(StorageKey.SALT, Base64.encodeToString(salt, Base64.DEFAULT)) putBoolean(StorageKey.PIN_IS_ENABLED, true) } ... } } 

 class InputPinViewModel(application: Application) : AndroidViewModel(application) { ... private val preferences by lazy { getApplication<App>().encryptedStorage } private val aead by lazy { getApplication<App>().pinSecuredAead } fun authenticate(pin: String) { authenticationState.value = if (pinIsValid(pin)) { AuthenticationState.AUTHENTICATED } else { AuthenticationState.INVALID_AUTHENTICATION } } private fun pinIsValid(pin: String): Boolean { val salt = Base64.decode( preferences.getString(StorageKey.SALT, null), Base64.DEFAULT ) val secretKey = Pbkdf2Factory.createKey(pin.toCharArray(), salt) val token = try { val encryptedToken = Base64.decode( preferences.getString(StorageKey.TOKEN, null), Base64.DEFAULT ) aead.decrypt(encryptedToken, secretKey.encoded) } catch (e: GeneralSecurityException) { null } return token?.isNotEmpty() ?: false } } 

Beau résultat! Maintenant, nous ne stockons plus le code PIN, et toutes les données sont cryptées par défaut. Bien sûr, il existe de nombreuses façons d'améliorer cette implémentation si vous le souhaitez. Je viens de montrer le principe de base.



Mais attendez, qu'en est-il de la biométrie?


Je ne pense pas que la "biométrie" soit une question de sécurité. Je préfère le nommer "une fonctionnalité utilisateur très pratique". Et c'est une guerre sainte terriblement ancienne entre commodité et sécurité. Mais la plupart des utilisateurs aiment ce type d'authentification et nous, en tant que développeurs, devons l'implémenter de la manière la plus sécurisée possible.


Malheureusement, la mise en œuvre de l'authentification biométrique est assez délicate. C'est pourquoi je vais commencer par vous montrer un principe d'implémentation commun et donner quelques explications. Après cela, nous plongerons profondément dans le code.



Ce schéma contient une nuance importante: la clé secrète est enregistrée sur le disque . Bien sûr, pas sous forme de texte brut, mais néanmoins.


Comme vous pouvez le voir, nous avons créé une nouvelle clé de cryptage dans le magasin de clés et nous utilisons cette clé pour crypter notre clé secrète dérivée d'un code PIN. Un tel schéma nous permet de ne pas rechiffrer toutes les données lors du changement d'une méthode d'authentification. De plus, nous avons toujours la possibilité de saisir un code PIN si l'authentification biométrique a échoué pour une raison quelconque. Ok, écrivons beaucoup de code.


Tout d'abord, je vais montrer les changements dans le flux de création de code PIN:


Afficher le code
 class CreatePinViewModel(application: Application): AndroidViewModel(application) { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" private const val KEY_NAME = "biometric_key" } ... val biometricEnableDialog = MutableLiveData<SingleLiveEvent<Unit>>() val biometricParams = MutableLiveData<BiometricParams>() val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) } override fun onAuthenticationSucceeded(result: AuthenticationResult) { super.onAuthenticationSucceeded(result) val encryptedSecretKey = result.cryptoObject?.cipher?.doFinal( secretKey.encoded ) preferences.edit { putString(StorageKey.KEY, Base64.encodeToString( encryptedSecretKey, Base64.DEFAULT )) } pinIsCreated.postValue(true) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() } } ... private val biometricManager by lazy { getApplication<App>().biometricManager } private val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) private lateinit var secretKey: SecretKey ... fun enableBiometric(isEnabled: Boolean) { generateKey() val cipher = createCipher().also { preferences.edit { putString(StorageKey.KEY_IV, Base64.encodeToString(it.iv, Base64.DEFAULT)) } } val promptInfo = createPromptInfo() val cryptoObject = BiometricPrompt.CryptoObject(cipher) if (isEnabled) { biometricParams.value = BiometricParams(isEnabled, promptInfo, cryptoObject) } else { pinIsCreated.value = true } } private fun createPromptInfo(): BiometricPrompt.PromptInfo { return BiometricPrompt.PromptInfo.Builder() .setTitle("Create biometric authorization") .setSubtitle("Touch your biometric sensor") .setNegativeButtonText("Cancel") .build() } private fun generateKey() { try { keyStore.load(null) val keyProperties = PURPOSE_ENCRYPT or PURPOSE_DECRYPT val builder = KeyGenParameterSpec.Builder(KEY_NAME, keyProperties) .setBlockModes(BLOCK_MODE_CBC) .setUserAuthenticationRequired(true) .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) val keyGenerator = KeyGenerator.getInstance( KEY_ALGORITHM_AES, ANDROID_KEY_STORE ) keyGenerator.run { init(builder.build()) generateKey() } } catch (e: Exception) { authenticationCallback.onAuthenticationError( BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL, e.localizedMessage ) } } private fun createCipher(): Cipher { val key = with(keyStore) { load(null) getKey(KEY_NAME, null) } return Cipher.getInstance( "$KEY_ALGORITHM_AES/$BLOCK_MODE_CBC/$ENCRYPTION_PADDING_NONE" ).apply { init(Cipher.ENCRYPT_MODE, key) } } } 

Je serais heureux si Google incluait Tink dans la biométrie, mais ... Nous devons écrire ce code standard avec Cipher et KeyStore. Ce code est assez familier aux personnes qui travaillent avec la cryptographie dans Android, mais je veux prêter votre attention aux rembourrages de cryptage. Oui, pour empêcher l' attaque par Padding Oracle, nous n'utilisons pas du tout de padding. Ainsi, nous atténuons les risques lors du stockage de la clé secrète sur le disque.


Le code de vérification biométrique est très similaire:


Afficher le code
 class InputPinViewModel(application: Application) : AndroidViewModel(application) { companion object { private const val ANDROID_KEY_STORE = "AndroidKeyStore" private const val KEY_NAME = "biometric_key" } ... val biometricErrorMessage = MutableLiveData<SingleLiveEvent<String>>() val biometricParams = MutableLiveData<BiometricParams>() ... private val biometricManager by lazy { getApplication<App>().biometricManager } private val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) } override fun onAuthenticationSucceeded(result: AuthenticationResult) { super.onAuthenticationSucceeded(result) val encryptedSecretKey = Base64.decode( preferences.getString(StorageKey.KEY, ""), Base64.DEFAULT ) val secretKey = result.cryptoObject?.cipher?.doFinal(encryptedSecretKey) val token = try { val encryptedToken = Base64.decode( preferences.getString(StorageKey.TOKEN, null), Base64.DEFAULT ) aead.decrypt(encryptedToken, secretKey) } catch (e: GeneralSecurityException) { null } val state = if (token?.isNotEmpty() == true) { AuthenticationState.AUTHENTICATED } else { AuthenticationState.INVALID_AUTHENTICATION } authenticationState.postValue(state) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() } } ... fun biometricAuthenticate() { if (preferences.contains(StorageKey.KEY)) { when (biometricManager.canAuthenticate()) { BiometricManager.BIOMETRIC_SUCCESS -> { val promptInfo = createPromptInfo() val cryptoObject = BiometricPrompt.CryptoObject(createCipher()) biometricParams.value = BiometricParams(promptInfo, cryptoObject) } } } else { biometricErrorMessage.value = SingleLiveEvent( "Biometric authentication isn't configured" ) } } ... private fun createPromptInfo(): BiometricPrompt.PromptInfo { return BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") .setNegativeButtonText("Cancel") .build() } private fun createCipher(): Cipher { val key = with(keyStore) { load(null) getKey(KEY_NAME, null) } return Cipher.getInstance( "$KEY_ALGORITHM_AES/$BLOCK_MODE_CBC/$ENCRYPTION_PADDING_NONE" ).apply { val iv = Base64.decode( preferences.getString(StorageKey.KEY_IV, null), Base64.DEFAULT ) init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) } } } 

Faites attention à l' authenticationCallback.onAuthenticationSucceeded , elle contient la logique clé de l'authentification post-biométrique. En fait, il s'agit d'une implémentation alternative de la méthode pinIsValid() . Si vous ne comprenez pas bien ce qui se passe dans les deux blocs de code précédents, veuillez vous référer à la documentation officielle biométrique .



Suis-je complètement protégé?


Nous avons fait beaucoup de choses intéressantes pour réaliser l'authentification avec un code PIN et la biométrie, mais est-ce si fiable et sécurisé? Bien sûr, nous avons fait de notre mieux, mais il y a quelques points à prendre en compte.


Un code PIN classique ne comporte que quatre chiffres et son entropie est trop faible. Donc, ce type de code n'est pas assez sûr à utiliser. Malgré tout ce que nous avons fait, il est possible qu'un intrus puisse déchiffrer ce code. Oui, il doit effectuer l'ingénierie inverse de votre application et comprendre comment vous cryptez les données des utilisateurs, mais néanmoins. Si un attaquant est suffisamment motivé, il le fera sans hésitation.


Le deuxième point concerne les smartphones enracinés. En ce qui concerne les appareils enracinés, vous pouvez jeter toutes vos tentatives d'assurance de sécurité. Tout malware avec un accès root peut contourner tous les mécanismes de sécurité. Par conséquent, vous devez ajouter des fonctionnalités de sécurité et des contrôles supplémentaires à l'application. Je vous suggère deux choses les plus simples pour atténuer ces défauts:


  • SafetyNet - il fournit un ensemble de services et d'API qui aident à protéger votre application contre les menaces de sécurité, y compris la falsification d'appareil, les URL incorrectes, les applications potentiellement dangereuses et les faux utilisateurs
  • Obfuscation - n'oubliez pas que ProGuard n'est pas un outil d'obfuscation! ProGuard concerne la réduction et la réduction des ressources, pas l'obscurcissement ou la sécurité. Utilisez quelque chose comme DexGuard, DexProtector, etc.

L'utilisation de SafetyNet et l'obfuscation sont une bonne étape suivante après avoir appliqué les approches de cet article. Si vous constatez des inexactitudes, des failles de sécurité ou d'autres conneries, faites-le moi savoir. Vous pouvez trouver tout le code de l'article sur GitHub .


Et la prochaine fois, je vous montrerai comment implémenter une authentification PIN à l'aide d'un back-end. Restez à l'écoute.



Source: https://habr.com/ru/post/fr475112/


All Articles