Schlüssel, Anmeldeinformationen und Speicherung auf Android

In dem vorherigen Beitrag zur Datensicherheit von Android-Benutzern haben wir uns mit der Verschlüsselung von Daten über einen vom Benutzer angegebenen Passcode befasst. In diesem Lernprogramm wird der Fokus auf die Speicherung von Anmeldeinformationen und Schlüsseln verschoben. Beginnen Sie mit der Einführung der Kontoanmeldeinformationen und enden Sie mit einem Beispiel für den Schutz von Daten mithilfe des KeyStore.

Bei der Arbeit mit einem Dienst eines Drittanbieters ist häufig eine Form der Authentifizierung erforderlich. Dies kann so einfach wie ein sein /Anmeldung Endpunkt, der einen Benutzernamen und ein Kennwort akzeptiert. 

Zunächst scheint es eine einfache Lösung zu sein, eine Benutzeroberfläche zu erstellen, die den Benutzer auffordert, sich anzumelden, und dann seine Anmeldeinformationen erfasst und speichert. Dies ist jedoch nicht die bewährte Methode, da unsere App die Anmeldeinformationen für ein Drittanbieter-Konto nicht kennen muss. Stattdessen können wir den Account Manager verwenden, der den Umgang mit diesen vertraulichen Informationen für uns delegiert.

Buchhalter

Der Account Manager ist ein zentraler Helfer für Anmeldeinformationen für Benutzerkonten, sodass Ihre App nicht direkt mit Kennwörtern umgehen muss. Anstelle des echten Benutzernamens und Kennworts wird häufig ein Token bereitgestellt, mit dem authentifizierte Anforderungen an einen Dienst gestellt werden können. Ein Beispiel ist das Anfordern eines OAuth2-Tokens. 

Manchmal sind alle erforderlichen Informationen bereits auf dem Gerät gespeichert. In anderen Fällen muss der Kontomanager einen Server anrufen, um ein aktualisiertes Token zu erhalten. Sie haben vielleicht gesehen Konten Abschnitt in den Einstellungen Ihres Geräts für verschiedene Apps. Wir können diese Liste der verfügbaren Konten folgendermaßen erhalten:

AccountManager accountManager = AccountManager.get (this); Konto [] Konten = KontoManager.getAccounts ();

Der Code erfordert die android.permission.GET_ACCOUNTS Genehmigung. Wenn Sie nach einem bestimmten Konto suchen, können Sie es wie folgt finden:

AccountManager accountManager = AccountManager.get (this); Konto [] Konten = KontoManager.getAccountsByType ("com.google");

Sobald Sie das Konto haben, können Sie ein Token für das Konto abrufen, indem Sie die getAuthToken (Account, String, Bundle, Aktivität, AccountManagerCallback, Handler) Methode. Das Token kann dann verwendet werden, um authentifizierte API-Anforderungen an einen Dienst zu stellen. Dies kann eine RESTful-API sein, bei der Sie während einer HTTPS-Anforderung einen Tokenparameter übergeben, ohne die privaten Kontodetails des Benutzers zu kennen.

Da für jeden Dienst die privaten Anmeldeinformationen auf unterschiedliche Weise authentifiziert und gespeichert werden können, stellt der Kontomanager Authentifizierungsmodule für die Implementierung eines Drittanbieterdiensts bereit. Android bietet zwar Implementierungen für viele beliebte Dienste, bedeutet aber, dass Sie Ihren eigenen Authentifikator schreiben können, um die Authentifizierung Ihrer App und die Speicherung der Anmeldeinformationen zu verwalten. Auf diese Weise können Sie sicherstellen, dass die Anmeldeinformationen verschlüsselt sind. Beachten Sie, dass dies auch bedeutet, dass Anmeldeinformationen im Account Manager, die von anderen Diensten verwendet werden, in Klartext gespeichert werden können, sodass sie für jeden sichtbar sind, der sein Gerät gerootet hat.

