iOS中实现Secure Enclave生物识别+应用密码Fallback的签名认证方案
需求目标
我需要在iOS Swift应用中实现类似主流银行APP的本地认证流程,用于访问Secure Enclave中的密钥,具体需求如下:
- 默认设置应用专属PIN码
- 用户可开启生物识别(Face ID或Touch ID)实现快速认证
- 使用Secure Enclave中的私钥签名消息时,支持选择生物识别;若用户取消生物识别或识别失败,可回退到输入应用专属PIN码完成认证
已尝试方案
SecAccessControlCreateFlags中的.applicationPassword标志看起来是实现应用自定义密码认证的合理选项,同时该标志集还包含devicePasscode、biometryCurrentSet等约束,可通过逻辑连词组合。但需要注意,文档明确.applicationPassword是一个“选项”而非约束,且相关功能描述不够详细。
我尝试了两种访问控制配置:
let flags1: SecAccessControlCreateFlags = [.privateKeyUsage, .biometryCurrentSet, .or, .applicationPassword] let flags2: SecAccessControlCreateFlags = [.privateKeyUsage, .biometryCurrentSet, .applicationPassword] let access1 = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags1, &error) let access2 = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags2, &error)
两种配置的表现基本一致:系统会先弹出生物识别验证,再显示应用密码输入框。我尝试通过LAContext.setCredential以编程方式传入应用密码,此时密码输入框不再显示,但iOS仍强制要求完成生物识别验证——即使使用了.or标志。这说明.or和.applicationPassword的组合没有达到预期的“二选一”效果,当前策略似乎要求同时通过生物识别和应用密码验证,这不符合我的需求。另外,我尝试用LAContext.interactionNotAllowed阻止Face ID等生物识别提示,但因访问控制标志的限制而无效。
基础测试配置
为测试流程,我基于Secure Enclave官方文档搭建了一个简单的iOS应用,核心函数如下:
func getAccessControl() -> SecAccessControl { var access: SecAccessControl? access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .biometryCurrentSet, .or, .applicationPassword], nil)! return access } func generatePrivateKey() throws -> SecKey { let context = LAContext() context.setCredential("pwd123".data(using: .utf8), type: .applicationPassword) let attributes: NSDictionary = [ kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeySizeInBits: 256, kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, kSecUseAuthenticationContext as String: context, kSecAttrLabel: "label-for-reference", kSecClass: kSecClassKey, kSecPrivateKeyAttrs: [ kSecAttrIsPermanent: true, kSecAttrApplicationTag: "tag-for-reference", kSecAttrAccessControl: getAccessControl(), ] ] var error: Unmanaged<CFError>? guard let privateKey = SecKeyCreateRandomKey(attributes, &error) else { throw error!.takeRetainedValue() as Error } return privateKey } func retrieveKey(context: LAContext? = nil) -> SecKey? { var attributes: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrLabel as String: "label-for-reference", kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnRef as String: true, ] if let context = context { attributes[kSecUseAuthenticationContext as String] = context } var item: CFTypeRef? let res = SecItemCopyMatching(attributes as CFDictionary, &item) if (res == errSecSuccess) { return (item as! SecKey) } else { return nil } } func sign(data: String, key: SecKey) throws -> Data? { if (SecKeyIsAlgorithmSupported(key, .sign, .ecdsaSignatureMessageX962SHA256)) { var error: Unmanaged<CFError>? guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data.data(using: .utf8)! as CFData, &error) as Data? else { throw error!.takeRetainedValue() as Error } return signature } return nil }
我通过以下方式测试签名流程(省略部分细节):
let privateKey = generatePrivateKey() let context = LAContext() context.setCredential("pwd123".data(using: .utf8), type: .applicationPassword) let retrievedKey = retrieveKey(context) var signedMessage: Data? try signedMessage = sign(data: "abc", key: retrievedKey!))
疑问与寻求解决方向
我想确认是否遗漏了Secure Enclave密钥链项访问控制的相关文档内容,或者是否有特定的方案可以实现我的需求。比如我曾考虑过链式解锁方案:Face ID → 解锁应用密码 → 解锁私钥,用户可以选择先通过Face ID获取应用密码,或者直接输入应用密码,但这种方案会将应用密码加载到内存中(如果通过自定义UI输入PIN码可能可以规避部分风险)。想了解主流银行类APP通常是如何实现这种“生物识别优先,回退PIN码”的认证流程的?
内容的提问来源于stack exchange,提问作者user15681986

