
Ich höre häufig Fragen wie "Wie implementiere ich die Authentifizierung in einer Android-App?", "Wo speichere ich eine PIN?", "Hey Mann, bin ich sicher, wenn ich eine Authentifizierungsfunktion auf diese Weise implementiere?" und viele davon. Ich wurde es sehr müde, diese Fragen zu beantworten, also habe ich beschlossen, alle meine Gedanken darüber einmal zu schreiben, um sie mit allen Fragestellern zu teilen.
Inhaltsverzeichnis
Authentifizierung: Warum muss ich das tun?
Beginnen wir mit der Definition. Authentifizierung (aus dem Griechischen: αὐθεντικός authentikos, "echt, echt", aus αὐθὐντης authentisch, "Autor") ist der Vorgang des Beweises einer Behauptung, beispielsweise der Identität eines Computersystembenutzers.
Wenn Ihre Anwendung vertrauliche Informationen enthält (Benutzerinformationen sind IMHO vertraulich), müssen Sie der App ein Authentifizierungsszenario hinzufügen, um den unbefugten Zugriff auf diese Informationen zu verhindern.
Die beliebtesten Authentifizierungsszenarien lauten wie folgt:
- Login + Passwort
- Master-Passwort
- PIN (4 oder mehr Ziffern)
- Biometrie
Die Anmeldung und Kennwortauthentifizierung erfolgt natürlich über ein Back-End in Ihrer Anwendung, und die Sicherheit dieses Mechanismus überlassen wir dem Back-End-Sicherheitsteam. Vergessen Sie nur nicht, Public Key Pinning zu implementieren.
Die Master-Passwort- Authentifizierung wird sehr selten und nur in Apps verwendet, die ein hohes Maß an Sicherheit erfordern (z. B. Passwort-Manager).
Wir haben also nur zwei der beliebtesten Szenarien: eine PIN und Biometrics . Sie sind sehr benutzerfreundlich und relativ einfach zu implementieren (tatsächlich sind sie nicht ...). In diesem Artikel werden die wichtigsten Aspekte der korrekten Implementierung dieser Funktionen behandelt.
Einfacher Weg
Stellen Sie sich vor, Sie sind ein Android-Entwickler und Ihr Code druckt Ihnen Geld. Sie brauchen sich um nichts zu kümmern und brauchen keine ernsthaften Kenntnisse über die Sicherheit von mobilen Apps. Aber eines Tages kommt ein Manager zu Ihnen und gibt die Aufgabe, "eine zusätzliche Authentifizierung über eine PIN und einen Fingerabdruck in unserer Anwendung zu implementieren". Die Geschichte beginnt hier ...
Um die PIN-Authentifizierung zu implementieren, erstellen Sie einige Bildschirme wie folgt:


