重要的事情說(shuō)三遍
使用keychain group的時(shí)候,測(cè)試一定要使用真機(jī)!
使用keychain group的時(shí)候,測(cè)試一定要使用真機(jī)!
使用keychain group的時(shí)候,測(cè)試一定要使用真機(jī)!
14年的年底開(kāi)始做iOS開(kāi)發(fā),一直在公司忙著加班趕項(xiàng)目,也沒(méi)時(shí)間靜下心來(lái)寫(xiě)一遍文章,最近公司不是很忙,就有一些業(yè)余時(shí)間可以研究一些技術(shù)!這篇就主要來(lái)講講iOS存儲(chǔ)機(jī)制中的keychain!有理解不到或者是理解不全的地方,希望各位同行們能夠給予補(bǔ)充,謝謝!以下測(cè)試在ipone5s真機(jī)運(yùn)行,系統(tǒng)10.3.2!
什么是Keychain?
根據(jù)蘋(píng)果的介紹,iOS設(shè)備中的Keychain是一個(gè)安全的存儲(chǔ)容器,可以用來(lái)為不同應(yīng)用保存敏感信息比如用戶名,密碼,網(wǎng)絡(luò)密碼,認(rèn)證令牌。蘋(píng)果自己用keychain來(lái)保存Wi-Fi網(wǎng)絡(luò)密碼,VPN憑證等等。它是一個(gè)在所有app之外的sqlite數(shù)據(jù)庫(kù)。
如果我們手動(dòng)把自己的私密信息加密,然后通過(guò)寫(xiě)文件保存在本地,再?gòu)谋镜厝〕霾粌H麻煩,而且私密信息也會(huì)隨著App的刪除而丟失。iOS的Keychain能完美的解決這些問(wèn)題。并且從iOS 3.0開(kāi)始,Keychain還支持跨程序分享。這樣就極大的方便了用戶。省去了很多要記憶密碼的煩惱。
keychain的用途以及與userDefault的比較
NSUserDefaults
NSUserDefaults其實(shí)是plist文件中鍵值存儲(chǔ),并且最大的問(wèn)題是存在與沙盒中,這就對(duì)安全性埋下了隱患。如果攻擊者破解app,拿到了沙盒中的數(shù)據(jù),就會(huì)造成數(shù)據(jù)泄漏,后果不堪設(shè)想。
當(dāng)然,一般也不會(huì)有把密碼直接使用NSUserDefaults存儲(chǔ)的,都會(huì)進(jìn)行加密、或者是多重加密后再進(jìn)行NSUserDefaults存儲(chǔ)。這么做其實(shí)是可行的,前提是加密算法不能泄漏。有個(gè)小問(wèn)題就是,如果用戶刪掉app重裝的話,之前所有存儲(chǔ)的敏感信息都會(huì)消失。比如,一個(gè)用戶誤刪了使用NSUserDefaults存儲(chǔ)密碼的app,當(dāng)重新安裝之后,由于以前是記住密碼免登錄,只因?yàn)樽约翰僮鞑划?dāng),接下來(lái)要進(jìn)入找回密碼功能,重新修改密碼才能再次使用app。這對(duì)用戶來(lái)說(shuō)是一種相當(dāng)不友好的體驗(yàn)。
所以,正確的姿勢(shì)是使用Keychain服務(wù)來(lái)存儲(chǔ)。Keychain保存的數(shù)據(jù)不僅僅是加密過(guò)的,而且由于Keychain是存在與沙盒之外的,當(dāng)應(yīng)用刪除之后,app存儲(chǔ)的數(shù)據(jù)并沒(méi)有被刪掉,第二次安裝時(shí)只要讀取Keychain里的數(shù)據(jù),即可得到以前存儲(chǔ)的信息。
Keychain使用場(chǎng)景
存儲(chǔ)隱私信息
在iOS系統(tǒng)中,最常用的keychain服務(wù)就是存儲(chǔ)用戶密碼了。使用keychain保存用戶密碼最大的好處已經(jīng)在上面說(shuō)過(guò),個(gè)人極力推薦這么做。
數(shù)據(jù)共享
如果我們有多個(gè)app,它們之間需要共享一些數(shù)據(jù),以提供更好的用戶體驗(yàn),那么使用Keychain群組可以實(shí)現(xiàn)。但前提是同一個(gè)公司的產(chǎn)品才能共享,比如com.hyyy.test1和com.hyyy.test2兩個(gè)同一公司下的不同產(chǎn)品之前可以實(shí)現(xiàn)數(shù)據(jù)共享。
設(shè)備唯一標(biāo)示存儲(chǔ)
在iOS中,為了在蘋(píng)果的打壓下獲取唯一標(biāo)示符,開(kāi)發(fā)者們也是想盡了辦法,目前最好的方式就是獲取IDFV,并將其存儲(chǔ)到keychain中。IDFV是設(shè)備區(qū)別應(yīng)用提供商的,一般來(lái)說(shuō)可以作為應(yīng)用唯一標(biāo)示符。但是IDFV缺陷就是當(dāng)設(shè)備刪除了該所有應(yīng)用提供商的app之后,IDFV值會(huì)發(fā)生變化,所以IDFV+Keychain的組合目前被經(jīng)常用到,來(lái)替代UDID的作用。特別是加上Keychain的共享服務(wù),可以使應(yīng)用提供商下的所有app下獲取的IDFV都不會(huì)發(fā)生變化。這一服務(wù)可以說(shuō)是目前最佳的識(shí)別用戶的辦法。
Structure of a Keychain
Keychain內(nèi)部可以保存很多的信息。每條信息作為一個(gè)單獨(dú)的keychain item,keychain item一般為一個(gè)字典,每條keychain item包含一條data和很多attributes。舉個(gè)例子,一個(gè)用戶賬戶就是一條item,用戶名可以作為一個(gè)attribute , 密碼就是data。 keychain雖然是可以保存15000條item,每條50個(gè)attributes,但是蘋(píng)果工程師建議最好別放那么多,存幾千條密碼,幾千字節(jié)沒(méi)什么問(wèn)題。
如果把keychain item的類型指定為需要保護(hù)的類型比如password或者private key,item的data會(huì)被加密并且保護(hù)起來(lái),如果把類型指定為不需要保護(hù)的類型,比如certificates,item的data就不會(huì)被加密。
item可以指定為以下幾種類型:
extern CFTypeRef kSecClassGenericPassword
extern CFTypeRef kSecClassInternetPassword
extern CFTypeRef kSecClassCertificate
extern CFTypeRef kSecClassKey
extern CFTypeRef kSecClassIdentityOSX_AVAILABLE_STARTING(MAC_10_7, __IPHONE_2_0);

