You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS中实现Secure Enclave生物识别+应用密码Fallback的签名认证方案

iOS(Swift)实现银行类APP本地认证访问Secure Enclave密钥方案探讨

需求目标

我需要在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 22:25:18