Anstelle von einfachen Anmeldeinformationen müssen Sie manchmal mit einem Schlüssel oder einem Zertifikat für eine Einzelperson oder Entität umgehen, z. B. wenn ein Dritter Ihnen eine Zertifikatsdatei sendet, die Sie aufbewahren müssen. Das häufigste Szenario ist, wenn sich eine App beim Server einer privaten Organisation authentifizieren muss. 

Im nächsten Lernprogramm werden Zertifikate für die Authentifizierung und sichere Kommunikation verwendet. Ich möchte jedoch noch darauf eingehen, wie diese Elemente in der Zwischenzeit gespeichert werden. Die Schlüsselbund-API wurde ursprünglich für diesen speziellen Zweck entwickelt, indem ein privater Schlüssel oder ein Zertifikatspaar aus einer PKCS # 12-Datei installiert wurde.

Der Schlüsselbund

Die in Android 4.0 (API Level 14) eingeführte Keychain-API befasst sich mit der Schlüsselverwaltung. Insbesondere funktioniert es mit Privat Schlüssel und X509Zertifikat Objekte und bietet einen sichereren Container als den Datenspeicher Ihrer App. Das liegt daran, dass Berechtigungen für private Schlüssel nur Ihrer eigenen App den Zugriff auf die Schlüssel ermöglichen, und dies nur nach einer Benutzerautorisierung. Das bedeutet, dass auf dem Gerät ein Sperrbildschirm eingerichtet werden muss, bevor Sie den Anmeldeinformationsspeicher verwenden können. Darüber hinaus sind die Objekte im Schlüsselbund möglicherweise an Hardware gebunden, sofern verfügbar. 

Der Code zum Installieren eines Zertifikats lautet wie folgt:

Absicht Absicht = KeyChain.createInstallIntent (); byte [] p12Bytes = //… aus Datei lesen, z. B. example.pfx oder example.p12… intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (Absicht);

Der Benutzer wird zur Eingabe eines Kennworts für den Zugriff auf den privaten Schlüssel und einer Option zum Benennen des Zertifikats aufgefordert. Um den Schlüssel abzurufen, zeigt der folgende Code eine Benutzeroberfläche, mit der der Benutzer aus der Liste der installierten Schlüssel auswählen kann.

KeyChain.choosePrivateKeyAlias ​​(dies, dieser neue String [] "RSA", null, null, -1, null);

Sobald die Auswahl getroffen wurde, wird ein String-Aliasname in der Liste zurückgegeben Alias ​​(letzter String-Alias) Rückruf, bei dem Sie direkt auf den privaten Schlüssel oder die Zertifikatkette zugreifen können.

public class KeychainTest erweitert Activity implementiert…, KeyChainAliasCallback //… @Override öffentlicher void-Alias ​​(abschließender String-Alias) Log.e ("MyApp", "Alias ​​ist" + Alias); try PrivateKey privateKey = KeyChain.getPrivateKey (dieser Alias); X509Certificate [] certificateChain = KeyChain.getCertificateChain (this, Alias);  Fang…  //… 

Mit diesem Wissen können wir nun sehen, wie wir den Speicher für Berechtigungsnachweise verwenden können, um Ihre eigenen sensiblen Daten zu speichern.

Der KeyStore

In der vorherigen Übung haben wir uns mit dem Schutz von Daten durch einen vom Benutzer angegebenen Passcode befasst. Diese Art der Einrichtung ist gut, aber die App-Anforderungen halten sich häufig davon ab, dass sich Benutzer jedes Mal anmelden und sich einen zusätzlichen Passcode merken. 

Hier kann die KeyStore-API verwendet werden. Seit API 1 wird der KeyStore vom System zum Speichern von WLAN- und VPN-Anmeldeinformationen verwendet. Ab Version 4.3 (API 18) können Sie mit Ihren eigenen app-spezifischen asymmetrischen Schlüsseln arbeiten. In Android M (API 23) kann ein symmetrischer AES-Schlüssel gespeichert werden. Während die API das direkte Speichern von sensiblen Zeichenfolgen nicht zulässt, können diese Schlüssel gespeichert und dann zum Verschlüsseln von Zeichenfolgen verwendet werden. 