Keychain的用法
首先導(dǎo)入Security.framework 。
Keychain的API提供以下幾個(gè)函數(shù)來(lái)操作Keychain
SecItemAdd 添加一個(gè)keychain item
SecItemUpdate 修改一個(gè)keychain item
SecItemCopyMatching 搜索一個(gè)keychain item
SecItemDelete 刪除一個(gè)keychain item
Keychain Access Group
Keychain通過(guò)provisioning profile來(lái)區(qū)分不同的應(yīng)用,provisioning文件內(nèi)含有應(yīng)用的bundle id和添加的access groups。不同的應(yīng)用是完全無(wú)法訪問(wèn)其他應(yīng)用保存在Keychain的信息,除非指定了同樣的access group。指定了同樣的group名稱后,不同的應(yīng)用間就可以分享保存在Keychain內(nèi)的信息。
Keychain Access Group的使用方法:
首先要在Capabilities下打開(kāi)工程的Keychain Sharing按鈕。然后需要分享Keychain的不同應(yīng)用添 加相同的Group名稱。Xcode6以后Group可以隨便命名,不需要加AppIdentifierPrefix前綴,并且Xcode會(huì)在以entitlements結(jié)尾的文件內(nèi)自動(dòng)添加所有Group名稱,然后在每一個(gè)Group前自動(dòng)加上$(AppIdentifierPrefix)前綴。雖然文檔內(nèi)提到還需要添加一個(gè)包含group的.plist文件,其實(shí)它和.entitlements文件是同樣的作用,所以不需要重復(fù)添加。 但是每個(gè)不同的應(yīng)用第一條Group最好以自己的bundleID命名,因?yàn)槿绻鹐ntitlements文件內(nèi)已經(jīng)有Keychain Access Groups數(shù)組后item的Group屬性默認(rèn)就為數(shù)組內(nèi)的第一條Group,其實(shí)keychaingroups的名字你可以隨便起,(最好使用app的buddleid),在keychain groups里面加上你想要共享哪個(gè)group的數(shù)據(jù),想共享幾個(gè)就可以填寫(xiě)幾個(gè),前提是這些group必須的team必須相同,也就是(AppIdentifierPrefix)必須相同!

