談談iOS Keychain的使用

iOS的keychain可以說是系統裏唯一可以做到安全可靠存儲應用敏感數據並且可以在應用卸載或重新安裝時仍然保留其數據的地方。當使用itunes進行數據備份時,每個應用程序在keychain裏的數據都會得到備份,而且備份的數據是經過加密的,所以無論數據存儲在哪裏,都十分安全。鑑於keychain的這些特性,使得它成爲開發者存儲應用敏感數據的首選,應用程序常見的敏感數據通常有密碼,祕鑰等等。

 

然而,開發者面臨的一個問題是,iOS sdk裏提供的keychain接口十分隱晦難用,初學者在理解及應用上遇到了不小的麻煩,甚至Apple官方提供的keychain示例代碼GenericKeychain也都十分難讀懂。因此,本文試圖將keychain的使用進行一下梳理,讓初學者能夠了解其基本使用。


準備工作

準備使用keychain接口前,首先需要在xcode里加入Security.framework,然後在代碼里加入頭文件 "#import <Security/Security.h>"。注意,如果你使用的是object-c,Security.framework裏的接口都是C語言風格接口而不是object-c語言風格的接口。此外,Security.framework只工作在實際設備上,無法在模擬器上使用。


Keychain概覽

Keychain裏可以存儲若干條目(item),每個條目都屬於某一個類別(class),以下是常見的幾種類別:

  • kSecClassInternetPassword

    屬於該類別的條目往往用來存儲上網登錄密碼,遠程服務器密碼等

  • kSecClassGenericPassword

    存儲一些通用的密碼,比如數據庫密碼,***連接的密碼等等

  • kSecClassCertificate, kSecClassKey和kSecClassIdentity

    這三類條目往往用於建立基於證書,祕鑰和公鑰系統的安全連接。


條目類別是一個條目最基本的屬性,每個存入keychain的條目,都需要爲它制定一個類別。

操作keychain常見的3個方法

  • SecItemAdd 

    添加新條目到keychain.

  • SecItemUpdate

    修改一個已存在keychain裏的條目

  • SecItemCopyMatching

    查詢keychain裏的條目並且讀取條目信息


下面用一張摘取自蘋果官方網站的一張圖闡述一下一個常見keychain的操作流程,該圖以一個應用連接ftp服務器爲例

keychain.jpg


在上圖中,應用大概流程是這樣的:應用開始連接ftp服務器,首先查詢keychain是否存在服務器密碼,如果存在,那麼直接取出密碼登陸;如果不存在,那麼應用彈出對話框要求用戶輸入,然後進行登陸,如果登陸成功,那麼存儲密碼到keychain。


Keychain接口參數

所有對keychain接口的操作,參數的傳遞基本上都用到字典,將所需要的參數放入字典,然後將字典傳遞給keychain接口。上面提到的條目類別,就是一個參數,除此之外,操作條目往往還需要條目ID,條目所屬服務名,條目所屬賬戶名等。這些參數的名稱如下:

kSecClass:條目類別

kSecAttrGeneric:條目id

kSecAttrService:條目所屬服務

kSecAttrAccount:條目所屬賬戶名

其中kSecAttrService和kSecAttrAccount在整個keychain裏必須唯一,不能重名。

 

Keychain條目查詢

使用SecItemCopyMatching進行查詢,查詢時,我們需要指明要查詢條目的類別(kSecClass),條目的id(kSecAttrGeneric),條目所屬的服務和賬戶(kSecAttrService,kSecAttrAccount)。此外,我們還可以設置其他一些查詢條件,比如返回條目的數量(kSecMatchLimit),返回條目的數據類型,比如:

kSecReturnData:返回條目所存儲的數據,返回值類型是CFDataRef

kSecReturnAttributes:返回該條目的屬性,返回值是字典類型CFDictionaryRef

kSecReturnRef:返回條目的引用,根據條目所屬類別,返回值類型可能是:SecKeychainItemRef, SecKeyRef,SecCertificateRef, SecIdentityRef.

kSecReturnPersistentRef:返回條目的引用,返回值類型是CFDataRef

 

如下代碼示例用來查詢存儲在keychain裏的密碼

//建立詞典,用來傳遞參數