Schreiben Sie einen solchen Code, um Ihre PIN zu erstellen und zu überprüfen
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 }
Das ist alles Jetzt haben Sie ein cooles Authentifizierungssystem über eine PIN. Herzlichen glückwunsch Es war so einfach, nicht wahr?
Natürlich haben Sie die Ironie in meinen Worten bereits erkannt. Dieser Weg ist fürchterlich schlecht, da eine PIN als Klartext gespeichert wird. Wenn Malware auf irgendeine Weise Zugriff auf den internen Anwendungsspeicher erhält, erhält sie die Benutzer-PIN wie sie ist. Sie können mich fragen "Warum ist es so schlimm? Es ist nur eine PIN von der lokalen Authentifizierung ...". Ja, aber Benutzer legen in der Regel überall dieselbe PIN fest. Daher ermöglicht die Kenntnis einer Benutzer-PIN einem Eindringling, die Angriffsfläche zu erweitern.
Darüber hinaus können Sie mit einem solchen Authentifizierungsschema die Verschlüsselung von Benutzerdaten auf der Grundlage einer PIN nicht sicher implementieren (wir werden später darauf eingehen).
Machen wir es besser
Wie können wir unsere bisherige Implementierung verbessern? Der erste und naheliegende Ansatz besteht darin, einen Hash von Ihrer PIN zu nehmen und diesen Hash zu speichern.
Eine Hash-Funktion ist eine Funktion, mit der Daten beliebiger Größe auf Werte fester Größe abgebildet werden können. Die von einer Hash-Funktion zurückgegebenen Werte heißen Hash-Werte, Hash-Codes, Digests oder einfach Hashes. Die Werte werden verwendet, um eine Tabelle mit fester Größe zu indizieren, die als Hash-Tabelle bezeichnet wird. Die Verwendung einer Hash-Funktion zum Indizieren einer Hash-Tabelle wird als Hash- oder Streuspeicheradressierung bezeichnet.
Es gibt viele verfügbare Hash-Funktionen im Android Framework (genauer gesagt in der Java Cryptography Architecture ), aber heute wird nicht jede von ihnen als sicher angesehen. Aufgrund von Kollisionen empfehle ich MD5 und SHA-1 nicht. SHA-256 ist eine gute Wahl für die meisten Aufgaben.
fun sha256(byteArray: ByteArray): ByteArray { val digest = try { MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { MessageDigest.getInstance("SHA") } return with(digest) { update(byteArray) digest() } }
Ändern wir unsere savePin(...)
-Methode, um die Hash-PIN zu speichern
fun savePin(pin: String) { val hashedPin = sha256(pin.toByteArray()) val encodedHash = Base64.encodeToString(hashedPin, Base64.DEFAULT) preferences.edit().putString(StorageKey.PIN, encodedHash).apply() }
Die Verwendung von Hash ist ein guter Anfang, aber nackter Hash reicht für unsere Aufgabe nicht aus. In der Realität hat ein Angreifer bereits alle vierstelligen PIN-Hashes vorberechnet . Er wird in der Lage sein, alle diese gestohlenen PINs ganz einfach zu entschlüsseln. Es gibt einen Ansatz, um damit umzugehen - ein Salz .
In der Kryptografie sind Salt zufällige Daten, die als zusätzliche Eingabe für eine Einwegfunktion verwendet werden, die Daten, ein Passwort oder eine Passphrase "hascht". Salze werden zum Schutz von Passwörtern im Speicher verwendet. In der Vergangenheit wurde ein Kennwort im Klartext auf einem System gespeichert. Im Laufe der Zeit wurden jedoch zusätzliche Schutzmaßnahmen entwickelt, um das Kennwort eines Benutzers vor dem Lesen aus dem System zu schützen. Ein Salz ist eine dieser Methoden.
Um unserem Sicherheitsmechanismus ein Salz hinzuzufügen, müssen wir den oben gezeigten Code so ändern
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() }
Beachten Sie, dass Sie das Salt zusammen mit der PIN speichern müssen, da Sie den resultierenden Hash (unter Verwendung von Salt) jedes Mal berechnen müssen, wenn Sie die PIN anhand von Benutzereingaben überprüfen.
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 }
Wie Sie sehen, ist der Code immer noch nicht so schwer zu verstehen, aber die Sicherheit dieser Lösung ist wesentlich höher. Ich sage noch mehr, dieser Ansatz ist für die meisten Anwendungen, die kein hohes Maß an Sicherheit erfordern, durchaus produktionsbereit.
"Aber was ist, wenn ich eine viel sicherere Lösung brauche?", Fragen Sie. Ok, folge mir.
Der richtige Weg
Lassen Sie uns einige Verbesserungspunkte für unseren Authentifizierungsansatz diskutieren.
Erstens ist der Hauptfehler von "gewöhnlichen Hashes" (und sogar "gesalzenen gewöhnlichen Hashes") die relativ hohe Geschwindigkeit eines Brute-Force-Angriffs (etwa Milliarden von Hashes pro Minute ). Um diesen Fehler zu beheben, müssen wir eine spezielle KDF-Funktion wie PBKDF2 verwenden, die nativ vom Android Framework unterstützt wird. Natürlich gibt es einen Unterschied zwischen den KDF-Funktionen und Sie werden wahrscheinlich die andere wählen wollen, aber dies ist nicht in diesem Artikelbereich. Ich werde Ihnen am Ende des Artikels einige nützliche Links zu diesem Thema geben.
Zweitens haben wir zu diesem Zeitpunkt keine Verschlüsselung der Benutzerdaten. Es gibt viele Möglichkeiten, dies umzusetzen, und ich zeige die einfachste und zuverlässigste. Es wird ein Satz von zwei Bibliotheken und ein Code um sie herum sein.
Lassen Sie uns zunächst einen PBKDF2-Schlüssel schreiben, der die Factory erstellt.
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) } }
Mit dieser Fabrik müssen wir nun unsere savePin()
und 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 }
So haben wir gerade den Hauptfehler unserer vorherigen Lösung gemildert. Es ist gut und jetzt müssen wir die Verschlüsselung der Benutzerdaten hinzufügen. Um es zu implementieren, nehmen wir diese Bibliotheken:
- Tink - Eine mehrsprachige, plattformübergreifende Bibliothek, die kryptografische APIs bereitstellt, die sicher, einfach zu verwenden und schwer zu missbrauchen sind.
- Jetpack-Sicherheit - Lesen und schreiben Sie verschlüsselte Dateien und freigegebene Einstellungen, indem Sie die bewährten Sicherheitsmethoden befolgen.
Um einen guten verschlüsselten Speicher zu erhalten, müssen wir folgenden Code schreiben:
class App : Application() { ... val encryptedStorage by lazy { EncryptedSharedPreferences.create( "main_storage", "main_storage_key", this, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) } ... }
Das ist alles Später können wir damit arbeiten, als ob es normale SharedPreferences
, aber alle Daten werden verschlüsselt. Jetzt können wir die vorherige Implementierung problemlos ersetzen.
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 } ... }
Lassen Sie uns die Zwischensumme zusammenfassen. Wir haben einen ziemlich sicheren Schlüssel, der von einer PIN abgeleitet ist, und einen ziemlich zuverlässigen Ansatz, um ihn zu speichern. Das sieht cool aus, aber nicht genug. Was ist, wenn wir davon ausgehen, dass der Angreifer Zugriff auf unser Gerät hat und die gesamten Daten daraus extrahiert hat? Theoretisch verfügt er zu diesem Zeitpunkt über alle Komponenten, um die Daten zu entschlüsseln. Um dieses Problem zu lösen, müssen wir zwei Dinge erreichen:
- Eine PIN wird überhaupt nicht gespeichert
- Verschlüsselungsvorgänge basieren auf der PIN
Wie können wir diese Ziele erreichen, ohne den gesamten Code neu zu schreiben? Es ist ganz einfach! Sofern wir Tink verwenden, können wir die Verschlüsselungsfunktion anwenden, die als zugehörige Daten bezeichnet wird.
Zugehörige Daten, die authentifiziert, aber nicht verschlüsselt werden sollen. Zugehörige Daten sind optional, daher kann dieser Parameter null sein. In diesem Fall entspricht der Nullwert einem leeren Byte-Array (Länge Null). Für eine erfolgreiche Entschlüsselung müssen die gleichen zugehörigen Daten zusammen mit dem Chiffretext bereitgestellt werden.
Das war's Wir können eine PIN als zugehörige Daten verwenden, um unsere festgelegten Ziele zu erreichen. Die Möglichkeit oder Unmöglichkeit, die Benutzerdaten zu entschlüsseln, ist somit ein Indikator für die Richtigkeit der PIN. Dieses Schema funktioniert normalerweise wie folgt:

