前言
Keychain 在 Mac 上大家都比較熟悉, 主要進(jìn)行一些敏感信息存儲(chǔ)使用 如用戶名,密碼,網(wǎng)絡(luò)密碼,認(rèn)證令牌, Wi-Fi網(wǎng)絡(luò)密碼,VPN憑證等. iOS 中 Keychain, 也有相同的功能實(shí)現(xiàn) , 保存的信息存儲(chǔ)在設(shè)備中, 獨(dú)立于每個(gè)App沙盒之外. 作者這篇就簡(jiǎn)單整理下iOS 中的 Keychain.
特點(diǎn) :
1 . 更安全. 對(duì)比 NSUserDefault 存儲(chǔ)一些數(shù)據(jù), 會(huì)更加安全.
2 . 即便 App 被卸載, 存儲(chǔ)的信息依舊存在, 再次安裝App, 存儲(chǔ)是信息依舊可以使用.
3 . 相同的 Team ID 開發(fā), 可實(shí)現(xiàn)多個(gè)App 共享數(shù)據(jù)
一 Keychain 結(jié)構(gòu)說明
Keychain 結(jié)構(gòu)是由 key-value 組成.
多個(gè) key-value 標(biāo)簽的作用 : 表明該鑰匙的唯一性.
當(dāng)我們對(duì) Keychain 進(jìn)行操作時(shí), 就可以定義一個(gè)包含該 key-value 字典, 調(diào)用API, 對(duì) Keychain 進(jìn)行操作
結(jié)構(gòu)說明
圖片引用 : http://www.lxweimin.com/p/fa87b6879b99
二 Keychain 增 / 刪 / 改 / 查
對(duì)Keychain 的結(jié)構(gòu)有了簡(jiǎn)單印象, 那么就可以對(duì)其進(jìn)行操作.
主要API
/** 添加 */
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result);
/** 查詢 */
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result);
/** 更新 */
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
/** 刪除 */
OSStatus SecItemDelete(CFDictionaryRef query);
/** 增/改 */
- (IBAction)insertAndUpdate:(id)sender {
/**
說明:當(dāng)添加的時(shí)候我們一般需要判斷一下當(dāng)前鑰匙串里面是否已經(jīng)存在我們要添加的鑰匙。如果已經(jīng)存在我們就更新好了,不存在再添加,所以這兩個(gè)操作一般寫成一個(gè)函數(shù)搞定吧。
過程關(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
*/
NSLog(@"插入 : %d", [self addItemWithService:@"com.tencent" account:@"李雷" password:@"911"]);
}
-(BOOL)addItemWithService:(NSString *)service account:(NSString *)account password:(NSString *)password{
//先查查是否已經(jīng)存在
//構(gòu)造一個(gè)操作字典用于查詢
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:service forKey:(__bridge id)kSecAttrService]; //標(biāo)簽service
[queryDic setObject:account forKey:(__bridge id)kSecAttrAccount]; //標(biāo)簽account
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存儲(chǔ)的是一個(gè)密碼
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)queryDic, &result);
if (status == errSecItemNotFound) { //沒有找到則添加
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 轉(zhuǎn)換為 NSData
[queryDic setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密碼
status = SecItemAdd((__bridge CFDictionaryRef)queryDic, NULL); //!!!!!關(guān)鍵的添加API
}else if (status == errSecSuccess){ //成功找到,說明鑰匙已經(jīng)存在則進(jìn)行更新
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 轉(zhuǎn)換為 NSData
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:queryDic];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密碼
status = SecItemUpdate((__bridge CFDictionaryRef)queryDic, (__bridge CFDictionaryRef)dict);//!!!!關(guān)鍵的更新API
}
return (status == errSecSuccess);
}
/** 查 */
- (IBAction)select:(id)sender {
/**
過程:
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 返回
*/
NSLog(@"%@", [self passwordForService:@"com.tencent" account:@"李雷"]);
}
//用原生的API 實(shí)現(xiàn)查詢密碼
- (NSString *)passwordForService:(nonnull NSString *)service account:(nonnull NSString *)account{
//生成一個(gè)查詢用的 可變字典
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
//首先添加獲取密碼所需的搜索鍵和類屬性:
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; //表明為一般密碼可能是證書或者其他東西
[queryDic setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData]; //返回Data
[queryDic setObject:service forKey:(__bridge id)kSecAttrService]; //輸入service
[queryDic setObject:account forKey:(__bridge id)kSecAttrAccount]; //輸入account
//查詢
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)queryDic,&result);//核心API 查找是否匹配 和返回密碼!
if (status != errSecSuccess) { //判斷狀態(tài)
return nil;
}
//返回?cái)?shù)據(jù)
// NSString *password = [[NSString alloc] initWithData:(__bridge_transfer NSData *)result encoding:NSUTF8StringEncoding];//轉(zhuǎn)換成string
//刪除kSecReturnData鍵; 我們不需要它了:
[queryDic removeObjectForKey:(__bridge id)kSecReturnData];
//將密碼轉(zhuǎn)換為NSString并將其添加到返回字典:
NSString *password = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)result bytes] length:[(__bridge NSData *)result length] encoding:NSUTF8StringEncoding];
[queryDic setObject:password forKey:(__bridge id)kSecValueData];
NSLog(@"查詢 : %@", queryDic);
return password;
}
/** 刪 */
- (IBAction)delete:(id)sender {
NSLog(@"刪除 : %d", [self deleteItemWithService:@"com.tencent" account:@"李雷"]);
}
-(BOOL)deleteItemWithService:(NSString *)service account:(NSString *)account{
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:service forKey:(__bridge id)kSecAttrService]; //標(biāo)簽service
[queryDic setObject:account forKey:(__bridge id)kSecAttrAccount]; //標(biāo)簽account
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存儲(chǔ)的是一個(gè)密碼
OSStatus status = SecItemDelete((CFDictionaryRef)queryDic);
return (status == errSecSuccess);
}
三 Keychain 封裝
3.1
說明 :
作者查找資料時(shí)看到 網(wǎng)上多數(shù)都是此 封裝的文章, 對(duì)于 該方法出處 作者附上時(shí)間最早的鏈接 : iOS開發(fā)——密碼存儲(chǔ)之keychain的使用
#import "UserInfo.h"
@implementation UserInfo
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
return [NSMutableDictionary dictionaryWithObjectsAndKeys:(id)kSecClassGenericPassword,(id)kSecClass,
service, (id)kSecAttrService,
service, (id)kSecAttrAccount,
(id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
nil];
}
#pragma mark 寫入
// 說明: 該封裝 添加與更新 為同一方法, 不進(jìn)行判斷, 直接先刪除后添加
+ (void)save:(NSString *)service data:(id)data {
//Get search dictionary
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Delete old item before add new item
SecItemDelete((CFDictionaryRef)keychainQuery);
//Add new object to search dictionary(Attention:the data format)
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
//Add item to keychain with the search dictionary
SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}
#pragma mark 讀取
+ (id)load:(NSString *)service {
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Configure the search setting
//Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
@try {
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
} @catch (NSException *e) {
NSLog(@"Unarchive of %@ failed: %@", service, e);
} @finally {
}
}
if (keyData)
CFRelease(keyData);
return ret;
}
#pragma mark 刪除
+ (void)delete:(NSString *)service {
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
}
@end
NSString * const KEY_USERNAME_PASSWORD = @"com.company.app.usernamepassword";
NSString * const KEY_USERNAME = @"com.company.app.username";
NSString * const KEY_PASSWORD = @"com.company.app.password";
// 調(diào)用
NSMutableDictionary *userNamePasswordKVPairs = [NSMutableDictionary dictionary];
[userNamePasswordKVPairs setObject:@"userName" forKey:KEY_USERNAME];
[userNamePasswordKVPairs setObject:@"password" forKey:KEY_PASSWORD];
NSLog(@"%@", userNamePasswordKVPairs); //有KV值
// A、將用戶名和密碼寫入keychain
[UserInfo save:KEY_USERNAME_PASSWORD data:userNamePasswordKVPairs];
// B、從keychain中讀取用戶名和密碼
NSMutableDictionary *readUsernamePassword = (NSMutableDictionary *)[UserInfo load:KEY_USERNAME_PASSWORD];
NSString *userName = [readUsernamePassword objectForKey:KEY_USERNAME];
NSString *password = [readUsernamePassword objectForKey:KEY_PASSWORD];
NSLog(@"username = %@", userName);
NSLog(@"password = %@", password);
// C、將用戶名和密碼從keychain中刪除
[UserInfo delete:KEY_USERNAME_PASSWORD];
3.2
該封裝 已提供說明和 demo下載, 作者不贅述, 附上鏈接
傳送門 :iOS中Keychain保存用戶名和密碼
3.3
蘋果官方 demo 說明, 附上鏈接
四 Keychain 三方
傳送門 : soffes/SAMKeychain
五 Keychain App間共享數(shù)據(jù)
對(duì)于 App間共享數(shù)據(jù), 有篇文章已詳細(xì)介紹了, 作者不再贅述
傳送門 :iOS 開發(fā)keychain 使用與多個(gè)APP之間共享keychain數(shù)據(jù)的使用
對(duì)于該文章說的 開發(fā)者ID 為Apple developer中的Membership => Team ID, 或者可以使用 $(AppIdentifierPrefix) 代替, 也可以 用代碼獲取
- (NSString *)bundleSeedID {
NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
kSecClassGenericPassword, kSecClass,
@"bundleSeedID", kSecAttrAccount,
@"", kSecAttrService,
(id)kCFBooleanTrue, kSecReturnAttributes,
nil];
CFDictionaryRef result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&result);
if (status == errSecItemNotFound)
status = SecItemAdd((CFDictionaryRef)query, (CFTypeRef *)&result);
if (status != errSecSuccess)
return nil;
NSString *accessGroup = [(__bridge NSDictionary *)result objectForKey:kSecAttrAccessGroup];
NSArray *components = [accessGroup componentsSeparatedByString:@"."];
NSString *bundleSeedID = [[components objectEnumerator] nextObject];
CFRelease(result);
return bundleSeedID;
}
六 Keychain 的安全性
Keychain 并不是十分安全,在越獄的設(shè)備上,可以通過一些相應(yīng)的工具很輕松的 dump 所有的 Keychain 數(shù)據(jù),比如Keychain-Dumper,通過 ssh 登錄設(shè)備,下載 keychain_dumper 至 /tmp 目錄,然后 chmox +x keychain_dumper
賦予執(zhí)行權(quán)限,直接 ./keychain_dumper > keychain_content.txt
,即可查看到相應(yīng)的數(shù)據(jù)
Keychain 數(shù)據(jù)可以通過 iTunes 備份,iTunes 備份可以讓用戶選擇是否加密備份,不加密的備份可以恢復(fù)到任何設(shè)備,而加密的備份不能恢復(fù)到其它設(shè)備。雖然 ThisDeviceOnly 類型的 Item 不會(huì)備份,但是 Item 則會(huì)備份
iOS 7 之后,Keychain 數(shù)據(jù)還可以通過 iCloud 同步跨越多個(gè)設(shè)備。默認(rèn)情況下不同步,但是可以通過 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecAttrSynchronizable];
來設(shè)置同步,即使給 ThisDeviceOnly 設(shè)置同步,也不會(huì)生效
總而言之,考慮到 iCloud 服務(wù)器端、設(shè)備越獄等情況、甚至某些 Wi-FI 漏洞攻擊,都有可能會(huì)泄露 Keychain 數(shù)據(jù),最好是加密存儲(chǔ)
以 上 !