iOS - keychain 详解及变化
keychain介绍
keychain的基本使用
keychain的类型
- kSecClassGenericPassword
- kSecClassInternetPassword
- kSecClassCertificate
- kSecClassKey
- kSecClassIdentity
这5个类型只是对应于不同的item,存储的属性有区别,使用上都是一样的。
不同类型对应的属性:
既然苹果是采用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;
改
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等等)。
刚刚上面是返回存储的value的引用,如果我们想看看这个item所有的属性怎么办?我们可以使用kSecReturnRef
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即可。
每个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