iOS安全-鑰匙串服務(iOS Keychain Services Tasks)

本文描述iOS中基本的鑰匙串訪問,內容整理于蘋果官方文檔。
本文主要講述以下內容:

  • 鑰匙串中添加一個條目
  • 鑰匙串中查找條目
  • 獲取鑰匙串條目中的屬性和數據
  • 改變鑰匙串條目中的屬性和數據

注意:在iPhone上,鑰匙鏈的訪問權限取決于簽名應用程序的描述文件。在應用程序版本中務必要一直使用相同的描述文件。

向應用程序中添加鑰匙串服務

大多數iOS應用程序使用鑰匙串只是向鑰匙串中添加一個密碼,修改現有的鑰匙串條目,或者在需要的時候檢索一個密碼。鑰匙串服務提供了以下方法來完成這些任務:

  • SecItemAdd 向鑰匙串中添加一個條目
  • SecItemUpdate 更改鑰匙串中已有的條目
  • SecItemCopyMatching 查找鑰匙串條目并提取信息

下圖展示了應用程序如何使用這些函數來訪問互聯網的FTP服務器的流程圖。

使用iPhone Keychain Services 訪問網絡服務器流程圖

應用程序用戶從選擇一個文件傳輸協議(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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,826評論 18 139
  • Ubuntu的發音 Ubuntu,源于非洲祖魯人和科薩人的語言,發作 oo-boon-too 的音。了解發音是有意...
    螢火蟲de夢閱讀 99,456評論 9 467
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,074評論 6 13
  • 本文是Medusa和Hydra快速入門手冊的第二部分,第一部分的傳送門這兩篇也是后續爆破篇的一部分,至于字典,放在...
    LinuxSelf閱讀 2,883評論 0 4
  • 一路走來,只覺得滿滿的感恩。教會我的,受益匪淺。在路上的,感慨萬千。 皎潔如雪的月下,細數撲簌而過的回憶。絲絲侵入...
    三石三味閱讀 329評論 2 23