Das Speichern eines Schlüssels im KeyStore bietet den Vorteil, dass Schlüssel bearbeitet werden können, ohne den geheimen Inhalt dieses Schlüssels preiszugeben. Schlüsseldaten gelangen nicht in den App-Raum. Beachten Sie, dass Schlüssel durch Berechtigungen geschützt sind, sodass nur Ihre App auf sie zugreifen kann. Außerdem können sie durch Hardware gesichert werden, wenn das Gerät in der Lage ist. Dadurch wird ein Container erstellt, der das Extrahieren von Schlüsseln aus einem Gerät erschwert. 

Erzeugen Sie einen neuen Zufallsschlüssel

In diesem Beispiel können Sie, anstatt einen AES-Schlüssel aus einem vom Benutzer angegebenen Passcode zu generieren, automatisch einen zufälligen Schlüssel generieren, der im KeyStore geschützt wird. Wir können dies tun, indem Sie eine Schlüsselgenerator Stellen Sie beispielsweise auf "AndroidKeyStore" Anbieter.

// Generiere einen Schlüssel und speichere ihn im KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ( "MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // erfordert Bildschirm sperren, für ungültig erklärt, wenn Sperrbildschirm ist deaktiviert //.setUserAuthenticationValidityDurationSeconds (120) // // Nur x Sekunden nach Kennwortauthentifizierung verfügbar. -1 erfordert Fingerabdruck - jedes Mal .setRandomizedEncryptionRequired (true) // Unterschiedlicher Geheimtext für denselben Klartext bei jedem Aufruf .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();

Wichtige Teile zum Anschauen sind die .setUserAuthenticationRequired (true) und .setUserAuthenticationValidityDurationSeconds (120) Spezifikationen. Dazu muss ein Sperrbildschirm eingerichtet und der Schlüssel gesperrt werden, bis sich der Benutzer authentifiziert hat. 

Betrachten Sie die Dokumentation für .setUserAuthenticationValidityDurationSeconds (), Sie werden feststellen, dass der Schlüssel nur eine bestimmte Anzahl von Sekunden nach der Kennwortauthentifizierung verfügbar ist und der Schlüssel übergeben wird -1 Bei jedem Zugriff auf den Schlüssel ist eine Authentifizierung per Fingerabdruck erforderlich. Das Aktivieren der Anforderung für die Authentifizierung bewirkt auch, dass der Schlüssel gesperrt wird, wenn der Benutzer den Sperrbildschirm entfernt oder ändert. 

Da das Speichern eines ungeschützten Schlüssels neben den verschlüsselten Daten wie das Einfügen eines Hausschlüssels unter die Türmatte ist, versuchen diese Optionen, den Schlüssel im Ruhezustand zu schützen, falls ein Gerät beschädigt wird. Ein Beispiel könnte ein Offline-Daten-Dump des Geräts sein. Wenn das Kennwort für das Gerät nicht bekannt ist, werden diese Daten unbrauchbar.

Das .setRandomizedEncryptionRequired (true) Die Option aktiviert die Anforderung, dass genügend Randomisierung vorhanden ist (jedes Mal eine neue Zufalls-IV), so dass die verschlüsselte Ausgabe, wenn dieselben Daten ein zweites Mal verschlüsselt werden, immer noch unterschiedlich ist. Dadurch wird verhindert, dass ein Angreifer anhand der Eingabe derselben Daten Hinweise auf den Geheimtext erhält. 

Eine weitere zu beachtende Option ist setUserAuthenticationValidWhileOnBody (boolean bleibtValid), Dadurch wird der Schlüssel gesperrt, sobald das Gerät erkannt hat, dass es sich nicht mehr auf der Person befindet.

Daten verschlüsseln

