Qualsiasi app che salva i dati dell'utente deve prendersi cura della sicurezza e della privacy di tali dati. Come abbiamo visto con recenti violazioni dei dati, ci possono essere conseguenze molto gravi per la mancata protezione dei dati memorizzati degli utenti. In questo tutorial imparerai alcune best practice per proteggere i dati dei tuoi utenti.
Nel post precedente, hai imparato come proteggere i file usando l'API di Data Protection. La protezione basata su file è una potente funzionalità per la memorizzazione sicura di dati collettivi. Ma potrebbe essere eccessivo per una piccola quantità di informazioni da proteggere, come una chiave o una password. Per questi tipi di articoli, il portachiavi è la soluzione consigliata.
Il portachiavi è un ottimo posto dove archiviare piccole quantità di informazioni come stringhe sensibili e ID che persistono anche quando l'utente elimina l'app. Un esempio potrebbe essere un token di dispositivo o di sessione che il server ritorna all'app durante la registrazione. Sia che tu lo chiami una stringa segreta o un token unico, il portachiavi si riferisce a tutti questi elementi come Le password.
Esistono alcune popolari librerie di terze parti per i servizi portachiavi, come Strongbox (Swift) e SSKeychain (Objective-C). Oppure, se vuoi il controllo completo sul tuo codice, potresti voler utilizzare direttamente l'API dei servizi Portachiavi, che è un'API C.
Spiegherò brevemente come funziona il portachiavi. Puoi pensare al portachiavi come un tipico database in cui esegui le query su un tavolo. Le funzioni dell'API del portachiavi richiedono tutte a CFDictionary
oggetto che contiene gli attributi della query.
Ogni voce nel portachiavi ha un nome di servizio. Il nome del servizio è un identificativo: a chiave per qualsiasi cosa valore vuoi archiviare o recuperare nel portachiavi. Per consentire l'archiviazione di un elemento portachiavi solo per un utente specifico, spesso si desidera specificare un nome account.
Poiché ogni funzione di portachiavi richiede un dizionario simile con molti degli stessi parametri per creare una query, è possibile evitare il codice duplicato creando una funzione di supporto che restituisce questo dizionario di query.
import Security // ... class func passwordQuery (servizio: String, account: String) -> Dizionariolet dictionary = [kSecClass come stringa: kSecClassGenericPassword, kSecAttrAccount as String: account, kSecAttrService as String: service, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked // Se è necessario l'accesso in background, potrebbe prendere in considerazione kSecAttrAccessibleAfterFirstUnlock] come [String: Any] dizionario di ritorno
Questo codice imposta la query Dizionario
con il tuo account e i nomi dei servizi e dice al portachiavi che memorizzeremo una password.
Analogamente a come è possibile impostare il livello di protezione per i singoli file (come abbiamo discusso nel post precedente), è possibile anche impostare i livelli di protezione per l'elemento portachiavi utilizzando il kSecAttrAccessible
chiave.
Il SecItemAdd ()
la funzione aggiunge dati al portachiavi. Questa funzione richiede a Dati
oggetto, che lo rende versatile per la memorizzazione di molti tipi di oggetti. Utilizzando la funzione di interrogazione della password creata in precedenza, memorizziamo una stringa nel portachiavi. Per fare questo, dobbiamo solo convertire il Stringa
a Dati
.
@discardableResult class func setPassword (_ password: String, service: String, account: String) -> Bool var status: OSStatus = -1 if! (service.isEmpty) &&! (account.isEmpty) deletePassword (servizio: service , account: account) // elimina la password se passa una stringa vuota. Potrebbe passare a passare nil per cancellare la password, ecc se! Password.isEmpty var dictionary = passwordQuery (servizio: servizio, account: account) let dataFromString = password.data (usando: String.Encoding.utf8, allowLossyConversion: false) dizionario [ kSecValueData as String] = dataFromString status = SecItemAdd (dizionario come CFDictionary, nil) stato di ritorno == errSecSuccess
Per evitare inserimenti duplicati, il codice sopra prima cancella la voce precedente se ce n'è uno. Scriviamo ora quella funzione. Questo è realizzato usando il SecItemDelete ()
funzione.
@discardableResult class func deletePassword (service: String, account: String) -> Bool var status: OSStatus = -1 if! (service.isEmpty) &&! (account.isEmpty) let dictionary = passwordQuery (servizio: servizio, account : account) status = SecItemDelete (dizionario come CFDictionary); stato di ritorno == errSecSuccess
Quindi, per recuperare una voce dal portachiavi, utilizzare il SecItemCopyMatching ()
funzione. Restituirà un ANYOBJECT
che corrisponde alla tua richiesta.
class func password (servizio: String, account: String) -> String // restituisce stringa vuota se non trovata, potrebbe restituire uno stato facoltativo var: OSStatus = -1 var resultString = "" if! (service.isEmpty) &&! (account.isEmpty) var passwordData: AnyObject? var dictionary = passwordQuery (servizio: servizio, account: account) dizionario [kSecReturnData as String] = kCFBooleanTrue dictionary [kSecMatchLimit as String] = kSecMatchLimitOne status = SecItemCopyMatching (dizionario come CFDictionary e passwordData) se stato == errSecSuccess se let retrievedData = passwordData come? Dati resultString = String (data: retrievedData, encoding: String.Encoding.utf8)! return resultString
In questo codice, impostiamo il kSecReturnData
parametro a kCFBooleanTrue
. kSecReturnData
significa che i dati effettivi dell'articolo saranno restituiti. Un'opzione diversa potrebbe essere quella di restituire gli attributi (kSecReturnAttributes
) dell'articolo. La chiave richiede a CFBoolean
tipo che contiene le costanti kCFBooleanTrue
o kCFBooleanFalse
. Stiamo ambientando kSecMatchLimit
a kSecMatchLimitOne
in modo che venga restituito solo il primo elemento trovato nel portachiavi, anziché un numero illimitato di risultati.
Il portachiavi è anche il luogo consigliato per archiviare oggetti chiave pubblici e privati, ad esempio se l'app funziona e deve memorizzare EC o RSA SecKey
oggetti.
La differenza principale è che invece di dire al portachiavi di memorizzare una password, possiamo dirgli di memorizzare una chiave. In effetti, possiamo ottenere specifiche impostando i tipi di chiavi memorizzate, ad esempio se è pubblico o privato. Tutto ciò che è necessario fare è adattare la funzione di ricerca delle query per lavorare con il tipo di chiave che si desidera.
Le chiavi sono generalmente identificate usando un tag di dominio inverso come com.mydomain.mykey invece di nomi di servizio e account (poiché le chiavi pubbliche sono condivise apertamente tra diverse società o entità). Prenderemo le stringhe di servizio e account e le convertiremo in un tag Dati
oggetto. Ad esempio, il codice precedente è stato adattato per memorizzare un privato RSA SecKey
sarebbe simile a questo:
class func keyQuery (servizio: String, account: String) -> Dizionariolet tagString = "com.mydomain." + servizio + "." + account let tag = tagString.data (usando: .utf8)! // Memorizza come Dati, non come String let dictionary = [kSecClass as String: kSecClassKey, kSecAttrKeyType as String: kSecAttrKeyTypeRSA, kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked, kSecAttrApplicationTag as String: tag] as [String: Any ] return dictionary @discardableResult class func setKey (_ key: SecKey, service: String, account: String) -> Bool var status: OSStatus = -1 if! (service.isEmpty) &&! (account.isEmpty) deleteKey (servizio: servizio, account: account) var dizionario = keyQuery (servizio: servizio, account: account) dizionario [kSecValueRef as String] = stato chiave = SecItemAdd (dizionario come CFDictionary, nil); status di ritorno == errSecSuccess @discardableResult class func deleteKey (servizio: String, account: String) -> Bool var status: OSStatus = -1 if! (service.isEmpty) &&! (account.isEmpty) let dictionary = keyQuery (servizio: servizio, account: account) stato = SecItemDelete (dizionario come CFDictionary); status restituito == errSecSuccess chiave funzione class (servizio: String, account: String) -> SecKey? var item: CFTypeRef? if! (service.isEmpty) &&! (account.isEmpty) var dictionary = keyQuery (servizio: servizio, account: account) dizionario [kSecReturnRef as String] = kCFBooleanTrue dictionary [kSecMatchLimit as String] = kSecMatchLimitOne SecItemCopyMatching (dizionario come CFDictionary, &articolo); restituisci oggetto come! SecKey?
Articoli protetti con kSecAttrAccessibleWhenUnlocked
flag sono sbloccati solo quando il dispositivo è sbloccato, ma si basa sull'utente che ha un passcode o Touch ID impostato in primo luogo.
Il applicationPassword
le credenziali consentono di proteggere gli oggetti nel portachiavi utilizzando una password aggiuntiva. In questo modo, se l'utente non dispone di un passcode o di un ID Touch, gli articoli saranno comunque sicuri e aggiungerà un ulteriore livello di sicurezza se dispongono di un set di passcode.
Come scenario di esempio, dopo che l'app si autentica con il tuo server, il tuo server potrebbe restituire la password tramite HTTPS necessaria per sbloccare l'elemento portachiavi. Questo è il modo preferito di fornire quella password aggiuntiva. Hardcoding una password nel binario non è raccomandata.
Un altro scenario potrebbe essere quello di recuperare la password aggiuntiva da una password fornita dall'utente nella tua app; tuttavia, questo richiede più lavoro per proteggere correttamente (usando PBKDF2). Cercheremo di proteggere le password fornite dagli utenti nel prossimo tutorial.
Un altro uso di una password per l'applicazione è per la memorizzazione di una chiave sensibile, ad esempio quella che non si desidera venga esposta solo perché l'utente non ha ancora impostato un passcode.
applicationPassword
è disponibile solo su iOS 9 e versioni successive, quindi avrai bisogno di un fallback che non usi applicationPassword
se scegli come target versioni iOS ridotte. Per utilizzare il codice, dovrai aggiungere quanto segue alla tua intestazione di bridging:
#importare#importare
Il seguente codice imposta una password per la query Dizionario
.
se #available (iOS 9.0, *) // Usa questo al posto di kSecAttrAccessible per la query var error: Unmanaged? let accessControl = SecAccessControlCreateWithFlags (kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, SecAccessControlCreateFlags.applicationPassword, & error) if accessControl! = nil dizionario [kSecAttrAccessControl as String] = accessControl lascia localAuthenticationContext = LAContext.init () lascia theApplicationPassword = "passwordFromServer" .data (utilizzando: String .Encoding.utf8)! localAuthenticationContext.setCredential (theApplicationPassword, type: LACredentialType.applicationPassword) dictionary [kSecUseAuthenticationContext as String] = localAuthenticationContext
Si noti che abbiamo impostato kSecAttrAccessControl
sul Dizionario
. Questo è usato al posto di kSecAttrAccessible
, che era precedentemente impostato nel nostro passwordQuery
metodo. Se provi ad usare entrambi, otterrai un OSStatus
-50
errore.
A partire da iOS 8, è possibile memorizzare i dati nel portachiavi a cui è possibile accedere solo dopo che l'utente si è autenticato correttamente sul dispositivo con Touch ID o un passcode. Quando è il momento per l'autenticazione dell'utente, Touch ID avrà la priorità se è impostato, altrimenti viene visualizzata la schermata del passcode. Il salvataggio nel portachiavi non richiederà all'utente di autenticarsi, ma il recupero dei dati lo farà.
È possibile impostare un elemento portachiavi per richiedere l'autenticazione dell'utente fornendo un oggetto controllo accessi impostato su .userPresence
. Se non è impostato alcun passcode, allora qualsiasi richiesta keychain con .userPresence
avrà esito negativo.
if #available (iOS 8.0, *) let accessControl = SecAccessControlCreateWithFlags (kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, nil) se accessControl! = nil dizionario [kSecAttrAccessControl as String] = accessControl
Questa funzione è utile quando vuoi assicurarti che la tua app sia utilizzata dalla persona giusta. Ad esempio, sarebbe importante che l'utente effettui l'autenticazione prima di poter accedere a un'app di banking. Ciò proteggerà gli utenti che hanno lasciato il loro dispositivo sbloccato, in modo che non sia possibile accedere al sistema bancario.
Inoltre, se non si dispone di un componente lato server all'app, è possibile utilizzare questa funzione per eseguire invece l'autenticazione sul lato dispositivo.
Per la query di caricamento, è possibile fornire una descrizione del motivo per cui l'utente deve autenticarsi.
dizionario [kSecUseOperationPrompt as String] = "Autentica per recuperare x"
Quando si recuperano i dati con SecItemCopyMatching ()
, la funzione mostrerà l'interfaccia utente di autenticazione e attenderà che l'utente utilizzi Touch ID o inserisca il passcode. Da SecItemCopyMatching ()
bloccherà fino a quando l'utente non avrà completato l'autenticazione, sarà necessario chiamare la funzione da un thread in background per consentire al thread principale dell'interfaccia utente di rimanere reattivo.
DispatchQueue.global (). Async status = SecItemCopyMatching (dizionario come CFDictionary e passwordData) se stato == errSecSuccess if let retrievedData = passwordData as? Data DispatchQueue.main.async // ... ripassa il resto del lavoro sul thread principale
Di nuovo, stiamo ambientando kSecAttrAccessControl
sulla query Dizionario
. Dovrai rimuovere kSecAttrAccessible
, che era precedentemente impostato nel nostro passwordQuery
metodo. Usando entrambi contemporaneamente si otterrà un OSStatus
-50 errore.
In questo articolo, hai avuto un tour dell'API Keychain Services. Insieme all'API di protezione dei dati che abbiamo visto nel post precedente, l'uso di questa libreria è parte delle migliori pratiche per la protezione dei dati.
Tuttavia, se l'utente non ha un passcode o un Touch ID sul dispositivo, non esiste alcuna crittografia per entrambi i framework. Poiché le API Keychain Services e Data Protection sono comunemente utilizzate dalle app iOS, a volte vengono prese di mira dagli autori di attacchi, specialmente su dispositivi jailbreak. Se la tua app non funziona con informazioni altamente sensibili, questo potrebbe essere un rischio accettabile. Mentre iOS aggiorna costantemente la sicurezza dei framework, siamo ancora in balia degli utenti che aggiornano il sistema operativo, utilizzando un passcode forte e non eseguendo il jailbreak del loro dispositivo.
Il portachiavi è pensato per piccole porzioni di dati e potresti avere una maggiore quantità di dati per garantire che sia indipendente dall'autenticazione del dispositivo. Mentre gli aggiornamenti di iOS aggiungono alcune fantastiche nuove funzionalità come la password dell'applicazione, potresti comunque aver bisogno di supportare versioni iOS ridotte e avere comunque una forte sicurezza. Per alcuni di questi motivi, puoi decidere di crittografare i dati tu stesso.
L'ultimo articolo di questa serie tratta la crittografia dei dati dell'utente tramite la crittografia AES e, sebbene sia un approccio più avanzato, consente di avere il pieno controllo su come e quando i dati vengono crittografati.
Quindi rimanete sintonizzati. E nel frattempo, controlla alcuni dei nostri altri post sullo sviluppo di app per iOS!