本文描述iOS中基本的鑰匙串訪問,內容整理于蘋果官方文檔。
本文主要講述以下內容:
- 鑰匙串中添加一個條目
- 鑰匙串中查找條目
- 獲取鑰匙串條目中的屬性和數據
- 改變鑰匙串條目中的屬性和數據
注意:在iPhone上,鑰匙鏈的訪問權限取決于簽名應用程序的描述文件。在應用程序版本中務必要一直使用相同的描述文件。
向應用程序中添加鑰匙串服務
大多數iOS應用程序使用鑰匙串只是向鑰匙串中添加一個密碼,修改現有的鑰匙串條目,或者在需要的時候檢索一個密碼。鑰匙串服務提供了以下方法來完成這些任務:
- SecItemAdd 向鑰匙串中添加一個條目
- SecItemUpdate 更改鑰匙串中已有的條目
- SecItemCopyMatching 查找鑰匙串條目并提取信息
下圖展示了應用程序如何使用這些函數來訪問互聯網的FTP服務器的流程圖。
應用程序用戶從選擇一個文件傳輸協議(FTP)服務器開始。應用程序調用SecItemCopyMatching,傳一個包含確定密鑰串條目的屬性的字典。如果鑰匙串里有密碼, 函數將密碼返回到應用程序,將它發送到FTP服務器對用戶進行身份驗證。如果身份驗證成功,結束。如果身份驗證失敗,應用程序顯示一個對話框,要求輸入戶名和密碼。
如果鑰匙串里沒有相應的密碼,SecItemCopyMatching返回errSecItemNotFound結果代碼。在這種情況下,應用程序顯示一個對話框,要求輸入戶名和密碼。(這個對話框還應該包括一個取消按鈕,為了避免流程圖變得過于復雜已經省略了)。
從用戶那里拿到密碼之后,該應用程序繼續到FTP服務器驗證用戶的身份。身份驗證成功時,應用程序可以假設用戶輸入的信息是有效的。應用程序然后顯示另一個對話框詢問用戶是否保存密碼鑰匙鏈。如果用戶選擇不,那么結束。如果用戶選擇是,則應用程序調用SecItemAdd函數(如果這是一個新的鑰匙串條目)或SecItemUpdate函數(更新現有的鑰匙串條目),然后結束。
以下代碼展示了一個可能使用鑰匙串服務功能典型的應用程序,為通用項目獲取和設置密碼。使用同樣的方法你可以獲取和設置鑰匙串條目屬性(如用戶名或服務名稱)。
以下代碼來自蘋果官方
注意引入Security.Framework
KeychainWrapper.h
#import <Foundation/Foundation.h>
#import <Security/Security.h>
@interface KeychainWrapper : NSObject{
NSMutableDictionary *keychainData;
NSMutableDictionary *genericPasswordQuery;
}
@property (nonatomic, strong) NSMutableDictionary *keychainData;
@property (nonatomic, strong) NSMutableDictionary *genericPasswordQuery;
- (void)mySetObject:(id)inObject forKey:(id)key;
- (id)myObjectForKey:(id)key;
- (void)resetKeychainItem;
@end
KeychainWrapper.m
//Unique string used to identify the keychain item:
static const UInt8 kKeychainItemIdentifier[] = "com.ios.doris";
@implementation KeychainWrapper
- (id)init
{
if ((self = [super init])) {
OSStatus keychainErr = noErr;
// Set up the keychain search dictionary:
genericPasswordQuery = [[NSMutableDictionary alloc] init];
// This keychain item is a generic password.
[genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword
forKey:(__bridge id)kSecClass];
// The kSecAttrGeneric attribute is used to store a unique string that is used
// to easily identify and find this keychain item. The string is first
// converted to an NSData object:
NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
length:strlen((const char *)kKeychainItemIdentifier)];
[genericPasswordQuery setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric];
// Return the attributes of the first match only:
[genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
// Return the attributes of the keychain item (the password is
// acquired in the secItemFormatToDictionary: method):
[genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue
forKey:(__bridge id)kSecReturnAttributes];
//Initialize the dictionary used to hold return data from the keychain:
CFMutableDictionaryRef outDictionary = nil;
// If the keychain item exists, return the attributes of the item:
keychainErr = SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery,
(CFTypeRef *)&outDictionary);
if (keychainErr == noErr) {
// Convert the data dictionary into the format used by the view controller:
self.keychainData = [self secItemFormatToDictionary:(__bridge_transfer NSMutableDictionary *)outDictionary];
} else if (keychainErr == errSecItemNotFound) {
// Put default values into the keychain if no matching
// keychain item is found:
[self resetKeychainItem];
if (outDictionary) CFRelease(outDictionary);
} else {
// Any other error is unexpected.
NSAssert(NO, @"Serious error.\n");
if (outDictionary) CFRelease(outDictionary);
}
}
return self;
}
存到鑰匙串:
- (void)mySetObject:(id)inObject forKey:(id)key
{
if (inObject == nil) return;
id currentObject = [keychainData objectForKey:key];
if (![currentObject isEqual:inObject])
{
[keychainData setObject:inObject forKey:key];
[self writeToKeychain];
}
}
從鑰匙串中取:
- (id)myObjectForKey:(id)key
{
return [keychainData objectForKey:key];
}
// Reset the values in the keychain item, or create a new item if it
// doesn't already exist:
重置鑰匙串中的數據,或者不存在是創建相應條目:
- (void)resetKeychainItem
{
if (!keychainData) //Allocate the keychainData dictionary if it doesn't exist yet.
{
self.keychainData = [[NSMutableDictionary alloc] init];
}
else if (keychainData)
{
// Format the data in the keychainData dictionary into the format needed for a query
// and put it into tmpDictionary:
NSMutableDictionary *tmpDictionary =
[self dictionaryToSecItemFormat:keychainData];
// Delete the keychain item in preparation for resetting the values:
OSStatus errorcode = SecItemDelete((__bridge CFDictionaryRef)tmpDictionary);
NSAssert(errorcode == noErr, @"Problem deleting current keychain item." );
}
// Default generic data for Keychain Item:
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrLabel];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrDescription];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrAccount];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrService];
[keychainData setObject:@"" forKey:(__bridge id)kSecAttrComment];
[keychainData setObject:@"" forKey:(__bridge id)kSecValueData];
}
// Implement the dictionaryToSecItemFormat: method, which takes the attributes that
// you want to add to the keychain item and sets up a dictionary in the format
// needed by Keychain Services:
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
// This method must be called with a properly populated dictionary
// containing all the right key/value pairs for a keychain item search.
// Create the return dictionary:
NSMutableDictionary *returnDictionary =
[NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the keychain item class and the generic attribute:
NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
length:strlen((const char *)kKeychainItemIdentifier)];
[returnDictionary setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric];
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Convert the password NSString to NSData to fit the API paradigm:
NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
[returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding]
forKey:(__bridge id)kSecValueData];
return returnDictionary;
}
從鑰匙串中取出數據轉為字典
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
// This method must be called with a properly populated dictionary
// containing all the right key/value pairs for the keychain item.
// Create a return dictionary populated with the attributes:
NSMutableDictionary *returnDictionary = [NSMutableDictionary
dictionaryWithDictionary:dictionaryToConvert];
// To acquire the password data from the keychain item,
// first add the search key and class attribute required to obtain the password:
[returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Then call Keychain Services to get the password:
CFDataRef passwordData = NULL;
OSStatus keychainError = noErr; //
keychainError = SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary,
(CFTypeRef *)&passwordData);
if (keychainError == noErr)
{
// Remove the kSecReturnData key; we don't need it anymore:
[returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];
// Convert the password to an NSString and add it to the return dictionary:
NSString *password = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)passwordData bytes]
length:[(__bridge NSData *)passwordData length] encoding:NSUTF8StringEncoding];
[returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
}
// Don't do anything if nothing is found.
else if (keychainError == errSecItemNotFound) {
NSAssert(NO, @"Nothing was found in the keychain.\n");
if (passwordData) CFRelease(passwordData);
}
// Any other error is unexpected.
else
{
NSAssert(NO, @"Serious error.\n");
if (passwordData) CFRelease(passwordData);
}
return returnDictionary;
}
寫到鑰匙串的具體實現:
- (void)writeToKeychain
{
CFDictionaryRef attributes = nil;
NSMutableDictionary *updateItem = nil;
// If the keychain item already exists, modify it:
if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery,
(CFTypeRef *)&attributes) == noErr)
{
// First, get the attributes returned from the keychain and add them to the
// dictionary that controls the update:
updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSDictionary *)attributes];
// Second, get the class value from the generic password query dictionary and
// add it to the updateItem dictionary:
[updateItem setObject:[genericPasswordQuery objectForKey:(__bridge id)kSecClass]
forKey:(__bridge id)kSecClass];
// Finally, set up the dictionary that contains new values for the attributes:
NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainData];
//Remove the class--it's not a keychain attribute:
[tempCheck removeObjectForKey:(__bridge id)kSecClass];
// You can update only a single keychain item at a time.
OSStatus errorcode = SecItemUpdate(
(__bridge CFDictionaryRef)updateItem,
(__bridge CFDictionaryRef)tempCheck);
NSAssert(errorcode == noErr, @"Couldn't update the Keychain Item." );
} else {
// No previous item found; add the new item.
// The new value was added to the keychainData dictionary in the mySetObject routine,
// and the other values were added to the keychainData dictionary previously.
// No pointer to the newly-added items is needed, so pass NULL for the second parameter:
OSStatus errorcode = SecItemAdd(
(__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:keychainData],
NULL);
NSAssert(errorcode == noErr, @"Couldn't add the Keychain Item." );
if (attributes) CFRelease(attributes);
}
}
@end
在這個示例中,通用屬性是用來創建一個獨一無二的字符串,可以用來輕松識別鑰匙串條目。你也可以使用標準的屬性,如服務名稱和用戶名。
運行調試:
詳細代碼和示例程序:https://github.com/lilufeng/KeychainDemo