keychain介紹
keychain的基本使用
keychain的類型
- kSecClassGenericPassword
- kSecClassInternetPassword
- kSecClassCertificate
- kSecClassKey
- kSecClassIdentity
這5個類型只是對應於不同的item,存儲的屬性有區別,使用上都是一樣的。
不同類型對應的屬性:

既然蘋果是采用SQLite去存儲的,那么以上這些不同item的attribute可以理解是數據庫里面表的字段。那么對keychain的操作其實也就是普通數據庫的增刪改查了。這樣也許就會覺得那些API也沒那么難用了。
增
NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrService : @"loginPassword", }; CFErrorRef error = NULL; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);
以這個添加kSecClassGenericPassword item為例,在字典里面我們設置了以下幾個屬性:獲取權限為當設備處於未鎖屏狀態,item的類型為kSecClassGenericPassword,item的value為@"123456", item的賬戶名為@"account name", item的service為@"loginPassword"。最后,調用SecItemAdd進行插入。使用上有點像CoreData。
刪
NSDictionary *query = @{ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService : @"loginPassword", (__bridge id)kSecAttrAccount : @"account name" }; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
刪除同樣也是指定之前存的item的屬性,最后調用SecItemDelete這個方法。這邊要注意的是勁量用多個字段確定這個item,(雖然平常開發都可能是唯一)防止刪除了其他item;比如我們把kSecAttrAccount這個屬性去掉,那么將會刪除所有的kSecAttrService對應value為@"loginPassword"的item;
改
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrService : @"loginPassword", }; NSDictionary *update = @{ (__bridge id)kSecValueData : [@"654321" dataUsingEncoding:NSUTF8StringEncoding], }; OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
蘋果推薦我們用SecItemUpdate去修改一個已經存在的item,可能我們喜歡先調用SecItemDelete方法去刪除,再添加一個新的。這個主要目的是防止新添的item丟失了原來的部分屬性。這個方法需要兩個入參,一個字典是用來指定要更新的item,另一個字典是想要更新的某個屬性的value,最后調用SecItemUpdate。
查
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecReturnData : @YES, (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne, (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrService : @"loginPassword", }; CFTypeRef dataTypeRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef); if (status == errSecSuccess) { NSString *pwd = [[NSString alloc] initWithData:(__bridge NSData * _Nonnull)(dataTypeRef) encoding:NSUTF8StringEncoding]; NSLog(@"==result:%@", pwd); }
查和前面幾個操作類似,首先同樣是指定屬性定位到這個item,最后調用SecItemCopyMatching方法。既然是數據庫查詢,肯定會有記錄的條數的問題。本例中使用了kSecMatchLimitOne,表示返回結果集的第一個,當然這個也是默認的。如果是查詢出多個,kSecMatchLimitAll可以使用這個,那么返回的將是個數組。SecItemCopyMatching方法的入參dataTypeRef,是一個返回結果的引用,會根據不同的item,返回對應不同的類型(如NSCFData, NSCFDictionary, NSCFArray等等)。
剛剛上面是返回存儲的value的引用,如果我們想看看這個item所有的屬性怎么辦?我們可以使用kSecReturnRef
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecReturnRef : @YES, (__bridge id)kSecReturnData : @YES, (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne, (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrService : @"noraml", }; CFTypeRef dataTypeRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef); if (status == errSecSuccess) { NSDictionary *dict = (__bridge NSDictionary *)dataTypeRef; NSString *acccount = dict[(id)kSecAttrAccount]; NSData *data = dict[(id)kSecValueData]; NSString *pwd = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *service = dict[(id)kSecAttrService]; NSLog(@"==result:%@", dict); }
這樣,我們就得到了這個item的所有屬性。
Sharing Items
同一個開發者賬號下(teamID),各個應用之間可以共享item。keychain通過keychain-access-groups
來進行訪問權限的控制。在Xcode的Capabilities選項中打開Keychain Sharing即可。