NSMutableDictionary *dictionary = [[NSMutableDictionary dictionary];
 
//設置條目類別,我們用該條目存儲普通密碼,所以設置成kSecClassGenericPassword
[dictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 
//設置條目的id,比如“MyPasswordForFtp",條目id必須時NSDate,而不是NSString
NSString *itemIDString = @"MyPasswordForFtp";
NSData *itemID= [itemIDString dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:itemIDforKey:(id)kSecAttrGeneric];
 
//設置條目所屬的服務和賬戶,爲了避免重名,我們使用常見的反轉域名規則,比如com.mykeychain.ftppassword
NSString *account = @"com.mykeychain.ftppassword";
NSString *service = @"com.mykeychain.ftppassword";
[dictionary setObject:account forKey:(id)kSecAttrAccount];
[dictionary setObject:service forKey:(id)kSecAttrService];
 
//設置查詢條件,只返回一個條目
[dictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
 
//設置查詢條件,返回條目存儲的數據 (kSecReturnData == True)
[dictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
 
//開始查詢
NSData *result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)dictionary , (CFTypeRef *)&result);


Keychain條目添加

使用SecItemAdd()進行條目添加,我們需要指明新條目的類別(kSecClass),新條目的id(kSecAttrGeneric),新條目所屬的服務和賬戶(kSecAttrService,kSecAttrAccount),此外,還需要指明該條目所存儲的數據(kSecValueData),否則,沒有數據的條目就沒有任何存入keychain的意義了。

請看如下代碼示例:

//建立詞典,用來傳遞參數
NSMutableDictionary *dictionary = [[NSMutableDictionary dictionary];
 
//設置條目類別,我們用該條目存儲普通密碼,所以設置成kSecClassGenericPassword
[dictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 
//設置條目的id,比如“MyPasswordForFtp",條目id必須時NSDate,而不是NSString
NSString *itemIDString = @"MyPasswordForFtp";
NSData *itemID= [itemIDString dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:itemIDforKey:(id)kSecAttrGeneric];
 
//設置條目所屬的服務和賬戶,爲了避免重名,我們使用常見的反轉域名規則,比如com.mykeychain.ftppassword
NSString *account = @"com.mykeychain.ftppassword";
NSString *service = @"com.mykeychain.ftppassword";
[dictionary setObject:account forKey:(id)kSecAttrAccount];
[dictionary setObject:service forKey:(id)kSecAttrService];
 
//設置條目數據,條目數據時NSDate
NSString *password = @"123456";
NSData *itemData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:itemData forKey:(id)kSecValueData];
 
//添加條目到keychain
OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL);


keychain條目的修改

使用SecItemUpdate()進行條目修改,需要傳入兩個詞典,第一個詞典用來查詢條目,第二個詞典用來傳遞修改後的新值。查詢條目所用的詞典設置請看上述Keychain條目查詢章節,更新值所用詞典裏設置要更新的參數和值。這裏請注意的是,查詢詞典裏不能設置任何查詢條件,比如kSecMatchLimit和kSecReturnData,kSecReturnAttributes等,否則會出錯。因爲這裏的查詢是爲了更新數據,是以寫入爲目的,不是真正的查詢和讀取條目,所以設置這些和讀取有關的條件是無意義的,系統會認爲是和安全有關的錯誤,會返回-50(errSecParam)


下面代碼用來更新密碼,要更新的條目id爲“MyPasswordForFtp”的條目

//建立查詢字典
NSMutableDictionary *searchDictionary = [[NSMutableDictionary dictionary];
 
//設置查詢字典
[searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 
NSString *itemIDString = @"MyPasswordForFtp";
NSData *itemID= [itemIDString dataUsingEncoding:NSUTF8StringEncoding];
[searchDictionary setObject:itemIDforKey:(id)kSecAttrGeneric];
 
NSString *account = @"com.mykeychain.ftppassword";
NSString *service = @"com.mykeychain.ftppassword";
[searchDictionary setObject:account forKey:(id)kSecAttrAccount];
[searchDictionary setObject:service forKey:(id)kSecAttrService];
 
// 建立更新字典
NSMutableDictionary *updateDictionary = [[NSMutableDictionary dictionary];
 
// 設置要更新的新密碼
NSString* newPassword = @"654321";
NSData *passwordData = [newPassword dataUsingEncoding:NSUTF8StringEncoding];
[updateDictionary setObject:passwordData forKey:(id)kSecValueData];
 
//進行更新
OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary, (CFDictionaryRef)updateDictionary);

 

keychain接口常見錯誤

SecItemUpdate 返回-50(errSecParam):請檢查查詢詞典裏是否存在讀取條件,比如kSecReturnData,kSecMatchLimit等

SecItemAdd 返回-25299 (errSecDuplicateItem: 請檢查kSecAttrAccount和kSecAttrService是否已經存在於keychain中,請嘗試設置其他值避免重複



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章