Nun, da der Schlüssel im KeyStore gespeichert ist, können wir eine Methode erstellen, mit der Daten mithilfe von verschlüsselt werden Chiffre Objekt, gegeben die Geheimer Schlüssel. Es wird ein zurückkehren HashMap Enthält die verschlüsselten Daten und eine randomisierte IV, die zum Entschlüsseln der Daten benötigt wird. Die verschlüsselten Daten können dann zusammen mit der IV in einer Datei oder in den gemeinsam genutzten Einstellungen gespeichert werden.

private HashMap verschlüsseln (final byte [] decryptedBytes) final HashMap map = new HashMap(); try // Schlüssel abschließen keyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey (); // Daten verschlüsseln final Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); abschließendes Byte [] ivBytes = cipher.getIV (); abschließendes Byte [] encryptedBytes = cipher.doFinal (decryptedBytes); map.put ("iv", ivBytes); map.put ("encrypted", encryptedBytes);  catch (Throwable e) e.printStackTrace ();  Karte zurückgeben; 

Entschlüsseln in ein Byte-Array

Zur Entschlüsselung wird die Umkehrung angewendet. Das Chiffre Das Objekt wird mit der DECRYPT_MODE konstant und entschlüsselt Byte[] Array wird zurückgegeben.

privates Byte [] entschlüsseln (letzte HashMap map) byte [] entschlüsselteBytes = null; try // Schlüssel abschließen keyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey (); // Info aus Map final Byte extrahieren [] encryptedBytes = map.get ("encrypted"); abschließendes Byte [] ivBytes = map.get ("iv"); // Daten entschlüsseln final Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); letzte GCMParameterSpec-Spezifikation = neue GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal (encryptedBytes);  catch (Throwable e) e.printStackTrace ();  return decryptedBytes; 

Das Beispiel testen

Wir können jetzt unser Beispiel testen!

@TargetApi (Build.VERSION_CODES.M) private void testEncryption () try // Generieren Sie einen Schlüssel und speichern Sie ihn im KeyStore final KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ( "MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // erfordert Bildschirm sperren, für ungültig erklärt, wenn Sperrbildschirm ist deaktiviert //.setUserAuthenticationValidityDurationSeconds (120) // // Nur x Sekunden nach Kennwortauthentifizierung verfügbar. -1 erfordert Fingerabdruck - jedes Mal .setRandomizedEncryptionRequired (true) // Unterschiedlicher Geheimtext für denselben Klartext bei jedem Aufruf .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Testen Sie die letzte HashMap map = encrypt ("Mein sehr empfindlicher String!". getBytes ("UTF-8")); abschließendes Byte [] entschlüsseltBytes = Entschlüsselung (Karte); final String decryptedString = neuer String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "Die entschlüsselte Zeichenfolge ist" + decryptedString);  catch (Throwable e) e.printStackTrace (); 

Verwenden von asymetrischen RSA-Schlüsseln für ältere Geräte

Dies ist eine gute Lösung zum Speichern von Daten für Version M und höher. Was aber, wenn Ihre App frühere Versionen unterstützt? Während symmetrische AES-Schlüssel unter M nicht unterstützt werden, sind dies bei asymmetrischen RSA-Schlüsseln der Fall. Das bedeutet, dass wir RSA-Schlüssel und Verschlüsselung verwenden können, um dasselbe zu erreichen. 

Der Hauptunterschied besteht darin, dass ein asymmetrisches Schlüsselpaar zwei Schlüssel enthält, einen privaten und einen öffentlichen Schlüssel, wobei der öffentliche Schlüssel die Daten verschlüsselt und der private Schlüssel sie entschlüsselt. EIN KeyPairGeneratorSpec wird in die übergeben KeyPairGenerator das wird mit initialisiert KEY_ALGORITHM_RSA und das "AndroidKeyStore" Anbieter.

