iOS - keychain 詳解及變化

keychain介紹

iOS keychain 是一個相對獨立的空間, 保存到keychain鑰匙串中的信息不會由於卸載/重裝app而丟失, 。相對於NSUserDefaults、plist文件保存等通常方式,keychain保存更爲安全。因此咱們會用keyChain保存一些私密信息,好比密碼、證書、設備惟一碼( 把獲取到用戶設備的惟一ID 存到keychain 裏面這樣卸載或重裝以後還能夠獲取到id,保證了一個設備一個ID)等等。keychain是用SQLite進行存儲的。用蘋果的話來講是一個專業的數據庫,加密咱們保存的數據,能夠經過metadata(attributes)進行高效的搜索。keychain適合保存一些比較小的數據量的數據,若是要保存大的數據,能夠考慮文件的形式存儲在磁盤上,在keychain裏面保存解密這個文件的密鑰。
 

keychain的基本使用

keychain的類型

  • kSecClassGenericPassword
  • kSecClassInternetPassword
  • kSecClassCertificate
  • kSecClassKey
  • kSecClassIdentity

這5個類型只是對應於不一樣的item,存儲的屬性有區別,使用上都是同樣的。數據庫

不一樣類型對應的屬性:數組

14658371868385.jpg
14658371868385.jpg

既然蘋果是採用SQLite去存儲的,那麼以上這些不一樣item的attribute能夠理解是數據庫裏面表的字段。那麼對keychain的操做其實也就是普通數據庫的增刪改查了。這樣也許就會以爲那些API也沒那麼難用了。安全

NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding],
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };
   
    CFErrorRef error = NULL;
   
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);

 

以這個添加kSecClassGenericPassword item爲例,在字典裏面咱們設置瞭如下幾個屬性:獲取權限爲當設備處於未鎖屏狀態,item的類型爲kSecClassGenericPassword,item的value爲@"123456", item的帳戶名爲@"account name", item的service爲@"loginPassword"。最後,調用SecItemAdd進行插入。使用上有點像CoreData。服務器

NSDictionary *query = @{
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecAttrService : @"loginPassword",
                            (__bridge id)kSecAttrAccount : @"account name"
                            };
    
    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);

 

刪除一樣也是指定以前存的item的屬性,最後調用SecItemDelete這個方法。這邊要注意的是勁量用多個字段肯定這個item,(雖然日常開發均可能是惟一)防止刪除了其餘item;好比咱們把kSecAttrAccount這個屬性去掉,那麼將會刪除全部的kSecAttrService對應value爲@"loginPassword"的item;app

NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };
    NSDictionary *update = @{
                             (__bridge id)kSecValueData : [@"654321" dataUsingEncoding:NSUTF8StringEncoding],
                             };
    
    OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);

 

蘋果推薦咱們用SecItemUpdate去修改一個已經存在的item,可能咱們喜歡先調用SecItemDelete方法去刪除,再添加一個新的。這個主要目的是防止新添的item丟失了原來的部分屬性。這個方法須要兩個入參,一個字典是用來指定要更新的item,另外一個字典是想要更新的某個屬性的value,最後調用SecItemUpdate。框架

NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };
    
    CFTypeRef dataTypeRef = NULL;
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
    
    if (status == errSecSuccess) {
        NSString *pwd = [[NSString alloc] initWithData:(__bridge NSData * _Nonnull)(dataTypeRef) encoding:NSUTF8StringEncoding];
        NSLog(@"==result:%@", pwd);
    }

 

查和前面幾個操做相似,首先一樣是指定屬性定位到這個item,最後調用SecItemCopyMatching方法。既然是數據庫查詢,確定會有記錄的條數的問題。本例中使用了kSecMatchLimitOne,表示返回結果集的第一個,固然這個也是默認的。若是是查詢出多個,kSecMatchLimitAll可使用這個,那麼返回的將是個數組。SecItemCopyMatching方法的入參dataTypeRef,是一個返回結果的引用,會根據不一樣的item,返回對應不一樣的類型(如NSCFData, NSCFDictionary, NSCFArray等等)。async

