Swift 3中使用Keychain仅通过Touch ID访问保存数据

30
我正在处理一段代码,它应该完成以下操作:
  • 在Keychain中存储一些数据。
  • 只有在用户通过Touch ID或Pass Code进行身份验证后才能获取数据。
我观看了使用Touch ID的Keychain和身份验证演示,并理解了以下内容:

如果您在将新值添加到Keychain时设置正确的参数,则下次尝试取出它时,系统将自动显示Touch ID弹出窗口。

我编写了一些代码,但我的假设不起作用。这是我所写的:
    //
    //  Secret value to store
    //
    let valueData = "The Top Secret Message V1".data(using: .utf8)!;

    //
    //  Create the Access Controll object telling how the new value
    //  should be stored. Force Touch ID by the system on Read.
    //
    let sacObject =
        SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                            kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
                            .userPresence,
                            nil);

    //
    //  Create the Key Value array, that holds the query to store 
    //  our data
    //
    let insert_query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccessControl: sacObject!,
        kSecValueData: valueData,
        kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
        //  This two valuse ideifieis the entry, together they become the
        //  primary key in the Database
        kSecAttrService: "app_name",
        kSecAttrAccount: "first_name"
    ];

    //
    //  Execute the query to add our data to Keychain
    //
    let resultCode = SecItemAdd(insert_query as CFDictionary, nil);

起初我以为模拟器有问题,但是实际上我可以通过以下代码检查Touch ID是否存在:

    //
    //  Check if the device the code is running on is capapble of 
    //  finger printing.
    //
    let dose_it_can = LAContext()
        .canEvaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics, error: nil);

    if(dose_it_can)
    {
        print("Yes it can");
    }
    else
    {
        print("No it can't");
    }

我还能通过以下代码编程方式显示Touch ID弹出窗口:

    //
    //  Show the Touch ID dialog to check if we can get a print from 
    //  the user
    //
    LAContext().evaluatePolicy(
        LAPolicy.deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Such important reason ;)",
        reply: {
            (status: Bool, evaluationError: Error?) -> Void in

            if(status)
            {
                print("OK");
            }
            else
            {
                print("Not OK");
            }

    });

总结

Touch ID可以工作,但是使用系统本身强制Touch ID标志将一个值保存到Keychain中无法工作——我错过了什么?

苹果的示例

苹果提供的示例称为KeychainTouchID:使用Touch ID与Keychain和LocalAuthentication也显示不一致的结果,Touch ID没有被系统强制执行。

技术规格

  • Xcode 8.1
  • Swift 3

你可以使用一些封装库来使它更容易。我使用 KeychainAccess - Bryan Chen
你的 SecItemCopyMatching 在哪里?你有存储数据到钥匙串的代码,但没有读取它的代码吗? - Bryan Chen
我对包装不感兴趣,我想使用苹果提供给我的东西 ;) - David Gatti
@DavidGatti:你有机会检查答案吗?在我的测试中它有效,但是知道它是否也解决了你的问题会很好。 - Martin R
我肯定会测试代码的,但现在工作有点忙:( 希望周末能够测试。 - David Gatti
显示剩余6条评论
1个回答

25

只有在后台队列上调用 SecItemCopyMatching(),才会出现 Touch ID 弹窗。这在 Keychain and Authentication with Touch ID 的 PDF 演示文稿第 118 页中有说明:

读取密码
...

dispatch_async(dispatch_get_global_queue(...), ^(void){
    CFTypeRef dataTypeRef = NULL;
    OSStatus status = SecItemCopyMatching((CFDictionaryRef)query,
                                     &dataTypeRef);
});
否则你将阻塞主线程,弹出窗口无法出现。SecItemCopyMatching() 然后会因为超时而失败,并显示错误码 -25293 = errSecAuthFailed
在你的示例项目中,该故障并不立即显现,因为它在错误情况下打印了错误的变量,例如:
if(status != noErr)
{
    print("SELECT Error: \(resultCode)."); // <-- Should be `status`
}

对于更新和删除操作同样如此。

这是您示例代码的一个包含必要调度到后台队列以检索钥匙串项的简化版本。 (当然,必须将UI更新调度回主队列。)

在我的iPhone上使用Touch ID进行测试时,它按预期工作: Touch ID弹出窗口出现,并且只有在成功身份验证后才会检索钥匙串项。

Touch ID身份验证无法在iOS模拟器上使用。

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    //  This two values identify the entry, together they become the
    //  primary key in the database
    let myAttrService = "app_name"
    let myAttrAccount = "first_name"

    // DELETE keychain item (if present from previous run)

    let delete_query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrService: myAttrService,
        kSecAttrAccount: myAttrAccount,
        kSecReturnData: false
    ]
    let delete_status = SecItemDelete(delete_query)
    if delete_status == errSecSuccess {
        print("Deleted successfully.")
    } else if delete_status == errSecItemNotFound {
        print("Nothing to delete.")
    } else {
        print("DELETE Error: \(delete_status).")
    }

    // INSERT keychain item

    let valueData = "The Top Secret Message V1".data(using: .utf8)!
    let sacObject =
        SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                        kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                        .userPresence,
                                        nil)!

    let insert_query: NSDictionary = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccessControl: sacObject,
        kSecValueData: valueData,
        kSecUseAuthenticationUI: kSecUseAuthenticationUIAllow,
        kSecAttrService: myAttrService,
        kSecAttrAccount: myAttrAccount
    ]
    let insert_status = SecItemAdd(insert_query as CFDictionary, nil)
    if insert_status == errSecSuccess {
        print("Inserted successfully.")
    } else {
        print("INSERT Error: \(insert_status).")
    }

    DispatchQueue.global().async {
        // RETRIEVE keychain item

        let select_query: NSDictionary = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrService: myAttrService,
            kSecAttrAccount: myAttrAccount,
            kSecReturnData: true,
            kSecUseOperationPrompt: "Authenticate to access secret message"
        ]
        var extractedData: CFTypeRef?
        let select_status = SecItemCopyMatching(select_query, &extractedData)
        if select_status == errSecSuccess {
            if let retrievedData = extractedData as? Data,
                let secretMessage = String(data: retrievedData, encoding: .utf8) {

                print("Secret message: \(secretMessage)")

                // UI updates must be dispatched back to the main thread.

                DispatchQueue.main.async {
                    self.messageLabel.text = secretMessage
                }

            } else {
                print("Invalid data")
            }
        } else if select_status == errSecUserCanceled {
            print("User canceled the operation.")
        } else {
            print("SELECT Error: \(select_status).")
        }
    }
}

1
你的代码肯定是有效的。感谢你在PDF方面的发现,我错过了那个关键的信息。 - David Gatti
当您执行SecItemAdd时,它是否应提示您进行TouchID身份验证?或者您可以在没有身份验证的情况下放置,但只有在成功身份验证后才能读取? - AlexZd
1
@AlexZd:当读取密钥(SecItemCopyMatching)时,会出现身份验证提示。 - Martin R
哎呀,直接在主线程上调用它(因为它与UI相关)感觉很自然,但将其移至后台解决了那个神秘的“-25293 = errSecAuthFailed”错误,谢谢!顺便说一句,这是一个奇怪且不太描述清楚的错误。 - smat88dd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接