每個group命名開頭必須是開發者賬號的teamId。不同開發者賬號的teamId是唯一的,所以蘋果限制了只有同一個開發者賬號下的應用才可以進行共享。如果有多個sharedGroup,在添加的時候如果不指定,默認是第一個group。
添加:
NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test", (__bridge id)kSecAttrService : @"noraml1", (__bridge id)kSecAttrSynchronizable : @YES, }; CFErrorRef error = NULL; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);
取:
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecReturnRef : @YES, (__bridge id)kSecReturnData : @YES, (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll, (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test", (__bridge id)kSecAttrService : @"noraml1", }; CFTypeRef dataTypeRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
只需要添加一個kSecAttrAccessGroup屬性即可。
APP對keychain的訪問權限:
(1)未對應用APP的entitlement(授權)進行配置時,APP使用鑰匙串存儲時,會默認存儲在自身BundleID的條目下。
(2)對APP的entitlement(授權)進行配置后,說明APP有了對某個條目的訪問權限。
鑰匙串的可視化效果可參見Mac的APP-鑰匙串訪問。
APP鑰匙串訪問權限的配置方法:(這里XXXXX模擬器隨意,但真機必須為自己開發者賬號ID,否則無法通過編譯)
1.新建一個Plist文件,在Plist中的數組中添加可以訪問的條目的名字(如KeychainAccessGroups.plist),結構如下:
Plist代碼:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>keychain-access-groups</key> <array> <string>XXXXX.GrassInfoAppFamily</string> </array> </dict> </plist>
2.在Build-setting中進行配置,搜索entitlement,注意路徑別配置錯:
keychain安全方面的一些東西。
kSecAttrAccessible
這個屬性,決定了我們item在什么條件下可以獲取到里面的內容,我們在添加item的時候,可以添加這個屬性,來增強數據的安全性,具體的主要有以下幾個:
-
kSecAttrAccessibleWhenUnlocked
-
kSecAttrAccessibleAfterFirstUnlock
-
kSecAttrAccessibleAlways
-
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
-
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
-
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
-
kSecAttrAccessibleAlwaysThisDeviceOnly
每個意思都很明確,item默認就是kSecAttrAccessibleWhenUnlocked。也就是在設備未鎖屏的情況下。這個也是蘋果推薦的。kSecAttrAccessibleAlways,這個蘋果在WWDC中也說了,不建議使用,蘋果自己已經都棄用了。kSecAttrAccessibleAfterFirstUnlock這個是在設備第一次解鎖后,可以使用。這個最常見的就是后台喚醒功能里面,如果需要訪問某個item,那么需要使用這個屬性,不然是訪問不了item的數據的。最后幾個DeviceOnly相關的設置,如果設置了,那么在手機備份恢復到其他設備時,是不能被恢復的。同樣iCloud也不會同步到其他設備,因為在其他設備上是解密不出來的。
iCloud
keychain item可以備份到iCloud上,我們只需要在添加item的時候添加@{(__bridge id)kSecAttrSynchronizable : @YES,}。如果想同步到其他設備上也能使用,請避免使用DeviceOnly設置或者其他和設備相關的控制權限。
Access Control
ACL是iOS8新增的API,iOS9之后對控制權限進行了細化。在原來的基礎上加了一層本地驗證,主要是配合TouchID一起使用。對於我們使用者來說,在之前的item操作是一樣的,只是在添加的時候,加了一個SecAccessControlRef對象。
CFErrorRef error = NULL; SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAccessControlUserPresence, &error); if (error) { NSLog(@"failed to create accessControl"); return; } NSDictionary *query = @{ (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecValueData : [@"accesscontrol test" dataUsingEncoding:NSUTF8StringEncoding], (__bridge id)kSecAttrAccount : @"account name", (__bridge id)kSecAttrService : @"accesscontrol", (__bridge id)kSecAttrAccessControl : (__bridge id)accessControl, }; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);
我們只需要創建SecAccessControlRef對象,主要是兩個參數,一個是kSecAttrAccessible,另一個是SecAccessControlCreateFlags。在字典里面添加(__bridge id)kSecAttrAccessControl : (__bridge id)accessControl即可。
SecAccessControlCreateFlags:
-
kSecAccessControlUserPresence
item通過鎖屏密碼或者Touch ID進行驗證,Touch ID可以不設置,增加或者移除手指都能使用item。
-
kSecAccessControlTouchIDAny
item只能通過Touch ID驗證,Touch ID 必須設置,增加或移除手指都能使用item。
-
kSecAccessControlTouchIDCurrentSet
item只能通過Touch ID進行驗證,增加或者移除手指,item將被刪除。
-
kSecAccessControlDevicePasscode
item通過鎖屏密碼驗證訪問。
-
kSecAccessControlOr
如果設置多個flag,只要有一個滿足就可以。
-
kSecAccessControlAnd
如果設置多個flag,必須所有的都滿足才行。
-
kSecAccessControlPrivateKeyUsage
私鑰簽名操作
-
kSecAccessControlApplicationPassword
額外的item密碼,可以讓用戶自己設置一個訪問密碼,這樣只有知道密碼才能訪問。
獲取操作和以前的都是一樣的,只是加了一個提示信息kSecUseOperationPrompt,用來說明調用意圖:
NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecReturnData : @YES, (__bridge id)kSecAttrService : @"accesscontrol", (__bridge id)kSecUseOperationPrompt : @"獲取存儲密碼", }; CFTypeRef dataTypeRef = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef); if (status == errSecSuccess) { NSString *pwd = [[NSString alloc] initWithData:(__bridge NSData * _Nonnull)(dataTypeRef) encoding:NSUTF8StringEncoding]; NSLog(@"==result:%@", pwd); }

