iOS Keychain使用說明

前言

KeychainMac 上大家都比較熟悉, 主要進(jìn)行一些敏感信息存儲(chǔ)使用 如用戶名,密碼,網(wǎng)絡(luò)密碼,認(rèn)證令牌, Wi-Fi網(wǎng)絡(luò)密碼,VPN憑證等. iOSKeychain, 也有相同的功能實(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);
}

引用鏈接 : iOS Keychain,SSKeychain,使用 理解 原理

三 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 說明, 附上鏈接

傳送門 : iOS Keychain Services Tasks

四 Keychain 三方

傳送門 : soffes/SAMKeychain

五 Keychain App間共享數(shù)據(jù)

對(duì)于 App間共享數(shù)據(jù), 有篇文章已詳細(xì)介紹了, 作者不再贅述
傳送門 :iOS 開發(fā)keychain 使用與多個(gè)APP之間共享keychain數(shù)據(jù)的使用

對(duì)于該文章說的 開發(fā)者IDApple 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ǔ)

鏈接: iOS 安全儲(chǔ)存數(shù)據(jù)(三):Keychain Services

以 上 !

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評(píng)論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,835評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,459評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,819評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,004評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,257評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評(píng)論 1 288
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,717評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,003評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)...
    香橙柚子閱讀 24,000評(píng)論 8 183
  • iOS設(shè)備中的Keychain是一個(gè)安全的存儲(chǔ)容器,可以用來為不同應(yīng)用保存敏感信息比如用戶名,密碼,網(wǎng)絡(luò)密碼,認(rèn)證...
    像小強(qiáng)一樣活著閱讀 5,789評(píng)論 0 4
  • 喋喋_edbb閱讀 135評(píng)論 0 2
  • 1. 使用宏定義進(jìn)行打印### 2. 使用格式化字符串進(jìn)行打印### 在SDK中可能無法使用第一種打印方式, 可以...
    codeWorm閱讀 2,850評(píng)論 0 0
  • 生命中,總有一些觸動(dòng)我們靈魂的東西,需要用文字記錄下來,讓它留在歲月的河流里。——題記 故事從下圖這張紙片講起……...
    凡水伊人的凡閱讀 34,323評(píng)論 0 2