需要支持跨設(shè)備分享的Keychain item添加一條AccessGroup屬性,不過(guò)代碼里Group名稱一定要加上AppIdentifierPrefix前綴。我在測(cè)試的時(shí)候使用的是SAMkeychain框架以下會(huì)附上測(cè)試源碼;如果要在app內(nèi)部存私有的信息,group置為自己的bundleID即可,如果entitlements文件內(nèi)沒(méi)有指定Keychain Access Groups數(shù)組。那group也可以置為nil,這樣默認(rèn)也會(huì)以自己的bundleID作為Group。(但是當(dāng)我在10.3.2的系統(tǒng)做測(cè)試的時(shí)候,如果傳入的group為nil或者是不傳,這樣默認(rèn)認(rèn)為自己的bundleid作為group,并且有AppIdentifierPrefix前綴!如果傳入的group是沒(méi)有包含在entitlements里面的時(shí)候,會(huì)報(bào)錯(cuò),此時(shí)status 為-25300,不知道什么原因,上網(wǎng)查資料,在stackoverflow上有人說(shuō)這可能是蘋(píng)果方面的一個(gè)bug,具體問(wèn)題,我也在繼續(xù)探索中,如果有哪位大神知道原因,歡迎您的解答?。?/p>
(
這樣設(shè)置group會(huì)導(dǎo)致存儲(chǔ)數(shù)據(jù)失敗,如果不想共享某個(gè)group的數(shù)據(jù),建議group傳入AppIdentifierPrefix+bundleiD!在其他app的keychain group中,只填入想要共享數(shù)據(jù)的group!例如上圖中的MCUUId和MCaccount兩個(gè)keychain group!在另一個(gè)app中加入同樣的group,就能實(shí)現(xiàn)這兩個(gè)group中的數(shù)據(jù)共享,切記代碼中傳入group的時(shí)候要加上appidentifier前綴,否則會(huì)存儲(chǔ)失??!當(dāng)傳入的group不在entitlements文件內(nèi)時(shí),此時(shí)傳入的group的值必須為AppIdentifierPrefix+bundleiD,否則會(huì)造成存儲(chǔ)失??!查詢時(shí)也可以指定group查詢,但是必須使用真機(jī)測(cè)試
////? ? [query1 setAccessGroup:@"com.miaocaiwang.CircularScroll"];
(samkeychain的方法中涉及到的變量主要有三個(gè),分別如這一小節(jié)的標(biāo)題所示,是password、service、account。password、account分別保存的是密碼和用戶名信息。service保存的是服務(wù)的類型,就是用戶名和密碼是為什么應(yīng)用保存的一個(gè)標(biāo)志。比如一個(gè)用戶可以再不同的論壇中使用相同的用戶名和密碼,那么service保存的信息分別標(biāo)識(shí)不同的論壇。由于包名通常具有一定的唯一性,通常在程序中可以用包的名稱來(lái)作為service的標(biāo)識(shí)。)
SAMKeychainQuery *query1 = [[SAMKeychainQuery alloc] init];
//? ? query1.service = @"MCUUIDceshiceshi_111";
//? ? query1.account = @"MCUUIDceshi_111";
//? ? query1.password = @"MCUUID123456dasdas";
//? ? [query1 setAccessGroup:@"4K2U4QJKGX.com.miaocaiwang.CircularScroll"];
//? ? [query1 save:nil];
//
//
//? ? SAMKeychainQuery *query2 = [[SAMKeychainQuery alloc] init];
//? ? query2.service = @"MCUUIDceshiceshi_222";
//? ? query2.account = @"MCUUIDceshi_222";
//? ? query2.password = @"MCUUID123456dasdas";
//? ? [query2 setAccessGroup:@"4K2U4QJKGX.MCAccount"];
//? ? [query2 save:nil];
//
//
//? ? SAMKeychainQuery *query3 = [[SAMKeychainQuery alloc] init];
//? ? query3.service = @"MCUUIDceshiceshi_333";
//? ? query3.account = @"MCUUIDceshi_333";
//? ? query3.password = @"MCUUID123456dasdas";
//? ? [query3 setAccessGroup:@"4K2U4QJKGX.MCUUID"];
//? ? [query3 save:nil];
以下為log信息
{
acct = "MCUUIDceshi_111";
agrp = "4K2U4QJKGX.com.miaocaiwang.CircularScroll";
cdat = "2017-06-08 03:32:50 +0000";
mdat = "2017-06-08 03:32:50 +0000";
musr = <>;
pdmn = ak;
svce = "MCUUIDceshiceshi_111";
sync = 0;
tomb = 0;
},
{
acct = "MCUUIDceshi_222";
agrp = "4K2U4QJKGX.MCAccount";
cdat = "2017-06-08 03:32:50 +0000";
mdat = "2017-06-08 03:32:50 +0000";
musr = <>;
pdmn = ak;
svce = "MCUUIDceshiceshi_222";
sync = 0;
tomb = 0;
},
{
acct = "MCUUIDceshi_333";
agrp = "4K2U4QJKGX.MCUUID";
cdat = "2017-06-08 03:32:50 +0000";
mdat = "2017-06-08 03:32:50 +0000";
musr = <>;
pdmn = ak;
svce = "MCUUIDceshiceshi_333";
sync = 0;
tomb = 0;
}
在另一個(gè)app中做以下代碼處理獲取keychaingroup數(shù)據(jù)
SAMKeychainQuery *query1 = [[SAMKeychainQuery alloc] init];
NSLog(@"%@ %@",[query1 fetchAll:nil],[query1 password]);
log信息如下
{
acct = "MCUUIDceshi_222";
agrp = "4K2U4QJKGX.MCAccount";
cdat = "2017-06-08 03:32:50 +0000";
mdat = "2017-06-08 03:32:50 +0000";
musr = <>;
pdmn = ak;
svce = "MCUUIDceshiceshi_222";
sync = 0;
tomb = 0;
},
{
acct = "MCUUIDceshi_333";
agrp = "4K2U4QJKGX.MCUUID";
cdat = "2017-06-08 03:32:50 +0000";
mdat = "2017-06-08 03:32:50 +0000";
musr = <>;
pdmn = ak;
svce = "MCUUIDceshiceshi_333";
sync = 0;
tomb = 0;
}
此時(shí)你并不會(huì)看見(jiàn)buddleidentifier ? ?group中的內(nèi)容,這樣也就實(shí)現(xiàn)了某些數(shù)據(jù)私有化!
原理
至于sam的一些其他用法,可以自行查看,代碼很少,也比較簡(jiǎn)單!就是一些增刪改查操作
查找過(guò)程:
1.(關(guān)鍵)先配置一個(gè)操作字典內(nèi)容有:
kSecAttrService(屬性),kSecAttrAccount(屬性)這些屬性or標(biāo)簽是查找的依據(jù)
kSecReturnData(值為@YES 表明返回類型為data),kSecClass(值為kSecClassGenericPassword 表示重要數(shù)據(jù)為“一般密碼”類型)這些限制條件是返回結(jié)果類型的依據(jù)
2.然后用查找的API 得到查找狀態(tài)和返回?cái)?shù)據(jù)(密碼)
3.最后如果狀態(tài)成功那么將數(shù)據(jù)(密碼)轉(zhuǎn)換成string 返回
2.添加&更新
說(shuō)明:當(dāng)添加的時(shí)候我們一般需要判斷一下當(dāng)前鑰匙串里面是否已經(jīng)存在我們要添加的鑰匙。如果已經(jīng)存在我們就更新好了,不存在再添加,所以這兩個(gè)操作一般寫(xiě)成一個(gè)函數(shù)搞定吧。
過(guò)程關(guān)鍵:1.檢查是否已經(jīng)存在 構(gòu)建的查詢用的操作字典:kSecAttrService,kSecAttrAccount,kSecClass(標(biāo)明存儲(chǔ)的數(shù)據(jù)是什么類型,值為kSecClassGenericPassword 就代表一般的密碼)
2.添加用的操作字典: kSecAttrService,kSecAttrAccount,kSecClass,kSecValueData
3.更新用的操作字典1(用于定位需要更改的鑰匙):kSecAttrService,kSecAttrAccount,kSecClass
操作字典2(新信息)kSecAttrService,kSecAttrAccount,kSecClass ,kSecValueData
使用keychain需要注意的問(wèn)題
在使用keychain的時(shí)候要先選擇team
當(dāng)我們不支持Keychain Access Group,并且沒(méi)有entitlement文件時(shí),keychain默認(rèn)以bundle id為group。如果我們?cè)诎姹靖碌臅r(shí)候改變了bundle id,那么新版本就訪問(wèn)不了舊版本的keychain信息了。解決辦法是從一開(kāi)始我們就打開(kāi)KeychainSharing,添加Keychain Access Group,并且指定每條keychain Item的group,私有的信息就指定app的bundle id為它的group。
代碼內(nèi)Access group名稱一定要有AppIdentifierPrefix前綴。
Keychain是基于數(shù)據(jù)庫(kù)存儲(chǔ),不允許添加重復(fù)的條目。所以每條item都必須指定對(duì)應(yīng)的唯一標(biāo)識(shí)符也就是那些主要的key,如果Key指定不正確,可能會(huì)出現(xiàn)添加后查找不到的問(wèn)題。
kSecAttrSynchronizable也會(huì)作為主要的key之一。它的value值默認(rèn)為No,如果之前添加的item此條屬性為YES,在搜索,更新,刪除的時(shí)候必須添加此條屬性才能查找到之前添加的item。
Kechain item字典內(nèi)添加自定義key時(shí)會(huì)出現(xiàn)參數(shù)不合法的錯(cuò)誤。