Secure Enclave
Secure Enclave 首次出現在iPhone 5s中,就是協處理器M7,用來保護指紋數據。SE里面的數據我們用戶層面代碼是訪問不了的,哪怕系統越獄了,也無法訪問到里面數據。只有特定的代碼才能去訪問(CPU 切換成Monitor Mode)。SE本身也集成了加密庫,加密解密相關的都在SE內部完成,這樣應用程序只能拿到最后的結果,而無法拿到原始的數據。(關於Secure Enclave 可以搜些資料了解下,這里就不展開了)。在iOS9之后蘋果開放了一個新的屬性:kSecAttrTokenIDSecureEnclave,也就是將數據保存到SE里面,當然只是key。
如何使用:
//生成ECC公私鑰 CFErrorRef error = NULL; SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAccessControlPrivateKeyUsage | kSecAccessControlTouchIDAny, &error); if (error) { NSLog(@"failed to create accessControl"); return; } NSDictionary *params = @{ (__bridge id)kSecAttrTokenID: (__bridge id)kSecAttrTokenIDSecureEnclave, (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeEC, (__bridge id)kSecAttrKeySizeInBits: @256, (__bridge id)kSecPrivateKeyAttrs: @{ (__bridge id)kSecAttrAccessControl: (__bridge_transfer id)accessControl, (__bridge id)kSecAttrIsPermanent: @YES, (__bridge id)kSecAttrLabel: @"ECCKey", }, }; SecKeyRef publickKey, privateKey; OSStatus status = SecKeyGeneratePair((__bridge CFDictionaryRef)params, &publickKey, &privateKey); [self handleError:status]; if (status == errSecSuccess) { CFRelease(privateKey); CFRelease(publickKey); } //簽名 NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassKey, (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, (__bridge id)kSecAttrLabel: @"ECCKey", (__bridge id)kSecReturnRef: @YES, (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne, (__bridge id)kSecUseOperationPrompt: @"簽名數據" }; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Retrieve the key from the keychain. No authentication is needed at this point. SecKeyRef privateKey; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey); if (status == errSecSuccess) { // Sign the data in the digest/digestLength memory block. uint8_t signature[128]; size_t signatureLength = sizeof(signature); uint8_t digestData[16]; size_t digestLength = sizeof(digestData); status = SecKeyRawSign(privateKey, kSecPaddingPKCS1, digestData, digestLength, signature, &signatureLength); if (status == errSecSuccess) { NSLog(@"sign success"); } CFRelease(privateKey); } else { } });
以上代碼就是生成了一對公私鑰(ECC 256),私鑰會保存在SE中,而公鑰交給應用程序。簽名操作的時候,好像我們取到了私鑰,但是實際上我們並不能拿到私鑰,只是私鑰在SE中的一個引用。加密的操作也是在SE中完成,最后返回給我們簽名的數據。
蘋果在這邊舉了個簡單例子,如何利用Touch ID進行登錄。客戶端生成一對公私鑰,公鑰發給服務器,客戶端在通過Touch ID校驗后,加密一段內容(私鑰簽名操作),將內容和結果發送給服務器,服務器取出公鑰進行驗簽。如果一致,則通過驗證。
item解密過程

