某天,小熊碰見這樣一個錯誤 Couldn't update the Keychain Item問題處理 ,網上搜索了下網上很多解決方案,依然百撕不得騎姐。后來參考下面兩篇文章。才發現是用法不正確,網上好多一些錯誤用法的文章也是醉了。
keychain淺析 以及iOS簡單使用keychain存儲密碼
請原諒我的無恥copy部分原作內容,后面我會加一些原創注意事項,實在是擔心有一天搜不出來了就可惜了。贊原作者一下,可惜沒有作者微信,不然我一定發5毛錢紅包。注意代碼中下面注釋出現的地方。我就是跪那了。
什么是Keychain?
根據蘋果的介紹,iOS設備中的Keychain是一個安全的存儲容器,可以用來為不同應用保存敏感信息比如用戶名,密碼,網絡密碼,認證令牌。蘋果自己用keychain來保存Wi-Fi網絡密碼,VPN憑證等等。它是一個在所有app之外的sqlite數據庫。
如果我們手動把自己的私密信息加密,然后通過寫文件保存在本地,再從本地取出不僅麻煩,而且私密信息也會隨着App的刪除而丟失。iOS的Keychain能完美的解決這些問題。並且從iOS 3.0開始,Keychain還支持跨程序分享。這樣就極大的方便了用戶。省去了很多要記憶密碼的煩惱。
Structure of a Keychain
Keychain內部可以保存很多的信息。每條信息作為一個單獨的keychain item,keychain item一般為一個字典,每條keychain item包含一條data和很多attributes。舉個例子,一個用戶賬戶就是一條item,用戶名可以作為一個attribute , 密碼就是data。 keychain雖然是可以保存15000條item,每條50個attributes,但是蘋果工程師建議最好別放那么多,存幾千條密碼,幾千字節沒什么問題。
如果把keychain item的類型指定為需要保護的類型比如password或者private key,item的data會被加密並且保護起來,如果把類型指定為不需要保護的類型,比如certificates,item的data就不會被加密。
item可以指定為以下幾種類型:
- extern CFTypeRef kSecClassGenericPassword
- extern CFTypeRef kSecClassInternetPassword
- extern CFTypeRef kSecClassCertificate
- extern CFTypeRef kSecClassKey
- extern CFTypeRef kSecClassIdentity OSX_AVAILABLE_STARTING(MAC_10_7, __IPHONE_2_0);
Keychain的用法
首先導入Security.framework 。
Keychain的API提供以下幾個函數來操作Keychain
- SecItemAdd 添加一個keychain item
- SecItemUpdate 修改一個keychain item
- SecItemCopyMatching 搜索一個keychain item
- SecItemDelete 刪除一個keychain item
也可以參考以下這段簡單的代碼來了解下Keychain API的用法。
- (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier { NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init]; //指定item的類型為GenericPassword [searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; //類型為GenericPassword的信息必須提供以下兩條屬性作為unique identifier [searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrAccount]; [searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrService]; return searchDictionary; } - (NSData *)searchKeychainCopyMatching:(NSString *)identifier { NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; //在搜索keychain item的時候必須提供下面的兩條用於搜索的屬性 //只返回搜索到的第一條item,這個是搜索條件。 [searchDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; //返回item的kSecValueData 字段。也就是我們一般用於存放的密碼,返回類型為NSData *類型 [searchDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
//我來解釋下這里匹配出的是 找到一條符合ksecAttrAccount、類型為普通密碼類型kSecClass,返回ksecValueData字段。 NSData *result = nil; OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchDictionary, (CFTypeRef *)&result); [searchDictionary release]; return result; } - (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier { NSMutableDictionary *dictionary = [self newSearchDictionary:identifier];
//非常值得注意的事kSecValueData字段只接受UTF8格式的 NSData *類型,否則addItem/updateItem就會crash,並且一定記得帶上service和account字段 NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; [dictionary setObject:passwordData forKey:(id)kSecValueData]; OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL); [dictionary release]; if (status == errSecSuccess) { return YES; } return NO; } - (BOOL)updateKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier { NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init]; NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; [updateDictionary setObject:passwordData forKey:(id)kSecValueData];
//這里也有需要注意的地方,searchDictionary為搜索條件,updateDictionary為需要更新的字典。這兩個字典中一定不能有相同的key,否則就會更新失敗 OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary, (CFDictionaryRef)updateDictionary); [searchDictionary release]; [updateDictionary release]; if (status == errSecSuccess) { return YES; } return NO; } - (void)deleteKeychainValue:(NSString *)identifier { NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier]; SecItemDelete((CFDictionaryRef)searchDictionary); [searchDictionary release]; }
Keychain API的用法稍微有點復雜。不過Apple自己也提供了一個封裝了Keychain API的類: KeychainItemWrapper https://developer.apple.com/library/ios/samplecode/GenericKeychain/Introduction/Intro.html 雖然這個類封裝了Keychain的API,但是不僅代碼寫的很不容易理解,而且里面也有不少的Bug。所以還是不建議使用。 目前發現這個類的1.2版存在的Bug為:
- 如果需要某個keychain item支持iCloud備份,添加kSecAttrSynchronizable屬性之后,它並沒有在第二次更新item或者搜索item的時候加上這一條,所以導致item已經存在但是它卻獲取不到。
- 類型為GenericPassword的item必須使用kSecAttrAccount和kSecAttrService作為主要的key,但是這個類僅僅以kSecAttrGeneric作主要的key。所以在用它添加item的時候容易出現重復添加的錯誤。
每種類型的Keychain item都有不同的鍵作為主要的Key也就是唯一標示符用於搜索,更新和刪除,Keychain內部不允許添加重復的Item。
keychain item的類型,也就是kSecClass鍵的值 | 主要的Key |
---|---|
kSecClassGenericPassword | kSecAttrAccount,kSecAttrService |
kSecClassInternetPassword | kSecAttrAccount, kSecAttrSecurityDomain, kSecAttrServer, kSecAttrProtocol,kSecAttrAuthenticationType, kSecAttrPortkSecAttrPath |
kSecClassCertificate | kSecAttrCertificateType, kSecAttrIssuerkSecAttrSerialNumber |
kSecClassKey | kSecAttrApplicationLabel, kSecAttrApplicationTag, kSecAttrKeyType,kSecAttrKeySizeInBits, kSecAttrEffectiveKeySize |
kSecClassIdentity | kSecClassKey,kSecClassCertificate |
Keychain的備份
-
iOS的Keychain由系統管理並且進行加密,Keychain內的信息會隨着iPhone的數據一起備份。但是kSecAttrAccessible 屬性被設置為后綴是ThisDeviceOnly的數據會被以硬件相關的密鑰(key)加密。並且不會隨着備份移動至其他設備。
kSecAttrAccessiblein變量用來指定這條信息的保護程度。我們需要對這個選項特別注意,並且使用最嚴格的選項。這個鍵(key)可以設置6種值。
- CFTypeRef kSecAttrAccessibleWhenUnlocked;
- CFTypeRef kSecAttrAccessibleAfterFirstUnlock;
- CFTypeRef kSecAttrAccessibleAlways;
- CFTypeRef kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
- CFTypeRef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
- CFTypeRef kSecAttrAccessibleAlwaysThisDeviceOnly;
從iOS5.0開始kSecAttrAccessible默認為kSecAttrAccessibleWhenUnlocked。
-
Keychain從iOS7.0開始也支持iCloud備份。把kSecAttrSynchronizable屬性設置為@YES,這樣后Keychain就能被iCloud備份並且跨設備分享。
不過在添加kSecAttrSynchronizable屬性后,這條屬性會被作為每條Keychain Item的主要的Key之一,所以在搜索,更新,刪除的時候如果查詢字典內沒有這一條屬性,item就匹配不到。
Keychain Access Group
Keychain通過provisioning profile來區分不同的應用,provisioning文件內含有應用的bundle id和添加的access groups。不同的應用是完全無法訪問其他應用保存在Keychain的信息,除非指定了同樣的access group。指定了同樣的group名稱后,不同的應用間就可以分享保存在Keychain內的信息。
Keychain Access Group的使用方法:
-
首先要在Capabilities下打開工程的Keychain Sharing按鈕。然后需要分享Keychain的不同應用添 加相同的Group名稱。Xcode6以后Group可以隨便命名,不需要加AppIdentifierPrefix前綴,並且Xcode會在以entitlements結尾的文件內自動添加所有Group名稱,然后在每一個Group前自動加上$(AppIdentifierPrefix)前綴。雖然文檔內提到還需要添加一個包含group的.plist文件,其實它和.entitlements文件是同樣的作用,所以不需要重復添加。 但是每個不同的應用第一條Group最好以自己的bundleID命名,因為如果entitlements文件內已經有Keychain Access Groups數組后item的Group屬性默認就為數組內的第一條Grop。
-
需要支持跨設備分享的Keychain item添加一條AccessGroup屬性,不過代碼里Group名稱一定要加上AppIdentifierPrefix前綴。
[searchDictionary setObject:@“AppIdentifierPrefix.UC.testWriteKeychainSuit” forKey:(id)kSecAttrAccessGroup];
如果要在app內部存私有的信息,group置為自己的bundleID即可,如果entitlements文件內沒有指定Keychain Access Groups數組。那group也可以置為nil,這樣默認也會以自己的bundleID作為Group。
Keychain的安全性
Keychain內部的數據會自動加密。如果設備沒有越獄並且不暴力破解,keychain確實很安全。但是越獄后的設備,keychain就很危險了。
通過上面的一些信息我們已經知道訪問keychain里面的數據需要和app一樣的證書或者獲得access group的名稱。設備越獄后相當於對蘋果做簽名檢查的地方打了個補丁,偽造一個證書的app也能正常使用,並且加上Keychain Dumper這些工具獲取Keychain內的信息會非常容易。
使用keychain需要注意的問題
- 當我們不支持Keychain Access Group,並且沒有entitlement文件時,keychain默認以bundle id為group。如果我們在版本更新的時候改變了bundle id,那么新版本就訪問不了舊版本的keychain信息了。解決辦法是從一開始我們就打開KeychainSharing,添加Keychain Access Group,並且指定每條keychain Item的group,私有的信息就指定app的bundle id為它的group。
- 代碼內Access group名稱一定要有AppIdentifierPrefix前綴。
- Keychain是基於數據庫存儲,不允許添加重復的條目。所以每條item都必須指定對應的唯一標識符也就是那些主要的key,如果Key指定不正確,可能會出現添加后查找不到的問題。
- kSecAttrSynchronizable也會作為主要的key之一。它的value值默認為No,如果之前添加的item此條屬性為YES,在搜索,更新,刪除的時候必須添加此條屬性才能查找到之前添加的item。
- Kechain item字典內添加自定義key時會出現參數不合法的錯誤。
- ksecValueData必須為UTF8的NSData類型。
- kSecClass中的主鍵值,不允許添加2個相同的值。否則會添加失敗。 對應主要的key是一定要包含的,否則會寫入失敗。
- 更新函數SecItemUpdate ,第一個參數為搜索條件,第二個參數為需要更新的鍵值。兩個不能有重復key,否則會失敗。
總結
keychain很強大,是一個值得利用的工具,我們可以在保存密碼或者證書的時候使用keychain,並且支持不同應用分享Keychain內的信息,或者支持iCloud備份跨設備分享,但是越獄版應用還是不建議使用。