剛剛上面是返回存儲的value的引用,若是咱們想看看這個item全部的屬性怎麼辦?咱們可使用kSecReturnRefide

 NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnRef : @YES,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"noraml",
                            };
    
    CFTypeRef dataTypeRef = NULL;
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
    
    if (status == errSecSuccess) {
        NSDictionary *dict = (__bridge NSDictionary *)dataTypeRef;
        NSString *acccount = dict[(id)kSecAttrAccount];
        NSData *data = dict[(id)kSecValueData];
        NSString *pwd = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSString *service = dict[(id)kSecAttrService];
        NSLog(@"==result:%@", dict);
    }

 

這樣,咱們就獲得了這個item的全部屬性。測試

Sharing Items

同一個開發者帳號下(teamID),各個應用之間能夠共享item。keychain經過keychain-access-groups
來進行訪問權限的控制。在Xcode的Capabilities選項中打開Keychain Sharing便可。ui

 

每一個group命名開頭必須是開發者帳號的teamId。不一樣開發者帳號的teamId是惟一的,因此蘋果限制了只有同一個開發者帳號下的應用才能夠進行共享。若是有多個sharedGroup,在添加的時候若是不指定,默認是第一個group

添加:

NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding],
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test",
                            (__bridge id)kSecAttrService : @"noraml1",
                            (__bridge id)kSecAttrSynchronizable : @YES,
                            };
    
    CFErrorRef error = NULL;
    
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);

 

取:

NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnRef : @YES,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test",
                            (__bridge id)kSecAttrService : @"noraml1",
                            };
    
    CFTypeRef dataTypeRef = NULL;
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);

 

只須要添加一個kSecAttrAccessGroup屬性便可。

 

 

 

APP對keychain的訪問權限:

(1)未對應用APP的entitlement(受權)進行配置時,APP使用鑰匙串存儲時,會默認存儲在自身BundleID的條目下。

(2)對APP的entitlement(受權)進行配置後,說明APP有了對某個條目的訪問權限。

 

鑰匙串的可視化效果可參見Mac的APP-鑰匙串訪問。

 

APP鑰匙串訪問權限的配置方法:(這裏XXXXX模擬器隨意,但真機必須爲本身開發者帳號ID,不然沒法經過編譯)

1.新建一個Plist文件,在Plist中的數組中添加能夠訪問的條目的名字(如KeychainAccessGroups.plist),結構以下:

Plist代碼:

複製代碼
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>keychain-access-groups</key>
    <array>
        <string>XXXXX.GrassInfoAppFamily</string>
    </array>
</dict>
</plist>
複製代碼

2.在Build-setting中進行配置,搜索entitlement,注意路徑別配置錯:

 

 

 

keychain安全方面的一些東西。

kSecAttrAccessible

這個屬性,決定了咱們item在什麼條件下能夠獲取到裏面的內容,咱們在添加item的時候,能夠添加這個屬性,來加強數據的安全性,具體的主要有如下幾個:

  • kSecAttrAccessibleWhenUnlocked

  • kSecAttrAccessibleAfterFirstUnlock

  • kSecAttrAccessibleAlways

  • kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly

  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly

  • kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly

  • kSecAttrAccessibleAlwaysThisDeviceOnly

每一個意思都很明確,item默認就是kSecAttrAccessibleWhenUnlocked。也就是在設備未鎖屏的狀況下。這個也是蘋果推薦的。kSecAttrAccessibleAlways,這個蘋果在WWDC中也說了,不建議使用,蘋果本身已經都棄用了。kSecAttrAccessibleAfterFirstUnlock這個是在設備第一次解鎖後,可使用。這個最多見的就是後臺喚醒功能裏面,若是須要訪問某個item,那麼須要使用這個屬性,否則是訪問不了item的數據的。最後幾個DeviceOnly相關的設置,若是設置了,那麼在手機備份恢復到其餘設備時,是不能被恢復的。一樣iCloud也不會同步到其餘設備,由於在其餘設備上是解密不出來的。

iCloud

keychain item能夠備份到iCloud上,咱們只須要在添加item的時候添加@{(__bridge id)kSecAttrSynchronizable : @YES,}。若是想同步到其餘設備上也能使用,請避免使用DeviceOnly設置或者其餘和設備相關的控制權限。

Access Control

ACL是iOS8新增的API,iOS9以後對控制權限進行了細化。在原來的基礎上加了一層本地驗證,主要是配合TouchID一塊兒使用。對於咱們使用者來講,在以前的item操做是同樣的,只是在添加的時候,加了一個SecAccessControlRef對象。

