本文主要整理下iOS密碼存儲的相關知識。
毫無疑問,密碼存儲是一個比較老生常談的問題,原先項目中使用的是SFHFKeychainUtils),不知道什麼原因使用的它。不過還算好用,沒出現什麼問題。
本著獵奇心理,在github上搜索ios keychain
關鍵字,找出了星星最多的前兩個開源包:
SSKeychain
UICKeyChainStore
當然使用就使用星星最多的SSKeychain,pod install下,就應用到了項目裡。但是使用了一段時間經常會發生errSecDefault問題
出於無奈,我開始看UICKeyChainStore,但它貌似不如SSKeychain,沒有提供account。又想起原先使用的SFHFKeychainUtils。
基於以上原因,也或許原先僅是拿過來使用,而沒有理解裡面的實現細節,故整理下:
蘋果官方:Keychain Services Programming Guide
蘋果官方: Keychain Services Reference
蘋果官方代碼
由於上方文檔,即有Mac OS X的,又有iOS的,僅摘抄下關於iOS的重要部分:
A keychain is an encrypted container that holds passwords for multiple applications and secure services. Keychains are secure storage containers, which means that when the keychain is locked, no one can access its protected contents. In OS X, users can unlock a keychain—thus providing trusted applications access to the contents—by entering a single master password. In iOS, each application always has access to its own keychain items; the user is never asked to unlock the keychain. Whereas in OS X any application can access any keychain item provided the user gives permission, in iOS an application can access only its own keychain items.
鑰匙鏈是一個加密的容器,它保存了多個程序和安全服務的密碼。鑰匙鏈是一個安全的存儲容器,這意味著沒有應用程序或服務在它鎖住的情況下,可以訪問它的內容。在Mac OS X下,用戶可以解鎖鑰匙鏈,因此可以通過輸入主密碼的形式提供給信任的應用程序訪問裡面的內容。在iOS中,每個應用程序只能訪問自己的鑰匙鏈項;用戶不能解鎖鑰匙鏈。相反在Mac OS X裡,用戶給予了許可,每個應用程序都能訪問任何一個鑰匙鏈項。在iOS裡,一個應用程序只能訪問自己的鑰匙鏈項。
Note: On iPhone, Keychain rights depend on the provisioning profile used to sign your application. Be sure to consistently use the same provisioning profile across different versions of your application.
注意:在iPhone中,鑰匙鏈依賴於 provisioning profile 文件來標識你的應用程序。確保應用程序在不同版本中一貫的使用同一個 provisioning profile 文件
In iOS,there is a single keychain accessible to applications. Although it stores the keychain items of all the applications on the system, an application can access only its own keychain items (with the possible exception of a keychain item for which the application that created it obtained a persistent reference).
在iOS裡,有一個單獨的鑰匙鏈供應用程序訪問。盡管他存儲了系統裡全部應用程序的鑰匙鏈項,一個應用程序只能並且僅能訪問它自己的鑰匙鏈項(包括因為應用程序創建包含了一個持久的引用二引發的可能的鑰匙鏈項異常)
Structure of a Keychain
Each keychain can contain any number of keychain items. Each keychain item contains data plus a set of attributes. For a keychain item that needs protection, such as a password or private key (a string of bytes used to encrypt or decrypt data), the data is encrypted and protected by the keychain. For keychain items that do not need protection, such as certificates, the data is not encrypted.
鑰匙鏈結構
每一個鑰匙鏈可以包含任意數量的鑰匙鏈項。每個鑰匙鏈項包含數據加上一些屬性集。對一個鑰匙鏈項來說需要進行保護,比如像一個密碼或這個私有鍵(加密或者揭秘的二進制數據字符串),數據被鑰匙鏈加密和保護。對多個鑰匙鏈項來說不需要保護,比如像證書,它的數據是不加密的。
The iOS gives an application access to only its own keychain items.
在iOS中,僅給一個用程序訪問它自己的鑰匙項權限。
The iOS Keychain Services API uses a different paradigm. This API has a single function (SecItemAdd) for adding an item to a keychain.
在iOS鑰匙鏈服務API使用了一個不同的范式(相對於Mac OS X)。這個API有一個單獨的函數(SecItemAdd)來添加一個鑰匙鏈項。
In iOS, you call the SecItemCopyMatching function to find a keychain item owned by your application. In this case there’s only one keychain and the user is never prompted to unlock it.
在iOS中,你調用SecItemCopyMatching函數來查找你應用程序自己的鑰匙鏈項目。在這種情況下,僅有一個鑰匙鏈並且用戶不需要被提示來解鎖它(相對於Mac OS X)。
iOS_mi_ma_cun_chu.jpgiOS鑰匙鏈服務搜索字典
在iOS,鑰匙鏈服務使用鍵值對字典方式來制定鑰匙鏈項的屬性,這樣你就能查找或者創建。
經典的搜索字典組成:
class key:用來指定要搜索的鑰匙鏈項的類型(比如,互聯網密碼 或者密碼密鑰)。
一個或多個鍵值對來指定匹配屬性數據(比如標簽或者創建日期)
一個或多個搜索鍵值對,用來指定值來精確搜索,比如發行證書或者郵件地址來匹配。
一個返回值鍵值對,來指定你想要的數據(比如。一個字典或者是一個偏好引用)。
什麼樣的屬性值被指定,取決於你要搜索的鑰匙鏈項的類型。舉例,如果你指定鍵位kSecClass的值為kSecClassGenericPassword,然後你可以指定創建時間的值或這個修改時間的值,但是不能指定主題或發行者(這個被用於證書)。
舉例,如果你想執行一個對AppStore賬戶名為'ImaUser'密碼不敏感情況的搜索,你可以在SecItemCopyMatching函數,使用上面表格中的鍵值對。
kSecReturnData鍵是函數返回鑰匙鏈項的數據,如果你想得到鍵值對的值,你需要使用kSecReturnAttributes返回類型鍵並且值為kCFBooleanTrue.
<code>/** * @author iYiming, 15-04-19 22:20:51 * * 設置密碼(添加密碼、修改密碼) * * @param password 密碼 * @param serviceName 服務名稱 * @param account 賬戶名稱 * * @return 是否設置成功 */ + (BOOL) setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account{ NSMutableDictionary *keyChainDic = [[NSMutableDictionary alloc] init]; [keyChainDic setObject:account forKey:(__bridge id)kSecAttrAccount];//賬戶 [keyChainDic setObject:serviceName forKey:(__bridge id)kSecAttrService];//服務名 [keyChainDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//類型 [keyChainDic setObject:[password dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];//密碼數據 [keyChainDic setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];//返回數據鍵值對 [keyChainDic setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];//返回Data CFMutableDictionaryRef outDictionary = nil; OSStatus keychainErr = SecItemCopyMatching((__bridge CFDictionaryRef)keyChainDic,(CFTypeRef *)&outDictionary); BOOL resultState = NO; if (keychainErr == noErr) { NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSMutableDictionary *)outDictionary]; [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [keyChainDic removeObjectForKey:(__bridge id)kSecAttrAccount]; [keyChainDic removeObjectForKey:(__bridge id)kSecAttrService]; [keyChainDic removeObjectForKey:(__bridge id)kSecClass]; [keyChainDic removeObjectForKey:(__bridge id)kSecReturnAttributes]; [keyChainDic removeObjectForKey:(__bridge id)kSecReturnData]; OSStatus errorCode = SecItemUpdate((__bridge CFDictionaryRef)returnDictionary, (__bridge CFDictionaryRef)keyChainDic); if (errorCode == noErr) { resultState = YES; }else{ resultState = NO; } }else if (keychainErr == errSecItemNotFound) { //添加 OSStatus errorCode = SecItemAdd((__bridge CFDictionaryRef)keyChainDic,NULL); if (errorCode == noErr) { resultState = YES; }else{ resultState = NO; } } return resultState; }</code>
<code>/** * @author iYiming, 15-04-19 22:21:38 * * 獲取密碼 * * @param serviceName 服務名稱 * @param account 賬戶名稱 * * @return 密碼 */ + (NSString *) passwordForService:(NSString *)serviceName account:(NSString *)account { NSMutableDictionary *keyChainDic = [[NSMutableDictionary alloc] init]; [keyChainDic setObject:account forKey:(__bridge id)kSecAttrAccount]; [keyChainDic setObject:serviceName forKey:(__bridge id)kSecAttrService]; [keyChainDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [keyChainDic setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData]; CFDataRef passwordData = NULL; OSStatus keychainError = noErr; keychainError = SecItemCopyMatching((__bridge CFDictionaryRef)keyChainDic,(CFTypeRef *)&passwordData); NSString *pwdStr = nil; if (keychainError == noErr){ NSString *passwordString = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)passwordData bytes] length:[(__bridge NSData *)passwordData length] encoding:NSUTF8StringEncoding]; NSLog(@"%@",passwordString); pwdStr = passwordString; }else if (keychainError == errSecItemNotFound) { pwdStr = nil; } return pwdStr; }</code>
<code>/** * @author iYiming, 15-04-19 22:22:10 * * 刪除密碼 * * @param serviceName 服務名稱 * @param account 賬戶名稱 * * @return 是否刪除成功狀態 */ + (BOOL) deletePasswordForService:(NSString *)serviceName account:(NSString *)account { NSMutableDictionary *keyChainDic = [[NSMutableDictionary alloc] init]; [keyChainDic setObject:account forKey:(__bridge id)kSecAttrAccount]; [keyChainDic setObject:serviceName forKey:(__bridge id)kSecAttrService]; [keyChainDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; [keyChainDic setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes]; CFMutableDictionaryRef outDictionary = nil; OSStatus keychainError = SecItemCopyMatching((__bridge CFDictionaryRef)keyChainDic,(CFTypeRef *)&outDictionary); BOOL resultState = NO; if (keychainError == noErr){ NSString *pwd = [YMKeyChain passwordForService:serviceName account:account];//YMKeyChain 自己寫的是一個類,調用的上面的方法 [keyChainDic setObject:[pwd dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData]; OSStatus keychainError = SecItemDelete((__bridge CFDictionaryRef)keyChainDic);//密碼正確才能刪除成功 if (keychainError == noErr){ resultState = YES; }else{ resultState = NO; } } return resultState; }</code>
已經放到 GitHub 上。