Ricerca…
Sintassi
- kSecClassGenericPassword // Una chiave di valore che rappresenta una password non Internet
- kSecClassInternetPassword // Una chiave di valore che rappresenta una password internet
- kSecClassCertificate // Una chiave di valore che rappresenta un certificato
- kSecClassCertificate // Una chiave di valore che rappresenta una chiave
- kSecClassIdentity // Una chiave di valore che rappresenta un'identità, che è un certificato più una chiave
Osservazioni
iOS memorizza informazioni private come password, chiavi di crittografia, certificati e identità in un'area di archiviazione protetta chiamata Keychain. Questa area di memoria è gestita completamente da un co-processore chiamato Secure Enclave, che è incorporato all'interno del processore dell'applicazione. Poiché il portachiavi è in modalità sandbox su iOS, gli elementi portachiavi possono essere recuperati solo dall'applicazione che li ha messi lì in primo luogo.
In alcuni casi è necessario attivare Condivisione dei portachiavi nelle funzionalità Xcode per evitare errori.
Per interagire con il portachiavi, utilizziamo il framework ac chiamato Keychain Services. Per ulteriori informazioni, consulta la Guida alla programmazione dei servizi Keychain di Apple .
Poiché i servizi portachiavi sono al di sotto del livello Foundation
, è limitato all'utilizzo di tipi CoreFoundation
. Di conseguenza, la maggior parte degli oggetti sono internamente rappresentati come CFDictionary
che CFString
s come le loro chiavi e una varietà di tipi CoreFoundation
come i loro valori.
Mentre i Keychain Services sono inclusi come parte del framework Security
, l'importazione di Foundation
è di solito una buona opzione poiché include alcune funzioni di supporto nel back-end.
Inoltre, se non vuoi gestire direttamente i Keychain Services, Apple fornisce il progetto di esempio Generic Keychain Swift che fornisce i tipi Swift che utilizzano i servizi Keychain dietro le quinte.
Aggiunta di una password al portachiavi
Ogni elemento portachiavi è spesso rappresentato come un CFDictionary
. Tuttavia, è possibile utilizzare semplicemente NSDictionary
in Objective-C e sfruttare il bridging, oppure in Swift è possibile utilizzare Dictionary
e CFDictionary
espressamente a CFDictionary
.
È possibile costruire una password con il seguente dizionario:
veloce
var dict = [String : AnyObject]()
Innanzitutto, è necessaria una coppia chiave / valore che permetta al Portachiavi di sapere che questa è una password. Si noti che poiché la nostra chiave dict è una String
è necessario CFString
qualsiasi CFString
a una String
esplicito in Swift 3. CFString non può essere utilizzato come chiave per un dizionario Swift perché non è selezionabile.
veloce
dict[kSecClass as String] = kSecClassGenericPassword
Successivamente, la nostra password potrebbe avere una serie di attributi per descriverla e aiutarci a trovarla in seguito. Ecco una lista di attributi per le password generiche .
veloce
// The password will only be accessible when the device is unlocked
dict[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlocked
// Label may help you find it later
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
// Service name
dict[kSecAttrService as String] = "MyService" as CFString
Infine, abbiamo bisogno dei nostri dati privati reali. Assicurati di non tenerlo in memoria per troppo tempo. Questo deve essere CFData
.
veloce
dict[kSecValueData as String] = "my_password!!".data(using: .utf8) as! CFData
Infine, la funzione di aggiunta dei servizi Keychain vuole sapere come deve restituire l'elemento portachiavi di nuova costruzione. Dal momento che non dovresti tenere i dati molto a lungo nella memoria, ecco come puoi restituire gli attributi:
veloce
dict[kSecReturnAttributes as String] = kCFBooleanTrue
Ora abbiamo costruito il nostro oggetto. Aggiungiamolo:
veloce
var result: AnyObject?
let status = withUnsafeMutablePointer(to: &result) {
SecItemAdd(dict as CFDictionary, UnsafeMutablePointer($0))
}
let newAttributes = result as! Dictionary<String, AnyObject>
Questo pone i nuovi attributi dettati all'interno del result
. SecItemAdd
prende il dizionario che abbiamo costruito e un puntatore a dove vorremmo il nostro risultato. La funzione restituisce quindi un OSStatus
indica il successo o un codice di errore. I codici dei risultati sono descritti qui .
Trovare una password nel portachiavi
Per costruire una query, dobbiamo rappresentarla come un CFDictionary
. Puoi anche usare NSDictionary
in Objective-C o Dictionary
in Swift e CFDictionary
a CFDictionary
.
Abbiamo bisogno di una chiave di classe:
veloce
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
Successivamente, possiamo specificare gli attributi per restringere la nostra ricerca:
veloce
// Label
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
// Service name
dict[kSecAttrService as String] = "MyService" as CFString
Possiamo anche specificare i tasti di modifica di ricerca speciali qui descritti.
Infine, dobbiamo dire come vorremmo che i nostri dati fossero restituiti. Di seguito, ti chiederemo di restituire solo la password privata come oggetto CFData
:
veloce
dict[kSecReturnData as String] = kCFBooleanTrue
Ora, cerchiamo:
veloce
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(dict as CFDictionary, UnsafeMutablePointer($0))
}
// Don't keep this in memory for long!!
let password = String(data: queryResult as! Data, encoding: .utf8)!
Qui, SecItemCopyMatching
un dizionario di query e un puntatore a cui desideri ottenere il risultato. Restituisce un OSStatus
con un codice di risultato. Ecco le possibilità.
Aggiornamento di una password nel portachiavi
Come al solito, abbiamo prima bisogno di un CFDictionary
per rappresentare l'elemento che vogliamo aggiornare. Questo deve contenere tutti i vecchi valori per l'elemento, inclusi i vecchi dati privati. Quindi richiede un CFDictionary
di qualsiasi attributo o dei dati stessi che si desidera modificare.
Quindi, per prima cosa, costruiamo una chiave di classe e una lista di attributi. Questi attributi possono restringere la nostra ricerca ma devi includere tutti gli attributi e i vecchi valori se li cambierai.
veloce
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
// Label
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
Ora dobbiamo aggiungere i vecchi dati:
veloce
dict[kSecValueData as String] = "my_password!!".data(using: .utf8) as! CFData
Ora creiamo gli stessi attributi ma una password diversa:
veloce
var newDict = [String : AnyObject]()
newDict[kSecClass as String] = kSecClassGenericPassword
// Label
newDict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
newDict[kSecAttrAccount as String] = "My Name" as CFString
// New password
newDict[kSecValueData as String] = "new_password!!".data(using: .utf8) as! CFData
Ora, passiamo semplicemente ai servizi portachiavi:
veloce
let status = SecItemUpdate(dict as CFDictionary, newDict as CFDictionary)
SecItemUpdate
restituisce un codice di stato. I risultati sono descritti qui .
Rimozione di una password dal portachiavi
Abbiamo bisogno solo di una cosa per eliminare un oggetto dal Portachiavi: un CFDictionary
con attributi che descrivono gli elementi da eliminare. Tutti gli elementi che corrispondono al dizionario di query verranno eliminati in modo permanente, quindi se si intende eliminare un singolo elemento assicurarsi di essere specifici con la query. Come sempre, possiamo usare un NSDictionary
in Objective-C o in Swift possiamo usare un Dictionary
e quindi CFDictionary
a CFDictionary
.
Un dizionario di query, in questo contesto include esclusivamente una chiave di classe per descrivere l'elemento e gli attributi per descrivere le informazioni sull'elemento. L'inclusione di restrizioni di ricerca come kSecMatchCaseInsensitive
non è consentita.
veloce
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
// Label
dict[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
// Username
dict[kSecAttrAccount as String] = "My Name" as CFString
E ora possiamo semplicemente rimuoverlo:
veloce
let status = SecItemDelete(dict as CFDictionary)
SecItemDelete
restituisce un OSStatus
. I codici dei risultati sono descritti qui .
Portachiavi Aggiungi, Aggiorna, Rimuovi e Trova operazioni utilizzando un solo file.
Keychain.h
#import <Foundation/Foundation.h>
typedef void (^KeychainOperationBlock)(BOOL successfulOperation, NSData *data, OSStatus status);
@interface Keychain : NSObject
-(id) initWithService:(NSString *) service_ withGroup:(NSString*)group_;
-(void)insertKey:(NSString *)key withData:(NSData *)data withCompletion:(KeychainOperationBlock)completionBlock;
-(void)updateKey:(NSString*)key withData:(NSData*) data withCompletion:(KeychainOperationBlock)completionBlock;
-(void)removeDataForKey:(NSString*)key withCompletionBlock:(KeychainOperationBlock)completionBlock;
-(void)findDataForKey:(NSString*)key withCompletionBlock:(KeychainOperationBlock)completionBlock;
@end
Keychain.m
#import "Keychain.h"
#import <Security/Security.h>
@implementation Keychain
{
NSString * keychainService;
NSString * keychainGroup;
}
-(id) initWithService:(NSString *)service withGroup:(NSString*)group
{
self =[super init];
if(self) {
keychainService = [NSString stringWithString:service];
if(group) {
keychainGroup = [NSString stringWithString:group];
}
}
return self;
}
-(void)insertKey:(NSString *)key
withData:(NSData *)data
withCompletion:(KeychainOperationBlock)completionBlock
{
NSMutableDictionary * dict =[self prepareDict:key];
[dict setObject:data forKey:(__bridge id)kSecValueData];
[dict setObject:keychainService forKey:(id)kSecAttrService];
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dict, NULL);
if(errSecSuccess != status) {
DLog(@"Unable add item with key =%@ error:%d",key,(int)status);
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
}
if (status == errSecDuplicateItem) {
[self updateKey:key withData:data withCompletion:^(BOOL successfulOperation, NSData *updateData, OSStatus updateStatus) {
if (completionBlock) {
completionBlock(successfulOperation, updateData, updateStatus);
}
DLog(@"Found duplication item -- updating key with data");
}];
}
}
-(void)findDataForKey:(NSString *)key
withCompletionBlock:(KeychainOperationBlock)completionBlock
{
NSMutableDictionary *dict = [self prepareDict:key];
[dict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[dict setObject:keychainService forKey:(id)kSecAttrService];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict,&result);
if( status != errSecSuccess) {
DLog(@"Unable to fetch item for key %@ with error:%d",key,(int)status);
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
} else {
if (completionBlock) {
completionBlock(errSecSuccess == status, (__bridge NSData *)result, status);
}
}
}
-(void)updateKey:(NSString *)key
withData:(NSData *)data
withCompletion:(KeychainOperationBlock)completionBlock
{
NSMutableDictionary * dictKey =[self prepareDict:key];
NSMutableDictionary * dictUpdate =[[NSMutableDictionary alloc] init];
[dictUpdate setObject:data forKey:(__bridge id)kSecValueData];
[dictUpdate setObject:keychainService forKey:(id)kSecAttrService];
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)dictKey, (__bridge CFDictionaryRef)dictUpdate);
if( status != errSecSuccess) {
DLog(@"Unable to remove item for key %@ with error:%d",key,(int)status);
}
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
}
-(void)removeDataForKey:(NSString *)key
withCompletionBlock:(KeychainOperationBlock)completionBlock {
NSMutableDictionary *dict = [self prepareDict:key];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dict);
if( status != errSecSuccess) {
DLog(@"Unable to remove item for key %@ with error:%d",key,(int)status);
}
if (completionBlock) {
completionBlock(errSecSuccess == status, nil, status);
}
}
#pragma mark Internal methods
-(NSMutableDictionary*) prepareDict:(NSString *) key {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
[dict setObject:keychainService forKey:(__bridge id)kSecAttrService];
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
//This is for sharing data across apps
if(keychainGroup != nil) {
[dict setObject:keychainGroup forKey:(__bridge id)kSecAttrAccessGroup];
}
return dict;
}
@end
Controllo accesso portachiavi (TouchID con fallback password)
Il portachiavi consente di salvare gli articoli con l'attributo speciale SecAccessControl che consente di ottenere l'elemento dal Portachiavi solo dopo che l'utente sarà autenticato con Touch ID (o passcode se tale fallback è consentito). L'app viene notificata solo se l'autenticazione ha avuto successo o meno, l'intera UI è gestita da iOS.
In primo luogo, l'oggetto SecAccessControl deve essere creato:
veloce
let error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .userPresence, &error) else {
fatalError("Something went wrong")
}
Successivamente, aggiungilo al dizionario con la chiave kSecAttrAccessControl (che si esclude a vicenda con la chiave kSecAttrAccessible che hai utilizzato in altri esempi):
veloce
var dictionary = [String : Any]()
dictionary[kSecClass as String] = kSecClassGenericPassword
dictionary[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
dictionary[kSecAttrAccount as String] = "My Name" as CFString
dictionary[kSecValueData as String] = "new_password!!".data(using: .utf8) as! CFData
dictionary[kSecAttrAccessControl as String] = accessControl
E salvalo come hai fatto prima:
veloce
let lastResultCode = SecItemAdd(query as CFDictionary, nil)
Per accedere ai dati memorizzati, basta interrogare Keychain per una chiave. I servizi portachiavi presenteranno la finestra di autenticazione all'utente e restituiranno i dati o nil a seconda che sia stata fornita un'appropriata impronta digitale o che sia stato fornito il codice di accesso.
Facoltativamente, è possibile specificare una stringa di richiesta:
veloce
var query = [String: Any]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecAttrAccount as String] = "My Name" as CFString
query[kSecAttrLabel as String] = "com.me.myapp.myaccountpassword" as CFString
query[kSecUseOperationPrompt as String] = "Please put your fingers on that button" as CFString
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
Fai attenzione che lo status
sarà err
se l'utente rifiuta, ha cancellato o ha fallito l'autorizzazione.
veloce
if status == noErr {
let password = String(data: queryResult as! Data, encoding: .utf8)!
print("Password: \(password)")
} else {
print("Authorization not passed")
}