CFErrorRef error = NULL;
    SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                                        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                                                        kSecAccessControlUserPresence,
                                                                        &error);
    if (error) {
        NSLog(@"failed to create accessControl");
        return;
    }
    
    NSDictionary *query = @{
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecValueData : [@"accesscontrol test" dataUsingEncoding:NSUTF8StringEncoding],
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"accesscontrol",
                            (__bridge id)kSecAttrAccessControl : (__bridge id)accessControl,
                            };
    
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);

 

咱們只須要建立SecAccessControlRef對象,主要是兩個參數,一個是kSecAttrAccessible,另外一個是SecAccessControlCreateFlags。在字典裏面添加(__bridge id)kSecAttrAccessControl : (__bridge id)accessControl便可。

SecAccessControlCreateFlags:

  • kSecAccessControlUserPresence

    item經過鎖屏密碼或者Touch ID進行驗證,Touch ID能夠不設置,增長或者移除手指都能使用item。

  • kSecAccessControlTouchIDAny

    item只能經過Touch ID驗證,Touch ID 必須設置,增長或移除手指都能使用item。

  • kSecAccessControlTouchIDCurrentSet

    item只能經過Touch ID進行驗證,增長或者移除手指,item將被刪除。

  • kSecAccessControlDevicePasscode

    item經過鎖屏密碼驗證訪問。

  • kSecAccessControlOr

    若是設置多個flag,只要有一個知足就能夠。

  • kSecAccessControlAnd

    若是設置多個flag,必須全部的都知足才行。

  • kSecAccessControlPrivateKeyUsage

    私鑰簽名操做

  • kSecAccessControlApplicationPassword

    額外的item密碼,可讓用戶本身設置一個訪問密碼,這樣只有知道密碼才能訪問。

獲取操做和之前的都是同樣的,只是加了一個提示信息kSecUseOperationPrompt,用來講明調用意圖:

 NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecAttrService : @"accesscontrol",
                            (__bridge id)kSecUseOperationPrompt : @"獲取存儲密碼",
                            };
    
    CFTypeRef dataTypeRef = NULL;
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);
    
    if (status == errSecSuccess) {
        
        NSString *pwd = [[NSString alloc] initWithData:(__bridge NSData * _Nonnull)(dataTypeRef) encoding:NSUTF8StringEncoding];
        
        NSLog(@"==result:%@", pwd);
    }

 

 
 

Secure Enclave

Secure Enclave 首次出如今iPhone 5s中,就是協處理器M7,用來保護指紋數據。SE裏面的數據咱們用戶層面代碼是訪問不了的,哪怕系統越獄了,也沒法訪問到裏面數據。只有特定的代碼才能去訪問(CPU 切換成Monitor Mode)。SE自己也集成了加密庫,加密解密相關的都在SE內部完成,這樣應用程序只能拿到最後的結果,而沒法拿到原始的數據。(關於Secure Enclave 能夠搜些資料瞭解下,這裏就不展開了)。在iOS9以後蘋果開放了一個新的屬性:kSecAttrTokenIDSecureEnclave,也就是將數據保存到SE裏面,固然只是key。

如何使用:

//生成ECC公私鑰

CFErrorRef error = NULL;
    SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                                        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                                                        kSecAccessControlPrivateKeyUsage | kSecAccessControlTouchIDAny,
                                                                        &error);
    if (error) {
        NSLog(@"failed to create accessControl");
        return;
    }
    
    NSDictionary *params = @{
                             (__bridge id)kSecAttrTokenID: (__bridge id)kSecAttrTokenIDSecureEnclave,
                             (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeEC,
                             (__bridge id)kSecAttrKeySizeInBits: @256,
                             (__bridge id)kSecPrivateKeyAttrs: @{
                                     (__bridge id)kSecAttrAccessControl: (__bridge_transfer id)accessControl,
                                     (__bridge id)kSecAttrIsPermanent: @YES,
                                     (__bridge id)kSecAttrLabel: @"ECCKey",
                                     },
                             };
    
    SecKeyRef publickKey, privateKey;
    
    OSStatus status = SecKeyGeneratePair((__bridge CFDictionaryRef)params, &publickKey, &privateKey);
    
    [self handleError:status];
    
    if (status == errSecSuccess) {
        CFRelease(privateKey);
        CFRelease(publickKey);
    }

//簽名

 NSDictionary *query = @{
                            (__bridge id)kSecClass: (__bridge id)kSecClassKey,
                            (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
                            (__bridge id)kSecAttrLabel: @"ECCKey",
                            (__bridge id)kSecReturnRef: @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecUseOperationPrompt: @"簽名數據"
                            };
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Retrieve the key from the keychain.  No authentication is needed at this point.
        SecKeyRef privateKey;
        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
        
        if (status == errSecSuccess) {
            // Sign the data in the digest/digestLength memory block.
            uint8_t signature[128];
            size_t signatureLength = sizeof(signature);
            uint8_t digestData[16];
            size_t digestLength = sizeof(digestData);
            status = SecKeyRawSign(privateKey, kSecPaddingPKCS1, digestData, digestLength, signature, &signatureLength);
            
            if (status == errSecSuccess) {
                NSLog(@"sign success");
            }
            
            CFRelease(privateKey);
        }
        else {
            
        }
    });

 