上面這個圖就是普通item的一個解密流程。應用程序通過API訪問item,在keychain里面取出加密的item,將加密的item,傳遞給SE解密,解密完返回給keychain,最后返回給應用。
iOS8后,蘋果將中間的keychain框架進行了拆分,增加了本地授權認證:

這個最大的用途就是和Touch ID進行結合,來提高我們的數據安全性。當我們取item的時候,如果需要Touch ID進行驗證,在SE里面,如果通過驗證那么將對數據進行解密,並返回給keychain,最后返回給應用程序。
iOS9之后的keyStore也放進了SE里面,進一步提高了安全性。至於keychain的安全性在非越獄下的確是安全的,但是一旦手機越獄,應用可以訪問到其他應用程序item,或者通過Keychain-Dumper導出keychain數據,那么就不是很安全了。所以在我們存進鑰匙串的數據,不要直接存一些敏感信息,在程序中加一層數據保護。
參考:
安全白皮書
Keychain and Authentication with Touch ID
Protecting Secrets with the Keychain
Security and Your Apps
//一下的是有些人說跟新了 但是我測試了一下 刪除之后 keychain里面還是有數據的
我在官方文檔中並未找到相關的更新:https://developer.apple.com/documentation/security/keychain_services
大家還是可以放心用的
iOS 10.3 還未正式發布,beta 版中一個關於keychain 特性的小修改,就已經引起了廣泛的關注。
改動如下:
如果 App 被刪除,之前存儲於 keychain 中的數據也會一同被清除。
如果使用了 keychain group,只要當 group 所有相關的 App 被刪除時,keychain 中的數據才會被刪除。
這一改動,雖未經官方公布。但已在論壇帖子里得到了 Apple 員工的確認,原文如下:
This is an intentional change in iOS 10.3 to protect user privacy. Information that can identify a user should not be left on the device after the app that created it has been removed.
It has never been a part of the API contract that keychain items created by an app would survive when the app is removed. This has always been an implementation detail.
If a keychain item is shared with other apps, it won’t be deleted until those other apps have been deleted as well.
如果這是這樣的話,那么keychain存在還有什么意義么?
還有蘋果現在越來越注重用戶的隱私,就前幾天對於使用JSPatch熱更新的機制的應用發送的郵件來看,蘋果似乎要在這方面有動作了,我想說,蘋果爸爸這次難道真的要為Swift和OC兩個親兒子出頭了嗎?
其實我也覺得 app 都刪了 keychain 還在是挺不合理的一件事兒。在隱私保護上還是可以看得出 Apple 還是一直在作為。
由於蘋果頻繁的更新,之前的一些東西已經不能使用https://forums.developer.apple.com/message/210531#210531