James Yu


写了那么多的if else 依然未能参透其精髓。。。


keychain(二)

上一篇主要讲了keychain的基本使用,这篇主要讲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

comments powered by Disqus