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,存储的属性有区别,使用上都是一样的。

不同类型对应的属性:

既然苹果是采用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 (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

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

添加:

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 (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。

如何使用:

    //生成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校验后,加密一段内容(私钥签名操作),将内容和结果发送给服务器,服务器取出公钥进行验签。如果一致,则通过验证。

如果需要重新创建同名的密钥对,则需要先删除之前的密钥对,否则不能生效,删除的代码参考如下:

    // 公钥私钥一起删除
    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)。

代码参考如下(如果上面已经生成过密钥,需要先执行一次删除操作):

    // 删除已经生成过的密钥

    //生成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 文件查询。

如果需要导出公钥然后传递给服务器进行加密数据,可以参考如下代码:

    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 的系统密钥对创建完成后,无法直接查询公钥,只能查询私钥,导致公钥无法二次导出):

- (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

参考链接


发布者

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注