iOS
Brelok do kluczy
Szukaj…
Składnia
- kSecClassGenericPassword // Klucz wartości reprezentujący hasło inne niż internetowe
- kSecClassInternetPassword // Klucz wartości reprezentujący hasło internetowe
- kSecClassCertificate // Klucz wartości reprezentujący certyfikat
- kSecClassCertificate // Klucz wartości reprezentujący klucz
- kSecClassIdentity // Klucz wartości reprezentujący tożsamość, który jest certyfikatem plus klucz
Uwagi
iOS przechowuje prywatne informacje, takie jak hasła, klucze szyfrowania, certyfikaty i tożsamości w bezpiecznym obszarze przechowywania zwanym pęku kluczy. Ten obszar pamięci jest zarządzany całkowicie przez koprocesor o nazwie Secure Enclave, który jest wbudowany w procesor aplikacji. Ponieważ brelok jest w piaskownicy w systemie iOS, elementy pęku kluczy mogą być pobierane tylko przez aplikację, która je tam umieściła.
W niektórych przypadkach musisz włączyć udostępnianie pęku kluczy w funkcjach Xcode, aby uniknąć błędów.
W celu interakcji z pękiem kluczy używamy frameworka zwanego Keychain Services. Aby uzyskać więcej informacji, zobacz Podręcznik programowania pęku kluczy Apple .
Ponieważ Brelok Services jest poniżej Foundation
poziomie, jest on ograniczony do korzystania CoreFoundation
typy. W rezultacie większość obiektów jest wewnętrznie reprezentowana jako CFDictionary
trzymające CFString
s jako klucze i różne typy CoreFoundation
jako swoje wartości.
Chociaż usługi pęku kluczy są zawarte w ramach Security
, importowanie Foundation
jest zwykle dobrą opcją, ponieważ zawiera funkcje pomocnicze w wewnętrznej bazie danych.
Ponadto, jeśli nie chcesz bezpośrednio zajmować się Usługami pęku kluczy, Apple udostępnia przykładowy projekt Ogólnego pęku kluczy Swift, który zapewnia typy Swift korzystające z Usług pęku kluczy za kulisami.
Dodawanie hasła do pęku kluczy
Każdy element pęku kluczy jest najczęściej reprezentowany jako CFDictionary
. Możesz jednak po prostu użyć NSDictionary
w Objective-C i skorzystać z mostkowania, lub w Swift możesz użyć Dictionary
i jawnie CFDictionary
do CFDictionary
.
Możesz zbudować hasło za pomocą następującego słownika:
Szybki
var dict = [String : AnyObject]()
Po pierwsze, potrzebujesz pary klucz / wartość, która informuje pęku kluczy, że to hasło. Należy zauważyć, że ponieważ nasz klucz dict jest String
musimy rzucać żadnych CFString
do String
wyraźnie w Swift 3. CFString nie może być stosowany jako klucz do szybkiej Dictionary, ponieważ nie jest Hashable.
Szybki
dict[kSecClass as String] = kSecClassGenericPassword
Następnie nasze hasło może mieć szereg atrybutów, które je opisają i pomogą nam je później znaleźć. Oto lista atrybutów ogólnych haseł .
Szybki
// 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
Wreszcie potrzebujemy naszych rzeczywistych danych prywatnych. Pamiętaj, aby nie przechowywać tego zbyt długo w pamięci. To musi być CFData
.
Szybki
dict[kSecValueData as String] = "my_password!!".data(using: .utf8) as! CFData
Wreszcie funkcja dodawania usług pęku kluczy chce wiedzieć, w jaki sposób powinna zwrócić nowo skonstruowany element pęku kluczy. Ponieważ nie powinieneś długo trzymać danych w pamięci, oto jak możesz zwrócić atrybuty:
Szybki
dict[kSecReturnAttributes as String] = kCFBooleanTrue
Teraz zbudowaliśmy nasz przedmiot. Dodajmy to:
Szybki
var result: AnyObject?
let status = withUnsafeMutablePointer(to: &result) {
SecItemAdd(dict as CFDictionary, UnsafeMutablePointer($0))
}
let newAttributes = result as! Dictionary<String, AnyObject>
To powoduje, że nowe atrybuty dyktują wewnątrz result
. SecItemAdd
pobiera skonstruowany słownik, a także wskaźnik do miejsca, w którym chcielibyśmy uzyskać nasz wynik. Następnie funkcja zwraca OSStatus
wskazujący powodzenie lub kod błędu. Kody wyników są opisane tutaj .
Znajdowanie hasła w pęku kluczy
Aby zbudować zapytanie, musimy je przedstawić jako CFDictionary
. Możesz także użyć NSDictionary
w Objective-C lub Dictionary
w Swift i CFDictionary
do CFDictionary
.
Potrzebujemy klucza klasy:
Szybki
var dict = [String : AnyObject]()
dict[kSecClass as String] = kSecClassGenericPassword
Następnie możemy określić atrybuty, aby zawęzić nasze wyszukiwanie:
Szybki
// 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
Możemy również określić specjalne klucze modyfikujące wyszukiwanie opisane tutaj .
Na koniec musimy powiedzieć, w jaki sposób chcielibyśmy zwrócić nasze dane. Poniżej poprosimy o zwrócenie samego hasła prywatnego jako obiektu CFData
:
Szybki
dict[kSecReturnData as String] = kCFBooleanTrue
Teraz wyszukajmy:
Szybki
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)!
Tutaj SecItemCopyMatching
pobiera słownik zapytań i wskaźnik do miejsca, w którym chcesz przejść do wyniku. Zwraca OSStatus
z kodami wynikowymi. Oto możliwości.
Aktualizowanie hasła w pęku kluczy
Jak zwykle potrzebujemy najpierw CFDictionary
do przedstawienia elementu, który chcemy zaktualizować. Musi on zawierać wszystkie stare wartości elementu, w tym stare dane prywatne. Następnie pobiera CFDictionary
dowolnych atrybutów lub samych danych, które chcesz zmienić.
Najpierw stwórzmy klucz klasy i listę atrybutów. Te atrybuty mogą zawęzić nasze wyszukiwanie, ale jeśli chcesz je zmienić, musisz dołączyć wszelkie atrybuty i stare wartości.
Szybki
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
Teraz musimy dodać stare dane:
Szybki
dict[kSecValueData as String] = "my_password!!".data(using: .utf8) as! CFData
Teraz utwórzmy te same atrybuty, ale inne hasło:
Szybki
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
Teraz przekazujemy go do usług pęku kluczy:
Szybki
let status = SecItemUpdate(dict as CFDictionary, newDict as CFDictionary)
SecItemUpdate
zwraca kod stanu. Wyniki opisano tutaj .
Usuwanie hasła z pęku kluczy
Potrzebujemy tylko jednej rzeczy, aby usunąć element z pęku kluczy: CFDictionary
z atrybutami opisującymi elementy do usunięcia. Wszelkie elementy, które pasują do słownika zapytań, zostaną trwale usunięte, więc jeśli zamierzasz usunąć tylko jeden element, pamiętaj, aby podać szczegółowe informacje dotyczące zapytania. Jak zawsze możemy użyć NSDictionary
w Objective-C lub w Swift możemy użyć Dictionary
a następnie rzutować na CFDictionary
.
Słownik zapytań, w tym kontekście zawiera wyłącznie klucz klasy opisujący, czym jest element i atrybuty opisujące informacje o elemencie. Uwzględnienie ograniczeń wyszukiwania, takich jak kSecMatchCaseInsensitive
jest niedozwolone.
Szybki
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
A teraz możemy go po prostu usunąć:
Szybki
let status = SecItemDelete(dict as CFDictionary)
SecItemDelete
zwraca OSStatus
. Kody wyników są opisane tutaj .
Pęku kluczy Dodawanie, aktualizowanie, usuwanie i znajdowanie operacji za pomocą jednego pliku.
Pęku kluczy. 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
Kontrola dostępu do pęku kluczy (TouchID z rezerwowym hasłem)
Keychain pozwala zapisywać elementy ze specjalnym atrybutem SecAccessControl, który pozwoli uzyskać element z pęku kluczy dopiero po uwierzytelnieniu użytkownika za pomocą Touch ID (lub kodu dostępu, jeśli taka awaria jest dozwolona). Aplikacja jest powiadamiana tylko o tym, czy uwierzytelnienie powiodło się, czy nie, cały interfejs użytkownika jest zarządzany przez system iOS.
Najpierw należy utworzyć obiekt SecAccessControl:
Szybki
let error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .userPresence, &error) else {
fatalError("Something went wrong")
}
Następnie dodaj go do słownika za pomocą klucza kSecAttrAccessControl (który wyklucza się wzajemnie z kluczem kSecAttrAccessible, którego używałeś w innych przykładach):
Szybki
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
I zapisz to tak jak wcześniej:
Szybki
let lastResultCode = SecItemAdd(query as CFDictionary, nil)
Aby uzyskać dostęp do przechowywanych danych, po prostu zapytaj Keychain o klucz. Usługi pęku kluczy przedstawią użytkownikowi okno dialogowe uwierzytelnienia i zwrócą dane lub zero w zależności od tego, czy dostarczono odpowiedni odcisk palca, czy pasowano hasło.
Opcjonalnie można podać ciąg zachęty:
Szybki
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))
}
Zwróć uwagę, że status
będzie err
jeśli użytkownik odmówi, anuluje lub nie powiedzie się autoryzacja.
Szybki
if status == noErr {
let password = String(data: queryResult as! Data, encoding: .utf8)!
print("Password: \(password)")
} else {
print("Authorization not passed")
}