Wenn ein Benutzer eine falsche PIN eingibt, erhalten Sie GeneralSecurityException, wenn Sie versuchen, das Zugriffstoken zu entschlüsseln. Die endgültige Implementierung könnte also so aussehen:
Zeigen Sie den 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 } }
Nettes Ergebnis! Jetzt speichern wir die PIN nicht mehr und alle Daten werden standardmäßig verschlüsselt. Natürlich gibt es viele Möglichkeiten, diese Implementierung zu verbessern, wenn Sie möchten. Ich habe gerade das Grundprinzip gezeigt.
Aber warte, was ist mit Biometrie?
Ich glaube nicht, dass es bei "Biometrie" um Sicherheit geht. Ich würde es lieber "eine sehr praktische Benutzerfunktion" nennen. Und es ist ein schrecklich alter heiliger Krieg zwischen Bequemlichkeit und Sicherheit. Die meisten Benutzer mögen diese Art der Authentifizierung, und wir als Entwickler müssen sie so sicher wie möglich implementieren.
Leider ist die Implementierung der biometrischen Authentifizierung recht schwierig. Deshalb werde ich Ihnen zunächst einige allgemeine Implementierungsprinzipien zeigen und einige Erklärungen geben. Danach werden wir tief in den Code eintauchen.

Dieses Schema enthält eine wichtige Nuance: Der geheime Schlüssel wird auf der Festplatte gespeichert . Natürlich nicht im Klartext, aber trotzdem.
Wie Sie sehen, haben wir im Schlüsselspeicher einen neuen Verschlüsselungsschlüssel erstellt, mit dem wir unseren geheimen Schlüssel verschlüsseln, der von einer PIN abgeleitet ist. Ein solches Schema ermöglicht es uns, beim Ändern einer Authentifizierungsmethode nicht alle Daten neu zu verschlüsseln. Darüber hinaus haben wir weiterhin die Möglichkeit, eine PIN einzugeben, wenn die biometrische Authentifizierung aus irgendeinem Grund fehlgeschlagen ist. Ok, lass uns viel Code schreiben.
Zunächst zeige ich die Änderungen im Ablauf der PIN-Erstellung:
Zeigen Sie den 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) } } }
Ich würde mich freuen, wenn Google Tink in Biometrics einbinden würde, aber ... Wir müssen diesen Code mit Cipher und KeyStore schreiben. Dieser Code ist den Leuten, die mit Kryptografie in Android arbeiten, recht vertraut, aber ich möchte Ihre Aufmerksamkeit auf Verschlüsselungs-Paddings lenken. Ja, um Padding-Oracle-Angriffe zu verhindern , verwenden wir überhaupt kein Padding. Auf diese Weise verringern wir Risiken beim Speichern des geheimen Schlüssels auf der Festplatte.
Der Code für die biometrische Überprüfung ist sehr ähnlich:
Zeigen Sie den 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)) } } }
Achten Sie auf die authenticationCallback.onAuthenticationSucceeded
, die die Schlüssellogik der postbiometrischen Authentifizierung enthält. Tatsächlich ist dies eine alternative Implementierung der Methode pinIsValid()
. Wenn Sie nicht genau wissen, was in den beiden vorherigen Codeblöcken passiert, lesen Sie bitte die offizielle biometrische Dokumentation .
Bin ich vollständig geschützt?
Wir haben eine Menge cooler Dinge getan, um die Authentifizierung mit PIN und Biometrie zu realisieren. Aber ist sie so zuverlässig und sicher? Natürlich haben wir unser Bestes gegeben, aber es gibt ein paar Punkte zu berücksichtigen.
Eine klassische PIN hat nur vier Ziffern und die Entropie ist zu niedrig. Eine solche Art von Code ist also nicht ganz sicher zu verwenden. Trotz allem, was wir getan haben, besteht die Möglichkeit, dass ein Eindringling diesen Code knacken kann. Ja, er muss das Reverse Engineering Ihrer Anwendung durchführen und verstehen, wie Sie Benutzerdaten verschlüsseln, aber trotzdem. Wenn ein Angreifer motiviert genug ist, wird er es ohne zu zögern tun.
Der zweite Punkt betrifft verwurzelte Smartphones. Wenn es um gerootete Geräte geht, können Sie alle Ihre Sicherheitsversuche verwerfen. Jede Malware mit Root-Zugriff kann alle Sicherheitsmechanismen umgehen. Daher müssen Sie der Anwendung zusätzliche Sicherheitsfunktionen und -prüfungen hinzufügen. Ich schlage Ihnen zwei einfache Möglichkeiten vor, um diese Mängel zu beheben:
- SafetyNet - Es bietet eine Reihe von Diensten und APIs, mit denen Sie Ihre App vor Sicherheitsbedrohungen schützen können, darunter Gerätemanipulationen, ungültige URLs, potenziell schädliche Apps und falsche Benutzer
- Verschleierung - Bitte beachten Sie, dass ProGuard kein Verschleierungswerkzeug ist! Bei ProGuard geht es um Minimierung und Ressourcenverringerung, nicht um Verschleierung oder Sicherheit. Verwenden Sie so etwas wie DexGuard, DexProtector usw.
Die Verwendung von SafetyNet und die Verschleierung sind ein guter nächster Schritt, nachdem Sie die Ansätze aus diesem Artikel angewendet haben. Wenn Sie Ungenauigkeiten, Sicherheitsmängel oder andere Fehler bemerken, lassen Sie es mich bitte wissen. Sie finden den gesamten Code aus dem Artikel auf GitHub .
Und beim nächsten Mal werde ich Ihnen zeigen, wie Sie eine PIN-Authentifizierung mithilfe des Backends implementieren. Bleib dran.
Nützliche Links