前言
Keychain 在 Mac 上大家都比較熟悉, 主要進行一些敏感信息存儲使用 如用戶名,密碼,網絡密碼,認證令牌, Wi-Fi網絡密碼,VPN憑證等. iOS 中 Keychain, 也有相同的功能實現 , 保存的信息存儲在設備中, 獨立于每個App沙盒之外. 作者這篇就簡單整理下iOS 中的 Keychain.
特點 :
1 . 更安全. 對比 NSUserDefault 存儲一些數據, 會更加安全.
2 . 即便 App 被卸載, 存儲的信息依舊存在, 再次安裝App, 存儲是信息依舊可以使用.
3 . 相同的 Team ID 開發, 可實現多個App 共享數據
一 Keychain 結構說明
Keychain 結構是由 key-value 組成.
多個 key-value 標簽的作用 : 表明該鑰匙的唯一性.
當我們對 Keychain 進行操作時, 就可以定義一個包含該 key-value 字典, 調用API, 對 Keychain 進行操作
結構說明
圖片引用 : http://www.lxweimin.com/p/fa87b6879b99
二 Keychain 增 / 刪 / 改 / 查
對Keychain 的結構有了簡單印象, 那么就可以對其進行操作.
主要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 {
/**
說明:當添加的時候我們一般需要判斷一下當前鑰匙串里面是否已經存在我們要添加的鑰匙。如果已經存在我們就更新好了,不存在再添加,所以這兩個操作一般寫成一個函數搞定吧。
過程關鍵:1.檢查是否已經存在 構建的查詢用的操作字典:kSecAttrService,kSecAttrAccount,kSecClass(標明存儲的數據是什么類型,值為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{
//先查查是否已經存在
//構造一個操作字典用于查詢
NSMutableDictionary *queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:service forKey:(__bridge id)kSecAttrService]; //標簽service
[queryDic setObject:account forKey:(__bridge id)kSecAttrAccount]; //標簽account
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存儲的是一個密碼
OSStatus status = -1;
CFTypeRef result = NULL;
status = SecItemCopyMatching((__bridge CFDictionaryRef)queryDic, &result);
if (status == errSecItemNotFound) { //沒有找到則添加
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 轉換為 NSData
[queryDic setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密碼
status = SecItemAdd((__bridge CFDictionaryRef)queryDic, NULL); //!!!!!關鍵的添加API
}else if (status == errSecSuccess){ //成功找到,說明鑰匙已經存在則進行更新
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; //把password 轉換為 NSData
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:queryDic];
[dict setObject:passwordData forKey:(__bridge id)kSecValueData]; //添加密碼
status = SecItemUpdate((__bridge CFDictionaryRef)queryDic, (__bridge CFDictionaryRef)dict);//!!!!關鍵的更新API
}
return (status == errSecSuccess);
}
/** 查 */
- (IBAction)select:(id)sender {
/**
過程:
1.(關鍵)先配置一個操作字典內容有:
kSecAttrService(屬性),kSecAttrAccount(屬性) 這些屬性or標簽是查找的依據
kSecReturnData(值為@YES 表明返回類型為data),kSecClass(值為kSecClassGenericPassword 表示重要數據為“一般密碼”類型) 這些限制條件是返回結果類型的依據
2.然后用查找的API 得到查找狀態和返回數據(密碼)
3.最后如果狀態成功那么將數據(密碼)轉換成string 返回
*/
NSLog(@"%@", [self passwordForService:@"com.tencent" account:@"李雷"]);
}
//用原生的API 實現查詢密碼
- (NSString *)passwordForService:(nonnull NSString *)service account:(nonnull NSString *)account{
//生成一個查詢用的 可變字典
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) { //判斷狀態
return nil;
}
//返回數據
// NSString *password = [[NSString alloc] initWithData:(__bridge_transfer NSData *)result encoding:NSUTF8StringEncoding];//轉換成string
//刪除kSecReturnData鍵; 我們不需要它了:
[queryDic removeObjectForKey:(__bridge id)kSecReturnData];
//將密碼轉換為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]; //標簽service
[queryDic setObject:account forKey:(__bridge id)kSecAttrAccount]; //標簽account
[queryDic setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];//表明存儲的是一個密碼
OSStatus status = SecItemDelete((CFDictionaryRef)queryDic);
return (status == errSecSuccess);
}
三 Keychain 封裝
3.1
說明 :
作者查找資料時看到 網上多數都是此 封裝的文章, 對于 該方法出處 作者附上時間最早的鏈接 : iOS開發——密碼存儲之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 寫入
// 說明: 該封裝 添加與更新 為同一方法, 不進行判斷, 直接先刪除后添加
+ (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";
// 調用
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間共享數據
對于 App間共享數據, 有篇文章已詳細介紹了, 作者不再贅述
傳送門 :iOS 開發keychain 使用與多個APP之間共享keychain數據的使用
對于該文章說的 開發者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 并不是十分安全,在越獄的設備上,可以通過一些相應的工具很輕松的 dump 所有的 Keychain 數據,比如Keychain-Dumper,通過 ssh 登錄設備,下載 keychain_dumper 至 /tmp 目錄,然后 chmox +x keychain_dumper
賦予執行權限,直接 ./keychain_dumper > keychain_content.txt
,即可查看到相應的數據
Keychain 數據可以通過 iTunes 備份,iTunes 備份可以讓用戶選擇是否加密備份,不加密的備份可以恢復到任何設備,而加密的備份不能恢復到其它設備。雖然 ThisDeviceOnly 類型的 Item 不會備份,但是 Item 則會備份
iOS 7 之后,Keychain 數據還可以通過 iCloud 同步跨越多個設備。默認情況下不同步,但是可以通過 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecAttrSynchronizable];
來設置同步,即使給 ThisDeviceOnly 設置同步,也不會生效
總而言之,考慮到 iCloud 服務器端、設備越獄等情況、甚至某些 Wi-FI 漏洞攻擊,都有可能會泄露 Keychain 數據,最好是加密存儲
以 上 !