() translation by (you can also view the original English article)
In diesem Beitrag werden erweiterte Verwendungszwecke der Verschlüsselung für Benutzerdaten in iOS-Apps beschrieben. Wir beginnen mit einem allgemeinen Blick auf die AES-Verschlüsselung und sehen uns dann einige Beispiele für die Implementierung der AES-Verschlüsselung in Swift an.
Im letzten Beitrag haben Sie gelernt, wie Sie Daten mithilfe des Schlüsselbunds speichern. Dies ist gut für kleine Informationen wie Schlüssel, Kennwörter und Zertifikate.
Wenn Sie eine große Menge benutzerdefinierter Daten speichern, die erst nach der Authentifizierung des Benutzers oder Geräts verfügbar sein sollen, ist es besser, die Daten mit einem Verschlüsselungsframework zu verschlüsseln. Beispielsweise verfügen Sie möglicherweise über eine App, mit der vom Benutzer gespeicherte private Chat-Nachrichten oder vom Benutzer aufgenommene private Fotos archiviert oder die finanziellen Details des Benutzers gespeichert werden können. In diesen Fällen möchten Sie wahrscheinlich die Verschlüsselung verwenden.
In Anwendungen gibt es zwei gängige Abläufe zum Ver- und Entschlüsseln von Daten aus iOS-Apps. Entweder wird dem Benutzer ein Kennwortbildschirm angezeigt, oder die Anwendung wird bei einem Server authentifiziert, der einen Schlüssel zum Entschlüsseln der Daten zurückgibt.
Es ist nie eine gute Idee, das Rad neu zu erfinden, wenn es um Verschlüsselung geht. Daher werden wir den AES-Standard verwenden, der von der iOS Common Crypto-Bibliothek bereitgestellt wird.
AES
AES ist ein Standard, der Daten mit einem Schlüssel verschlüsselt. Der gleiche Schlüssel, der zum Verschlüsseln der Daten verwendet wird, wird zum Entschlüsseln der Daten verwendet. Es gibt verschiedene Schlüsselgrößen, und AES256 (256 Bit) ist die bevorzugte Länge für sensible Daten.
RNCryptor ist ein beliebter Verschlüsselungs-Wrapper für iOS, der AES unterstützt. RNCryptor ist eine gute Wahl, da Sie damit sehr schnell einsatzbereit sind, ohne sich um die zugrunde liegenden Details kümmern zu müssen. Es ist auch Open Source, damit Sicherheitsforscher den Code analysieren und prüfen können.
Wenn Ihre App jedoch mit sehr vertraulichen Informationen arbeitet und Sie der Meinung sind, dass Ihre Anwendung zielgerichtet und geknackt wird, möchten Sie möglicherweise Ihre eigene Lösung schreiben. Der Grund dafür ist, dass wenn viele Apps denselben Code verwenden, dies die Arbeit des Hackers erleichtern kann, sodass sie eine Cracking-App schreiben können, die gemeinsame Muster im Code findet und Patches auf sie anwendet.
Beachten Sie jedoch, dass das Schreiben einer eigenen Lösung nur einen Angreifer verlangsamt und automatisierte Angriffe verhindert. Der Schutz, den Sie durch Ihre eigene Implementierung erhalten, besteht darin, dass ein Hacker Zeit und Engagement darauf verwenden muss, Ihre App alleine zu knacken.
Unabhängig davon, ob Sie sich für eine Lösung von Drittanbietern oder für eine eigene Lösung entscheiden, ist es wichtig, über die Funktionsweise von Verschlüsselungssystemen informiert zu sein. Auf diese Weise können Sie entscheiden, ob ein bestimmtes Framework, das Sie verwenden möchten, wirklich sicher ist. Daher konzentriert sich der Rest dieses Tutorials darauf, Ihre eigene benutzerdefinierte Lösung zu schreiben. Mit dem Wissen, das Sie aus diesem Tutorial lernen, können Sie feststellen, ob Sie ein bestimmtes Framework sicher verwenden.
Wir beginnen mit der Erstellung eines geheimen Schlüssels, mit dem Ihre Daten verschlüsselt werden.
Erstellen Sie einen Schlüssel
Ein sehr häufiger Fehler bei der AES-Verschlüsselung besteht darin, das Kennwort eines Benutzers direkt als Verschlüsselungsschlüssel zu verwenden. Was ist, wenn der Benutzer ein allgemeines oder schwaches Passwort verwendet? Wie zwingen wir Benutzer, einen Schlüssel zu verwenden, der zufällig und stark genug (mit genügend Entropie) für die Verschlüsselung ist, und sie dann daran zu erinnern?
Die Lösung ist Key Stretching. Das Strecken von Schlüsseln leitet einen Schlüssel von einem Passwort ab, indem es mehrmals mit einem Salz gehasht wird. Das Salz ist nur eine Folge von Zufallsdaten, und es ist ein häufiger Fehler, dieses Salz wegzulassen - das Salz verleiht dem Schlüssel seine lebenswichtige Entropie, und ohne das Salz würde derselbe Schlüssel abgeleitet, wenn jemand dasselbe Passwort verwenden würde sonst.
Ohne das Salz könnte ein Wörterbuch von Wörtern verwendet werden, um gemeinsame Schlüssel abzuleiten, die dann verwendet werden könnten, um Benutzerdaten anzugreifen. Dies wird als "Wörterbuchangriff" bezeichnet. Zu diesem Zweck werden Tabellen mit allgemeinen Schlüsseln verwendet, die ungesalzenen Passwörtern entsprechen. Sie werden "Regenbogentische" genannt.
Eine weitere Gefahr beim Erstellen eines Salzes ist die Verwendung einer Zufallszahlengenerierungsfunktion, die nicht für die Sicherheit ausgelegt ist. Ein Beispiel ist die Funktion rand()
in C, auf die von Swift aus zugegriffen werden kann. Diese Ausgabe kann sehr vorhersehbar sein!
Um ein sicheres Salt zu erstellen, verwenden wir die Funktion SecRandomCopyBytes
, um kryptografisch sichere Zufallsbytes zu erstellen, dh Zahlen, die schwer vorherzusagen sind.
Um den Code zu verwenden, müssen Sie Folgendes in Ihren Bridging-Header einfügen:#import <CommonCrypto/CommonCrypto.h>
Hier ist der Anfang des Codes, der ein Salz erzeugt. Wir werden diesen Code im Laufe der Zeit ergänzen:
1 |
var salt = Data(count: 8) |
2 |
salt.withUnsafeMutableBytes { (saltBytes: UnsafeMutablePointer<UInt8>) -> Void in |
3 |
let saltStatus = SecRandomCopyBytes(kSecRandomDefault, salt.count, saltBytes) |
4 |
//...
|
Jetzt sind wir bereit, die Schlüssel zu dehnen. Glücklicherweise steht uns bereits eine Funktion zur Verfügung, um das eigentliche Stretching durchzuführen: die passwortbasierte Schlüsselableitungsfunktion (PBKDF2). PBKDF2 führt eine Funktion um ein Vielfaches aus, um den Schlüssel abzuleiten. Durch Erhöhen der Anzahl der Iterationen wird die Zeit verlängert, die für die Bearbeitung eines Schlüsselsatzes während eines Brute-Force-Angriffs erforderlich ist. Es wird empfohlen, PBKDF2 zum Generieren Ihres Schlüssels zu verwenden.
1 |
var setupSuccess = true |
2 |
var key = Data(repeating:0, count:kCCKeySizeAES256) |
3 |
var salt = Data(count: 8) |
4 |
salt.withUnsafeMutableBytes { (saltBytes: UnsafeMutablePointer<UInt8>) -> Void in |
5 |
let saltStatus = SecRandomCopyBytes(kSecRandomDefault, salt.count, saltBytes) |
6 |
if saltStatus == errSecSuccess |
7 |
{
|
8 |
let passwordData = password.data(using:String.Encoding.utf8)! |
9 |
key.withUnsafeMutableBytes { (keyBytes : UnsafeMutablePointer<UInt8>) in |
10 |
let derivationStatus = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, passwordData.count, saltBytes, salt.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 14271, keyBytes, key.count) |
11 |
if derivationStatus != Int32(kCCSuccess) |
12 |
{
|
13 |
setupSuccess = false |
14 |
}
|
15 |
}
|
16 |
}
|
17 |
else
|
18 |
{
|
19 |
setupSuccess = false |
20 |
}
|
21 |
}
|
Serverseitiger Schlüssel
Möglicherweise fragen Sie sich jetzt, in welchen Fällen Benutzer nicht aufgefordert werden müssen, ein Kennwort in Ihrer App anzugeben. Möglicherweise authentifizieren sie sich bereits mit einem Single Sign-On-Schema. Lassen Sie in diesem Fall Ihren Server mithilfe eines sicheren Generators einen AES-256-Bit-Schlüssel (32 Byte) generieren. Der Schlüssel sollte für verschiedene Benutzer oder Geräte unterschiedlich sein. Bei der Authentifizierung bei Ihrem Server können Sie dem Server eine Geräte- oder Benutzer-ID über eine sichere Verbindung übergeben und den entsprechenden Schlüssel zurücksenden.
Dieses Schema hat einen großen Unterschied. Wenn der Schlüssel vom Server stammt, kann die Entität, die diesen Server steuert, die verschlüsselten Daten lesen, wenn das Gerät oder die Daten jemals abgerufen wurden. Es besteht auch die Möglichkeit, dass der Schlüssel zu einem späteren Zeitpunkt durchgesickert oder freigelegt wird.
Wenn der Schlüssel jedoch von etwas abgeleitet ist, das nur der Benutzer kennt - dem Kennwort des Benutzers -, kann nur der Benutzer diese Daten entschlüsseln. Wenn Sie Informationen wie private Finanzdaten schützen, sollte nur der Benutzer in der Lage sein, die Daten zu entsperren. Wenn diese Informationen der Entität ohnehin bekannt sind, kann es akzeptabel sein, dass der Server den Inhalt über einen serverseitigen Schlüssel entsperrt.
Modi und IVs
Nachdem wir einen Schlüssel haben, verschlüsseln wir einige Daten. Es gibt verschiedene Verschlüsselungsmodi, aber wir werden den empfohlenen Modus verwenden: Cipher Block Chaining (CBC). Dies bearbeitet unsere Daten blockweise.
Eine häufige Gefahr bei CBC ist die Tatsache, dass jeder nächste unverschlüsselte Datenblock mit dem vorherigen verschlüsselten Block XOR-verknüpft ist, um die Verschlüsselung zu verstärken. Das Problem hierbei ist, dass der erste Block niemals so eindeutig ist wie alle anderen. Wenn eine zu verschlüsselnde Nachricht genauso wie eine andere zu verschlüsselnde Nachricht beginnen würde, wäre die beginnende verschlüsselte Ausgabe dieselbe, und dies würde einem Angreifer einen Hinweis darauf geben, wie die Nachricht aussehen könnte.
Um diese potenzielle Schwäche zu umgehen, beginnen wir die zu speichernden Daten mit einem sogenannten Initialisierungsvektor (IV): einem Block zufälliger Bytes. Die IV wird mit dem ersten Block von Benutzerdaten XOR-verknüpft. Da jeder Block von allen bis zu diesem Zeitpunkt verarbeiteten Blöcken abhängt, wird sichergestellt, dass die gesamte Nachricht eindeutig verschlüsselt wird, auch wenn sie dieselben Daten wie ein anderer enthält Botschaft. Mit anderen Worten, identische Nachrichten, die mit demselben Schlüssel verschlüsselt wurden, führen nicht zu identischen Ergebnissen. Während Salze und Infusionen als öffentlich angesehen werden, sollten sie nicht nacheinander erwendet oder wiederverwendet werden.
Wir werden dieselbe sichere SecRandomCopyBytes
-Funktion verwenden, um die IV zu erstellen.
1 |
var iv = Data.init(count: kCCBlockSizeAES128) |
2 |
iv.withUnsafeMutableBytes { (ivBytes : UnsafeMutablePointer<UInt8>) in |
3 |
let ivStatus = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes) |
4 |
if ivStatus != errSecSuccess |
5 |
{
|
6 |
setupSuccess = false |
7 |
}
|
8 |
}
|
Alles zusammenfügen
Um unser Beispiel zu vervollständigen, verwenden wir die CCCrypt
-Funktion entweder mit kCCEncrypt
oder kCCDecrypt
. Da wir eine Blockverschlüsselung verwenden und die Nachricht nicht gut in ein Vielfaches der Blockgröße passt, müssen wir die Funktion anweisen, am Ende automatisch Auffüllungen hinzuzufügen.
Wie bei der Verschlüsselung üblich, ist es am besten, etablierte Standards zu befolgen. In diesem Fall definiert das Standard-PKCS7, wie die Daten aufgefüllt werden. Wir weisen unsere Verschlüsselungsfunktion an, diesen Standard zu verwenden, indem wir die Option KCCOptionPKCS7Padding
bereitstellen. Hier finden Sie den vollständigen Code zum Ver- und Entschlüsseln eines Strings.
1 |
class func encryptData(_ clearTextData : Data, withPassword password : String) -> Dictionary<String, Data> |
2 |
{
|
3 |
var setupSuccess = true |
4 |
var outDictionary = Dictionary<String, Data>.init() |
5 |
var key = Data(repeating:0, count:kCCKeySizeAES256) |
6 |
var salt = Data(count: 8) |
7 |
salt.withUnsafeMutableBytes { (saltBytes: UnsafeMutablePointer<UInt8>) -> Void in |
8 |
let saltStatus = SecRandomCopyBytes(kSecRandomDefault, salt.count, saltBytes) |
9 |
if saltStatus == errSecSuccess |
10 |
{
|
11 |
let passwordData = password.data(using:String.Encoding.utf8)! |
12 |
key.withUnsafeMutableBytes { (keyBytes : UnsafeMutablePointer<UInt8>) in |
13 |
let derivationStatus = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, passwordData.count, saltBytes, salt.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 14271, keyBytes, key.count) |
14 |
if derivationStatus != Int32(kCCSuccess) |
15 |
{
|
16 |
setupSuccess = false |
17 |
}
|
18 |
}
|
19 |
}
|
20 |
else
|
21 |
{
|
22 |
setupSuccess = false |
23 |
}
|
24 |
}
|
25 |
|
26 |
var iv = Data.init(count: kCCBlockSizeAES128) |
27 |
iv.withUnsafeMutableBytes { (ivBytes : UnsafeMutablePointer<UInt8>) in |
28 |
let ivStatus = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes) |
29 |
if ivStatus != errSecSuccess |
30 |
{
|
31 |
setupSuccess = false |
32 |
}
|
33 |
}
|
34 |
|
35 |
if (setupSuccess) |
36 |
{
|
37 |
var numberOfBytesEncrypted : size_t = 0 |
38 |
let size = clearTextData.count + kCCBlockSizeAES128 |
39 |
var encrypted = Data.init(count: size) |
40 |
let cryptStatus = iv.withUnsafeBytes {ivBytes in |
41 |
encrypted.withUnsafeMutableBytes {encryptedBytes in |
42 |
clearTextData.withUnsafeBytes {clearTextBytes in |
43 |
key.withUnsafeBytes {keyBytes in |
44 |
CCCrypt(CCOperation(kCCEncrypt), |
45 |
CCAlgorithm(kCCAlgorithmAES), |
46 |
CCOptions(kCCOptionPKCS7Padding + kCCModeCBC), |
47 |
keyBytes, |
48 |
key.count, |
49 |
ivBytes, |
50 |
clearTextBytes, |
51 |
clearTextData.count, |
52 |
encryptedBytes, |
53 |
size, |
54 |
&numberOfBytesEncrypted) |
55 |
}
|
56 |
}
|
57 |
}
|
58 |
}
|
59 |
if cryptStatus == Int32(kCCSuccess) |
60 |
{
|
61 |
encrypted.count = numberOfBytesEncrypted |
62 |
outDictionary["EncryptionData"] = encrypted |
63 |
outDictionary["EncryptionIV"] = iv |
64 |
outDictionary["EncryptionSalt"] = salt |
65 |
}
|
66 |
}
|
67 |
|
68 |
return outDictionary; |
69 |
}
|
Und hier ist der Entschlüsselungscode:
1 |
class func decryp(fromDictionary dictionary : Dictionary<String, Data>, withPassword password : String) -> Data |
2 |
{
|
3 |
var setupSuccess = true |
4 |
let encrypted = dictionary["EncryptionData"] |
5 |
let iv = dictionary["EncryptionIV"] |
6 |
let salt = dictionary["EncryptionSalt"] |
7 |
var key = Data(repeating:0, count:kCCKeySizeAES256) |
8 |
salt?.withUnsafeBytes { (saltBytes: UnsafePointer<UInt8>) -> Void in |
9 |
let passwordData = password.data(using:String.Encoding.utf8)! |
10 |
key.withUnsafeMutableBytes { (keyBytes : UnsafeMutablePointer<UInt8>) in |
11 |
let derivationStatus = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, passwordData.count, saltBytes, salt!.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA512), 14271, keyBytes, key.count) |
12 |
if derivationStatus != Int32(kCCSuccess) |
13 |
{
|
14 |
setupSuccess = false |
15 |
}
|
16 |
}
|
17 |
}
|
18 |
|
19 |
var decryptSuccess = false |
20 |
let size = (encrypted?.count)! + kCCBlockSizeAES128 |
21 |
var clearTextData = Data.init(count: size) |
22 |
if (setupSuccess) |
23 |
{
|
24 |
var numberOfBytesDecrypted : size_t = 0 |
25 |
let cryptStatus = iv?.withUnsafeBytes {ivBytes in |
26 |
clearTextData.withUnsafeMutableBytes {clearTextBytes in |
27 |
encrypted?.withUnsafeBytes {encryptedBytes in |
28 |
key.withUnsafeBytes {keyBytes in |
29 |
CCCrypt(CCOperation(kCCDecrypt), |
30 |
CCAlgorithm(kCCAlgorithmAES128), |
31 |
CCOptions(kCCOptionPKCS7Padding + kCCModeCBC), |
32 |
keyBytes, |
33 |
key.count, |
34 |
ivBytes, |
35 |
encryptedBytes, |
36 |
(encrypted?.count)!, |
37 |
clearTextBytes, |
38 |
size, |
39 |
&numberOfBytesDecrypted) |
40 |
}
|
41 |
}
|
42 |
}
|
43 |
}
|
44 |
if cryptStatus! == Int32(kCCSuccess) |
45 |
{
|
46 |
clearTextData.count = numberOfBytesDecrypted |
47 |
decryptSuccess = true |
48 |
}
|
49 |
}
|
50 |
|
51 |
return decryptSuccess ? clearTextData : Data.init(count: 0) |
52 |
}
|
Zum Schluss noch ein Test, um sicherzustellen, dass die Daten nach der Verschlüsselung korrekt entschlüsselt werden:
1 |
class func encryptionTest() |
2 |
{
|
3 |
let clearTextData = "some clear text to encrypt".data(using:String.Encoding.utf8)! |
4 |
let dictionary = encryptData(clearTextData, withPassword: "123456") |
5 |
let decrypted = decryp(fromDictionary: dictionary, withPassword: "123456") |
6 |
let decryptedString = String(data: decrypted, encoding: String.Encoding.utf8) |
7 |
print("decrypted cleartext result - ", decryptedString ?? "Error: Could not convert data to string") |
8 |
}
|
In unserem Beispiel verpacken wir alle erforderlichen Informationen und geben sie als Dictionary
zurück, damit alle Teile später zum erfolgreichen Entschlüsseln der Daten verwendet werden können. Sie müssen nur die Infusion und das Salz entweder im Schlüsselbund oder auf Ihrem Server speichern.
Abschluss
Damit ist die dreiteilige Serie zur Sicherung ruhender Daten abgeschlossen. Wir haben gesehen, wie Passwörter, vertrauliche Informationen und große Mengen an Benutzerdaten richtig gespeichert werden. Diese Techniken bilden die Grundlage für den Schutz gespeicherter Benutzerinformationen in Ihrer App.
Es ist ein großes Risiko, wenn das Gerät eines Benutzers verloren geht oder gestohlen wird, insbesondere bei jüngsten Exploits, um Zugriff auf ein gesperrtes Gerät zu erhalten. Während viele Systemschwachstellen mit einem Software-Update behoben werden, ist das Gerät selbst nur so sicher wie der Passcode und die Version von iOS des Benutzers. Daher ist es Sache des Entwicklers jeder App, einen starken Schutz der gespeicherten sensiblen Daten zu gewährleisten.
Alle bisher behandelten Themen nutzen die Frameworks von Apple. Ich werde Ihnen eine Idee hinterlassen, über die Sie nachdenken können. Was passiert, wenn die Verschlüsselungsbibliothek von Apple angegriffen wird?
Wenn eine häufig verwendete Sicherheitsarchitektur gefährdet ist, werden auch alle darauf basierenden Apps gefährdet. Jede der dynamisch verknüpften Bibliotheken von iOS, insbesondere auf Geräten mit Jailbreak, kann gepatcht und gegen bösartige ausgetauscht werden.
Eine statische Bibliothek, die mit der Binärdatei Ihrer App gebündelt ist, ist jedoch vor dieser Art von Angriff geschützt, da Sie beim Versuch, sie zu patchen, die App-Binärdatei ändern müssen. Dadurch wird die Codesignatur der App beschädigt und der Start verhindert. Wenn Sie beispielsweise OpenSSL für Ihre Verschlüsselung importieren und verwenden, ist Ihre App nicht anfällig für einen weit verbreiteten Apple API-Angriff. Sie können OpenSSL selbst kompilieren und statisch mit Ihrer App verknüpfen.
Es gibt also immer mehr zu lernen und die Zukunft der App-Sicherheit unter iOS entwickelt sich ständig weiter. Die iOS-Sicherheitsarchitektur unterstützt bereits jetzt kryptografische Geräte und Smartcards! Abschließend kennen Sie jetzt die Best Practices für die Sicherung von Daten in Ruhe. Es liegt also an Ihnen, diese zu befolgen!
In der Zwischenzeit können Sie einige unserer anderen Inhalte zur Entwicklung von iOS-Apps und zur App-Sicherheit lesen.