private void testPreMEncryption () try // Ein Schlüsselpaar erzeugen und im KeyStore speichern KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Kalenderstart = Calendar.getInstance (); Kalenderende = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder (this) .setAlias ​​("MyKeyAlias") .setSubject (new X500Principal ("CN = MyKeyName, O = Android Authority")) .setSerialNumber (neuer BigInteger (1024, neuer Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // auf API-Ebene 18, im Ruhezustand verschlüsselt, muss der Sperrbildschirm eingerichtet werden; durch das Ändern des Sperrbildschirms wird der Schlüssel entfernt .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Encryption test abschließendes Byte [] encryptedBytes = rsaEncrypt ("My secret string!". GetBytes ("UTF-8")); abschließendes Byte [] entschlüsselteBytes = rsaDecrypt (encryptedBytes); final String decryptedString = neuer String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "Entschlüsselter String ist" + EntschlüsselterString);  catch (Throwable e) e.printStackTrace (); 

Zum Verschlüsseln bekommen wir die RSAPublicKey aus dem Schlüsselpaar und verwenden Sie es mit der Chiffre Objekt. 

public byte [] rsaEncrypt (abschließendes Byte [] 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 (); final Cipher cipher = Cipher.getInstance ("RSA / EZB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = neues ByteArrayOutputStream (); final CipherOutputStream cipherOutputStream = neuer CipherOutputStream (outputStream, cipher); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptedBytes = outputStream.toByteArray ();  catch (Throwable e) e.printStackTrace ();  return encryptedBytes; 

Die Entschlüsselung erfolgt mit RSAPrivateKey Objekt.

public byte [] rsaDecrypt (abschließendes Byte [] 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 (); final Cipher cipher = Cipher.getInstance ("RSA / EZB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privateKey); final CipherInputStream cipherInputStream = new CipherInputStream (neuer ByteArrayInputStream (encryptedBytes), Chiffre); letzte ArrayList arrayList = neue ArrayList <> (); int nextByte; while ((nextByte = cipherInputStream.read ())! = -1) arrayList.add ((Byte) nächstesByte);  decryptedBytes = neues Byte [arrayList.size ()]; für (int i = 0; i < decryptedBytes.length; i++)  decryptedBytes[i] = arrayList.get(i);   catch (Throwable e)  e.printStackTrace();  return decryptedBytes; 

Eine Sache bei RSA ist, dass die Verschlüsselung langsamer ist als bei AES. Dies ist in der Regel für kleine Informationsmengen geeignet, z. B. wenn Sie gemeinsam genutzte Präferenzzeichenfolgen sichern. Wenn Sie feststellen, dass ein Leistungsproblem bei der Verschlüsselung großer Datenmengen auftritt, können Sie dieses Beispiel stattdessen verwenden, um nur einen AES-Schlüssel zu verschlüsseln und zu speichern. Verwenden Sie dann die schnellere AES-Verschlüsselung, die im vorherigen Lernprogramm für den Rest Ihrer Daten beschrieben wurde. Sie können einen neuen AES-Schlüssel generieren und ihn in einen konvertieren Byte[] Array, das mit diesem Beispiel kompatibel ist.

KeyGenerator keyGenerator = KeyGenerator.getInstance ("AES"); keyGenerator.init (256); // AES-256 SecretKey secretKey = keyGenerator.generateKey (); Byte [] keyBytes = secretKey.getEncoded ();

Um den Schlüssel aus den Bytes zurückzuholen, gehen Sie folgendermaßen vor:

SecretKey key = new SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");

Das war viel Code! Um alle Beispiele einfach zu halten, habe ich auf eine sorgfältige Ausnahmebehandlung verzichtet. Denken Sie jedoch daran, dass es für Ihren Produktionscode nicht empfohlen wird, einfach alle zu fangen Wurffähig Fälle in einer catch-Anweisung.

Fazit

Damit ist das Lernprogramm zum Arbeiten mit Anmeldeinformationen und Schlüsseln abgeschlossen. Die Verwirrung bei den Schlüsseln und beim Speichern hängt größtenteils mit der Weiterentwicklung des Android-Betriebssystems zusammen. Sie können jedoch auswählen, welche Lösung verwendet werden soll, wenn die API-Ebene Ihrer App unterstützt wird. 

Nachdem wir die bewährten Methoden zum Sichern von Daten in Ruhe behandelt haben, wird sich das nächste Lernprogramm auf die Sicherung der Daten während des Transports konzentrieren.