以上代碼就是生成了一對公私鑰(ECC 256),私鑰會保存在SE中,而公鑰交給應用程序。簽名操做的時候,好像咱們取到了私鑰,可是實際上咱們並不能拿到私鑰,只是私鑰在SE中的一個引用。加密的操做也是在SE中完成,最後返回給咱們簽名的數據。
蘋果在這邊舉了個簡單例子,如何利用Touch ID進行登陸。客戶端生成一對公私鑰,公鑰發給服務器,客戶端在經過Touch ID校驗後,加密一段內容(私鑰簽名操做),將內容和結果發送給服務器,服務器取出公鑰進行驗籤。若是一致,則經過驗證。

item解密過程

 
 

上面這個圖就是普通item的一個解密流程。應用程序經過API訪問item,在keychain裏面取出加密的item,將加密的item,傳遞給SE解密,解密完返回給keychain,最後返回給應用。

iOS8後,蘋果將中間的keychain框架進行了拆分,增長了本地受權認證:

 
 

這個最大的用途就是和Touch ID進行結合,來提升咱們的數據安全性。當咱們取item的時候,若是須要Touch ID進行驗證,在SE裏面,若是經過驗證那麼將對數據進行解密,並返回給keychain,最後返回給應用程序。

iOS9以後的keyStore也放進了SE裏面,進一步提升了安全性。至於keychain的安全性在非越獄下的確是安全的,可是一旦手機越獄,應用能夠訪問到其餘應用程序item,或者經過Keychain-Dumper導出keychain數據,那麼就不是很安全了。因此在咱們存進鑰匙串的數據,不要直接存一些敏感信息,在程序中加一層數據保護。

參考:
安全白皮書
Keychain and Authentication with Touch ID
Protecting Secrets with the Keychain
Security and Your Apps

//一下的是有些人說跟新了 可是我測試了一下 刪除以後 keychain裏面仍是有數據的

我在官方文檔中並未找到相關的更新:https://developer.apple.com/documentation/security/keychain_services 

你們仍是能夠放心用的

iOS 10.3 還未正式發佈,beta 版中一個關於keychain 特性的小修改,就已經引發了普遍的關注。
  改動以下:
若是 App 被刪除,以前存儲於 keychain 中的數據也會一同被清除。
若是使用了 keychain group,只要當 group 全部相關的 App 被刪除時,keychain 中的數據纔會被刪除。
  這一改動,雖未經官方公佈。但已在論壇帖子裏獲得了 Apple 員工的確認,原文以下:

This is an intentional change in iOS 10.3 to protect user privacy. Information that can identify a user should not be left on the device after the app that created it has been removed.
It has never been a part of the API contract that keychain items created by an app would survive when the app is removed. This has always been an implementation detail.
If a keychain item is shared with other apps, it won’t be deleted until those other apps have been deleted as well.

若是這是這樣的話,那麼keychain存在還有什麼意義麼?
還有蘋果如今愈來愈注重用戶的隱私,就前幾天對於使用JSPatch熱更新的機制的應用發送的郵件來看,蘋果彷佛要在這方面有動做了,我想說,蘋果爸爸此次難道真的要爲Swift和OC兩個親兒子出頭了嗎?

其實我也以爲 app 都刪了 keychain 還在是挺不合理的一件事兒。在隱私保護上仍是能夠看得出 Apple 仍是一直在做爲。

因爲蘋果頻繁的更新,以前的一些東西已經不能使用https://forums.developer.apple.com/message/210531#210531

相關文章
相關標籤/搜索