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,存储的属性有区别,使用上都是一样的。
不同类型对应的属性:
既然苹果是采用SQLite去存储的,那么以上这些不同item的attribute可以理解是数据库里面表的字段。那么对keychain的操作其实也就是普通数据库的增删改查了。这样也许就会觉得那些API也没那么难用了。
增
1 2 3 4 5 6 7 8 9 10 |
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。
删
1 2 3 4 5 6 7 |
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;
改
1 2 3 4 5 6 7 |
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。
查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 (errSecSuccess == status) { 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 (errSecSuccess == status) { 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。
添加:
1 2 3 4 5 6 7 8 9 10 11 12 |
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); |
取:
1 2 3 4 5 6 7 8 9 10 11 12 |
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代码:
1 2 3 4 5 6 7 8 9 10 |
<?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对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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,用来说明调用意图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 (errSecSuccess == status) { 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。
如何使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
//生成ECC公私钥 CFErrorRef error = NULL; SecAccessControlCreateFlags accessCtrlFlags = kSecAccessControlPrivateKeyUsage; #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_11_3 if (@available(iOS 11.3, macOS 10.13.4,*)) { accessCtrlFlags |= kSecAccessControlBiometryAny; } else { accessCtrlFlags |= kSecAccessControlTouchIDAny; } #else accessCtrlFlags |= kSecAccessControlBiometryAny; #endif SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessCtrlFlags, &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", }, }; #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0,*)) { SecKeyRef keyPair = SecKeyCreateRandomKey((__bridge CFDictionaryRef)params, &error); if(keyPair) { CFRelease(keyPair); } } else { SecKeyRef publicKey, privateKey; OSStatus status = SecKeyGeneratePair((__bridge CFDictionaryRef)params, &publicKey, &privateKey); if (errSecSuccess == status) { CFRelease(privateKey); CFRelease(publicKey); } } #else SecKeyRef keyPair = SecKeyCreateRandomKey((__bridge CFDictionaryRef)params, &error); if(keyPair) { CFRelease(keyPair); } #endif // 处理报错 // [self handleError:status]; //签名 const NSMutableDictionary* query = [NSMutableDictionary dictionaryWithDictionary:@{ (__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, }]; NSString* signPrompt = @"签名数据"; // defined in Availability.h #if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_14_0) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0) if (@available(iOS 11.0, macOS 10.13,*)) { //#import <LocalAuthentication/LocalAuthentication.h> LAContext *context = [[LAContext alloc] init]; context.localizedReason = signPrompt; [query setObject:context forKey:(__bridge id)kSecUseAuthenticationContext]; } else { [query setObject:signPrompt forKey:(__bridge id)kSecUseOperationPrompt]; } #else //#import <LocalAuthentication/LocalAuthentication.h> LAContext *context = [[LAContext alloc] init]; context.localizedReason = signPrompt; [query setObject:context forKey:(__bridge id)kSecUseAuthenticationContext]; #endif 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 (errSecSuccess == status) { #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0,*)) { CFErrorRef err; NSData *dataToSign = [@"我是签名内容" dataUsingEncoding:NSUTF8StringEncoding]; CFDataRef ref = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)dataToSign, &err); if (ref) { NSData * sign = CFBridgingRelease(ref); NSLog(@"sign success %@", sign); } } else { // 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 (errSecSuccess == status) { NSLog(@"sign success"); } } #else CFErrorRef err; NSData *dataToSign = [@"我是签名内容" dataUsingEncoding:NSUTF8StringEncoding]; CFDataRef ref = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)dataToSign, &err); if (ref) { NSData * sign = CFBridgingRelease(ref); NSLog(@"sign success %@", sign); } #endif CFRelease(privateKey); } else { } }); |
以上代码就是生成了一对公私钥(ECC 256),私钥会保存在SE中,而公钥交给应用程序。签名操作的时候,好像我们取到了私钥,但是实际上我们并不能拿到私钥,只是私钥在SE中的一个引用。加密的操作也是在SE中完成,最后返回给我们签名的数据。
苹果在这边举了个简单例子,如何利用Touch ID进行登录。客户端生成一对公私钥,公钥发给服务器,客户端在通过Touch ID校验后,加密一段内容(私钥签名操作),将内容和结果发送给服务器,服务器取出公钥进行验签。如果一致,则通过验证。
如果需要重新创建同名的密钥对,则需要先删除之前的密钥对,否则不能生效,删除的代码参考如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 公钥私钥一起删除 NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassKey, (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPublic, (__bridge id)kSecAttrLabel: @"ECCKey", (__bridge id)kSecReturnRef: @YES, }; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); if (errSecSuccess == status) { NSLog(@"delete success"); } |
上面的代码,在调用 SecKeyRawSign 的时候,需要指纹/人脸确认,才能进行加密操作。但是有时候,我们只希望Secure Enclave 生成安全密钥,并不希望密钥与指纹/人脸关联(比如用来网络通信的加解密,不可能每次网络请求都让用户指纹确认)。我们只需要在创建密钥的时候,去掉验证的时候对于指纹/人脸的要求(移除 kSecAccessControlTouchIDAny/kSecAccessControlBiometryAny),同时去掉对于解锁才能访问的要求(kSecAttrAccessibleWhenUnlockedThisDeviceOnly 替换为: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)。
代码参考如下(如果上面已经生成过密钥,需要先执行一次删除操作):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
// 删除已经生成过的密钥 //生成ECC公私钥 CFErrorRef error = NULL; SecAccessControlRef accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, kSecAccessControlPrivateKeyUsage, &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", }, }; #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0,*)) { SecKeyRef keyPair = SecKeyCreateRandomKey((__bridge CFDictionaryRef)params, &error); if(keyPair) { CFRelease(keyPair); } } else { SecKeyRef publicKey, privateKey; OSStatus status = SecKeyGeneratePair((__bridge CFDictionaryRef)params, &publicKey, &privateKey); if (errSecSuccess == status) { CFRelease(privateKey); CFRelease(publicKey); } } #else SecKeyRef keyPair = SecKeyCreateRandomKey((__bridge CFDictionaryRef)params, &error); if(keyPair) { CFRelease(keyPair); } #endif // 处理报错 // [self handleError:status]; //签名 const NSMutableDictionary* query = [NSMutableDictionary dictionaryWithDictionary:@{ (__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, }]; NSString* signPrompt = @"签名数据"; // defined in Availability.h #if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_14_0) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0) if (@available(iOS 11.0, macOS 10.13,*)) { //#import <LocalAuthentication/LocalAuthentication.h> LAContext *context = [[LAContext alloc] init]; context.localizedReason = signPrompt; [query setObject:context forKey:(__bridge id)kSecUseAuthenticationContext]; } else { [query setObject:signPrompt forKey:(__bridge id)kSecUseOperationPrompt]; } #else //#import <LocalAuthentication/LocalAuthentication.h> LAContext *context = [[LAContext alloc] init]; context.localizedReason = signPrompt; [query setObject:context forKey:(__bridge id)kSecUseAuthenticationContext]; #endif 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 (errSecSuccess == status) { #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_15_0 if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0,*)) { CFErrorRef err; NSData *dataToSign = [@"我是签名内容" dataUsingEncoding:NSUTF8StringEncoding]; CFDataRef ref = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)dataToSign, &err); if (ref) { NSData * sign = CFBridgingRelease(ref); NSLog(@"sign success %@", sign); } } else { // 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 (errSecSuccess == status) { NSLog(@"sign success"); } } #else CFErrorRef err; NSData *dataToSign = [@"我是签名内容" dataUsingEncoding:NSUTF8StringEncoding]; CFDataRef ref = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)dataToSign, &err); if (ref) { NSData * sign = CFBridgingRelease(ref); NSLog(@"sign success %@", sign); } #endif CFRelease(privateKey); } else { } }); |
上述代码如果报错,错误代码详情可以从 SecBase.h 文件查询。
如果需要导出公钥然后传递给服务器进行加密数据,可以参考如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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, }; // Retrieve the key from the keychain. No authentication is needed at this point. SecKeyRef privateKey; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey); if (errSecSuccess == status) { SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); CFRelease(privateKey); if(publicKey) { CFErrorRef error = nil; CFDataRef data = SecKeyCopyExternalRepresentation(publicKey, &error); CFRelease(publicKey); if(data) { NSData * publicKeyData = (__bridge NSData *)data; NSString *publicKeyBase64 = [publicKeyData base64EncodedStringWithOptions : 0]; CFRelease(data); NSLog(@"%@", publicKeyBase64); } } } |
上述代码只能在 iOS 10.0以及之后的系统才能调用。如果要兼容之前的系统,则需要在创建密钥对的时候,立即从返回的公钥引用中导出。
执行如下代码(低于 iOS 10.0 的系统密钥对创建完成后,无法直接查询公钥,只能查询私钥,导致公钥无法二次导出):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
- (void) exportPublicKeyFromRef:(SecKeyRef)publicKey { NSData * data = nil; #if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_12) if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0,*)) { CFErrorRef error = nil; CFDataRef dataRef = SecKeyCopyExternalRepresentation(publicKey, &error); if(dataRef) { data = CFBridgingRelease(dataRef); } } else { NSDictionary* dict = @{ (__bridge id)kSecClass :(__bridge id)kSecClassKey, (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPublic, (__bridge id)kSecValueRef: (__bridge id)publicKey, (__bridge id)kSecAttrApplicationTag:@"customTag", (__bridge id)kSecAttrIsPermanent:@YES, (__bridge id)kSecReturnData:@YES, }; CFTypeRef dataRef; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dict, &dataRef); if (errSecDuplicateItem == status){ SecItemDelete((__bridge CFDictionaryRef)dict); status = SecItemAdd((__bridge CFDictionaryRef)dict, &dataRef); } if(dataRef && (errSecSuccess == status)) { data = CFBridgingRelease(dataRef); } } #else CFErrorRef error = nil; CFDataRef dataRef = SecKeyCopyExternalRepresentation(publicKey, &error); if(dataRef) { data = CFBridgingRelease(dataRef); } #endif if(data) { NSString *publicKeyBase64 = [data base64EncodedStringWithOptions : 0]; NSLog(@"%@",publicKeyBase64); } } |
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
参考链接
- iOS - keychain 详解及变化
- iOS keychain sharing 详细步骤
- iOS -- keyChain
- Signing data with kSecAttrKeyTypeEC key on iOS
- 苹果官方告诉你,Touch ID是怎么保护数据的?
- iOS使用Security.framework进行RSA 加密解密签名和验证签名
- 关于Secure Enclave的一些杂乱想法
- Storing Keys in the Secure Enclave
- 安全隔区
- Secure Enclave in iOS App
- Demystifying the Secure Enclave Processor
- Storing Keys in the Secure Enclave
- Keychain 浅析(示例代码)
- agens-no/EllipticCurveKeyPair
- iOS钥匙串:Secure Enclave加密存储的原理
- How do I encode an unmanaged<SecKey> to base64 to send to another server?
- Signing data with kSecAttrKeyTypeEC key on iOS
- iOS - ECC椭圆曲线、ECDSA签名验签和ECIES加解密本文来源
- iOS9 集成指纹解锁
- Use of LAContext requires LocalAuthentication framework linked in
- LocalAuthentication开发实践
- trailofbits / SecureEnclaveCrypto