Die Glaubwürdigkeit einer App hängt heute stark davon ab, wie die privaten Daten des Benutzers verwaltet werden. Der Android-Stack verfügt über zahlreiche leistungsfähige APIs für Anmeldeinformationen und Schlüsselspeicher. Bestimmte Funktionen sind nur in bestimmten Versionen verfügbar.
Diese kurze Serie beginnt mit einem einfachen Ansatz, um das Speichersystem aufzurufen und zu prüfen, wie vertrauliche Daten mithilfe eines vom Benutzer angegebenen Passcodes verschlüsselt und gespeichert werden. Im zweiten Tutorial werden wir uns mit komplexeren Methoden zum Schutz von Schlüsseln und Anmeldeinformationen befassen.
Die erste Frage, über die Sie nachdenken sollten, ist, wie viele Daten Sie tatsächlich erfassen müssen. Ein guter Ansatz besteht darin, das Speichern von privaten Daten zu vermeiden, wenn Sie dies nicht wirklich tun müssen.
Für Daten, die Sie speichern müssen, steht die Android-Architektur zur Verfügung. Seit 6.0 Marshmallow ist die Vollfestplattenverschlüsselung für Geräte mit dieser Funktion standardmäßig aktiviert. Dateien und Gemeinsame Einstellungen
die von der App gespeichert werden, werden automatisch mit eingestellt MODE_PRIVATE
Konstante. Dies bedeutet, dass die Daten nur von Ihrer eigenen App abgerufen werden können.
Es ist eine gute Idee, sich an diesen Standard zu halten. Sie können es explizit festlegen, wenn Sie eine gemeinsam genutzte Einstellung speichern.
SharedPreferences.Editor editor = getSharedPreferences ("preferenceName", MODE_PRIVATE) .edit (); editor.putString ("key", "value"); editor.commit ();
Oder beim Speichern einer Datei.
FileOutputStream fos = openFileOutput (filenameString, Context.MODE_PRIVATE); fos.write (Daten); fos.close ();
Speichern Sie die Daten nicht auf einem externen Speicher, da die Daten dann für andere Apps und Benutzer sichtbar sind. Um das Kopieren Ihrer App mit Binärdaten und Daten zu erschweren, können Sie verhindern, dass Benutzer die App auf einem externen Speicher installieren können. Hinzufügen android: installLocation
mit einem Wert von Nur intern
in der Manifest-Datei wird das erreichen.
Sie können auch verhindern, dass die App und ihre Daten gesichert werden. Dadurch wird auch verhindert, dass der Inhalt des privaten Datenverzeichnisses einer App mithilfe von heruntergeladen wird ADB-Sicherung
. Stellen Sie dazu das android: allowBackup
zuschreiben falsch
in der Manifestdatei. Standardmäßig ist dieses Attribut auf festgelegt wahr
.
Dies sind bewährte Vorgehensweisen, die jedoch für ein gefährdetes oder gerootetes Gerät nicht funktionieren. Die Festplattenverschlüsselung ist nur nützlich, wenn das Gerät mit einem Sperrbildschirm gesichert ist. Hier bietet sich ein app-seitiges Passwort an, das die verschlüsselten Daten schützt.
Conceal ist eine gute Wahl für eine Verschlüsselungsbibliothek, da Sie sehr schnell einsatzbereit sind, ohne sich um die zugrunde liegenden Details kümmern zu müssen. Ein Exploit, der auf ein beliebtes Framework abzielt, wirkt sich jedoch gleichzeitig auf alle Apps aus, die darauf angewiesen sind.
Es ist auch wichtig, sich mit der Funktionsweise von Verschlüsselungssystemen vertraut zu machen, um feststellen zu können, ob Sie ein bestimmtes Framework sicher verwenden. Für diesen Beitrag werden wir uns also die Hände schmutzig machen, indem wir den Kryptographieanbieter direkt betrachten.
Wir verwenden den empfohlenen AES-Standard, der Daten verschlüsselt, die mit einem Schlüssel angegeben wurden. Zum Entschlüsseln der Daten wird derselbe Schlüssel verwendet, der zum Verschlüsseln der Daten verwendet wird. Dies wird als symmetrische Verschlüsselung bezeichnet. Es gibt verschiedene Schlüsselgrößen, und AES256 (256 Bit) ist die bevorzugte Länge für die Verwendung mit sensiblen Daten.
Während die Benutzererfahrung Ihrer App einen Benutzer zwingen muss, einen starken Passcode zu verwenden, besteht die Möglichkeit, dass derselbe Passcode auch von einem anderen Benutzer ausgewählt wird. Die Sicherheit unserer verschlüsselten Daten in die Hände des Benutzers zu legen, ist nicht sicher. Unsere Daten müssen stattdessen mit einem gesichert werden Schlüssel das ist zufällig und groß genug (d.h. hat genug Entropie), um als stark angesehen zu werden. Aus diesem Grund wird es nie empfohlen, ein Kennwort direkt zum Verschlüsseln von Daten zu verwenden. Hierbei wird eine Funktion aufgerufen Passwortbasierte Schlüsselableitungsfunktion (PBKDF2) kommt ins Spiel.
PBKDF2 leitet a ab Schlüssel von einem Passwort durch hashas es viele Male mit einem Salz. Dies wird als Key Stretching bezeichnet. Das Salt ist nur eine zufällige Folge von Daten und macht den abgeleiteten Schlüssel eindeutig, auch wenn dasselbe Passwort von einer anderen Person verwendet wurde.
Beginnen wir damit, dieses Salz zu erzeugen.
SecureRandom random = new SecureRandom (); Byte Salt [] = neues Byte [256]; random.nextBytes (Salt);
Das SecureRandom
Klasse garantiert, dass die erzeugte Ausgabe schwer vorherzusagen ist - es handelt sich um einen "kryptographisch starken Zufallszahlengenerator". Wir können jetzt das Passwort und das Kennwort in ein kennwortbasiertes Verschlüsselungsobjekt einfügen: PBEKeySpec
. Der Konstruktor des Objekts nimmt auch eine Iterationszahl an, wodurch der Schlüssel stärker wird. Dies liegt daran, dass durch das Erhöhen der Anzahl von Iterationen die Zeit verlängert wird, die erforderlich ist, um bei einem Brute-Force-Angriff einen Tastensatz zu bearbeiten. Das PBEKeySpec
dann wird in die übergeben SecretKeyFactory
, das erzeugt schließlich den Schlüssel als Byte[]
Array. Wir werden das roh einpacken Byte[]
Array in eine SecretKeySpec
Objekt.
char [] passwordChar = passwordString.toCharArray (); // Passwort in char [] -Array umwandeln PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 Iterationen SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = neue SecretKeySpec (keyBytes, "AES");
Beachten Sie, dass das Passwort als übergeben wird verkohlen[]
Array und die PBEKeySpec
Klasse speichert es als verkohlen[]
Array auch. verkohlen[]
Arrays werden normalerweise für Verschlüsselungsfunktionen verwendet, da die String
Klasse ist unveränderlich, a verkohlen[]
Ein Array mit vertraulichen Informationen kann überschrieben werden, wodurch die vertraulichen Daten vollständig aus dem Gerätespeicher entfernt werden.
Wir sind jetzt bereit, die Daten zu verschlüsseln, aber wir haben noch etwas zu tun. Es gibt verschiedene Arten der Verschlüsselung mit AES, aber wir werden den empfohlenen verwenden: Cipher Block Chaining (CBC). Dies wirkt sich blockweise auf unsere Daten aus. Das Tolle an diesem Modus ist, dass jeder nächste unverschlüsselte Datenblock mit dem vorherigen verschlüsselten Block XOR-verknüpft wird, um die Verschlüsselung zu verstärken. Das bedeutet jedoch, dass der erste Block niemals so einzigartig ist wie alle anderen!
Wenn eine zu verschlüsselnde Nachricht genauso beginnen würde wie eine andere zu verschlüsselnde Nachricht, wäre die verschlüsselte Ausgabe am Anfang dieselbe und würde einem Angreifer einen Hinweis darauf geben, was die Nachricht sein könnte. Die Lösung ist die Verwendung eines Initialisierungsvektors (IV).
Ein IV ist nur ein Block von zufälligen Bytes, der mit dem ersten Block von Benutzerdaten XOR-verknüpft wird. Da jeder Block von allen bis zu diesem Zeitpunkt verarbeiteten Blöcken abhängt, wird die gesamte Nachricht verschlüsselt. Eindeutig identische Nachrichten, die mit demselben Schlüssel verschlüsselt werden, erzeugen keine identischen Ergebnisse.
Lassen Sie uns jetzt eine IV erstellen.
SecureRandom ivRandom = new SecureRandom (); // Zwischenspeicherung der zuvor gesetzten Instanz von SecureRandom byte [] iv = neues Byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv);
Eine Notiz über SecureRandom
. Bei den Versionen 4.3 und darunter bestand eine Sicherheitsanfälligkeit der Java Cryptography Architecture aufgrund einer fehlerhaften Initialisierung des zugrunde liegenden Pseudozufallszahlengenerators (PRNG). Wenn Sie auf Version 4.3 und darunter abzielen, ist ein Fix verfügbar.
Bewaffnet mit einem IvParameterSpec
, Wir können jetzt die eigentliche Verschlüsselung durchführen.
Cipher Cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] encrypted = cipher.doFinal (plainTextBytes);
Hier übergeben wir den String "AES / CBC / PKCS7Padding"
. Dies spezifiziert die AES-Verschlüsselung mit Verschlüsselungsblockverkettung. Der letzte Teil dieser Zeichenfolge bezieht sich auf PKCS7, einen etablierten Standard für das Auffüllen von Daten, die nicht perfekt in die Blockgröße passen. (Blöcke sind 128 Bit, und das Auffüllen erfolgt vor der Verschlüsselung.)
Um unser Beispiel zu vervollständigen, fügen wir diesen Code in eine Verschlüsselungsmethode ein, die das Ergebnis in eine HashMap
enthält die verschlüsselten Daten zusammen mit dem zur Entschlüsselung erforderlichen Salt- und Initialisierungsvektor.
private HashMapencryptBytes (Byte [] plainTextBytes, String passwordString) HashMap map = new HashMap (); versuchen Sie // Random Salt für den nächsten Schritt SecureRandom random = new SecureRandom (); Byte Salt [] = neues Byte [256]; random.nextBytes (Salt); // PBKDF2 - leiten Sie den Schlüssel vom Kennwort ab, verwenden Sie keine Kennwörter. Direkt char [] passwordChar = passwordString.toCharArray (); // Passwort in char [] -Array umwandeln PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 Iterationen SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = neue SecretKeySpec (keyBytes, "AES"); // Initialisierungsvektor für AES erstellen SecureRandom ivRandom = new SecureRandom (); // Zwischenspeicherung der zuvor gesetzten Instanz von SecureRandom byte [] iv = neues 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 [] encrypted = cipher.doFinal (plainTextBytes); map.put ("Salz", Salz); map.put ("iv", iv); map.put ("verschlüsselt", verschlüsselt); catch (Ausnahme e) Log.e ("MYAPP", "Verschlüsselungsausnahme", e); Karte zurückgeben;
Sie müssen lediglich die IV und Salt mit Ihren Daten speichern. Stellen Sie sicher, dass Salze und IVs nicht als öffentlich gelten, sondern sequenziell erhöht oder wiederverwendet werden. Um die Daten zu entschlüsseln, müssen wir nur den Modus im ändern Chiffre
Konstruktor von ENCRYPT_MODE
zu DECRYPT_MODE
.
Die Entschlüsselungsmethode dauert a HashMap
das die gleichen erforderlichen Informationen enthält (verschlüsselte Daten, Salt und IV) und eine entschlüsselte zurückgeben Byte[]
Array mit dem richtigen Passwort. Die Entschlüsselungsmethode generiert den Verschlüsselungsschlüssel anhand des Kennworts neu. Der Schlüssel sollte niemals gespeichert werden!
privates Byte [] decryptData (HashMapmap, String passwordString) byte [] entschlüsselt = null; try Byte Salz [] = map.get ("Salz"); Byte iv [] = map.get ("iv"); Byte verschlüsselt [] = map.get ("verschlüsselt"); // Schlüssel aus Passwort regenerieren 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 = neue SecretKeySpec (keyBytes, "AES"); // Decrypt Cipher Cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = new IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); entschlüsselt = cipher.doFinal (verschlüsselt); catch (Ausnahme e) Log.e ("MYAPP", "Entschlüsselungsausnahme", e); Rückkehr entschlüsselt;
Um das Beispiel einfach zu halten, lassen wir die Fehlerüberprüfung weg, um sicherzustellen, dass HashMap
enthält die erforderlichen Schlüssel / Wert-Paare. Wir können jetzt unsere Methoden testen, um sicherzustellen, dass die Daten nach der Verschlüsselung korrekt entschlüsselt werden.
// Encryption test String string = "Meine sensible Zeichenfolge, die ich verschlüsseln möchte"; byte [] bytes = string.getBytes (); HashMapmap = encryptBytes (Bytes, "UserSuppliedPassword"); // Entschlüsselungstestbyte [] entschlüsselt = decryptData (map, "UserSuppliedPassword"); if (entschlüsselt! = null) String decryptedString = neuer String (entschlüsselt); Log.e ("MYAPP", "Entschlüsselter String ist:" + decryptedString);
Die Methoden verwenden a Byte[]
Array, so dass Sie beliebige Daten statt nur verschlüsseln können String
Objekte.
Jetzt haben wir ein verschlüsselt Byte[]
Array, können wir es speichern.
FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (verschlüsselt); fos.close ();
Wenn Sie die IV und das Salz nicht separat speichern möchten, HashMap
ist mit dem serialisierbar ObjectInputStream
und ObjectOutputStream
Klassen.
FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = neuer ObjectOutputStream (fos); oos.writeObject (map); oos.close ();
Gemeinsame Einstellungen
Sie können auch sichere Daten in Ihren Apps speichern Gemeinsame Einstellungen
.
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 ();
Seit der Gemeinsame Einstellungen
Ist ein XML-System, das nur bestimmte Grundelemente und Objekte als Werte akzeptiert. Wir müssen unsere Daten in ein kompatibles Format wie a konvertieren String
Objekt. Mit Base64 können wir die Rohdaten in a konvertieren String
Darstellung, die nur die im XML-Format zulässigen Zeichen enthält. Verschlüsseln Sie sowohl den Schlüssel als auch den Wert, sodass ein Angreifer nicht herausfinden kann, wozu ein Wert verwendet werden kann.
Im obigen Beispiel, encryptedKey
und encryptedValue
sind beide verschlüsselt Byte[]
Arrays von unserem zurückgegeben encryptBytes ()
Methode. IV und Salt können in der Voreinstellungsdatei oder als separate Datei gespeichert werden. Um die verschlüsselten Bytes aus dem Gemeinsame Einstellungen
, Wir können eine Base64-Dekodierung auf die gespeicherten anwenden String
.
SharedPreferences preferences = getSharedPreferences ("prefs", Context.MODE_PRIVATE); String base64EncryptedString = preferences.getString (keyBase64String, "default"); Byte [] encryptedBytes = Base64.decode (base64EncryptedString, Base64.NO_WRAP);
Nun, da die gespeicherten Daten sicher sind, kann es vorkommen, dass Sie eine vorherige Version der App haben, in der die Daten unsicher gespeichert wurden. Bei einem Upgrade können die Daten gelöscht und erneut verschlüsselt werden. Mit dem folgenden Code wird eine Datei mit zufälligen Daten gelöscht.
Theoretisch können Sie einfach Ihre freigegebenen Einstellungen löschen, indem Sie die /data/data/com.your.package.name/shared_prefs/ihr_prefs_name.xml und ihr_prefsame.bak Dateien und Löschen der Voreinstellungen im Arbeitsspeicher mit folgendem Code:
getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (). clear (). commit ();
Anstatt zu versuchen, die alten Daten zu löschen und zu hoffen, dass sie funktionieren, ist es besser, sie zuerst zu verschlüsseln! Dies gilt insbesondere für Solid-State-Laufwerke, die häufig Datenschreibvorgänge in verschiedene Regionen ausbreiten, um Verschleiß zu vermeiden. Das heißt, auch wenn Sie eine Datei im Dateisystem überschreiben, behält der physische Halbleiterspeicher Ihre Daten möglicherweise am ursprünglichen Speicherort auf der Festplatte.
public static void secureWipeFile (File-Datei) löst IOException aus if (file! = null && file.exists ()) final long length = file.length (); final SecureRandom random = new SecureRandom (); final RandomAccessFile randomAccessFile = new RandomAccessFile (Datei, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); Byte [] Daten = neues Byte [64]; int Position = 0; während (Position < length) random.nextBytes(data); randomAccessFile.write(data); position += data.length; randomAccessFile.close(); file.delete();
Damit ist unser Tutorial zum Speichern verschlüsselter Daten abgeschlossen. In diesem Beitrag haben Sie gelernt, wie Sie vertrauliche Daten mit einem vom Benutzer angegebenen Kennwort sicher verschlüsseln und entschlüsseln können. Es ist einfach, wenn Sie wissen, wie es geht, aber es ist wichtig, alle bewährten Methoden zu befolgen, um sicherzustellen, dass die Daten Ihrer Benutzer wirklich sicher sind.
In dem nächsten Post werden wir einen Blick darauf werfen, wie wir das nutzen können KeyStore
und andere APIs, die sich auf Anmeldeinformationen beziehen, um Elemente sicher zu speichern. In der Zwischenzeit finden Sie einige unserer anderen großartigen Artikel zur Entwicklung von Android-Apps.