Save data in Keychain only accessible with Touch ID in Swift 3

前端 未结 1 909
礼貌的吻别
礼貌的吻别 2020-12-24 06:18

I\'m working on a peace of code that should do the following:

  • Store some data in Keychain.
  • Get the data only if a user authenticates with Touch ID or
相关标签:
1条回答
  • 2020-12-24 07:08

    The Touch ID popup appears only if you call SecItemCopyMatching() on a background queue. This is indicated on page 118 of the PDF presentation of Keychain and Authentication with Touch ID:

    Reading a Secret
    ...

    dispatch_async(dispatch_get_global_queue(...), ^(void){
        CFTypeRef dataTypeRef = NULL;
        OSStatus status = SecItemCopyMatching((CFDictionaryRef)query,
                                         &dataTypeRef);
    });
    

    Otherwise you are blocking the main thread and the popup does not appear. SecItemCopyMatching() then fails (after a timeout) with error code -25293 = errSecAuthFailed.

    The failure is not immediately apparent in your sample project because it prints the wrong variable in the error case, for example

    if(status != noErr)
    {
        print("SELECT Error: \(resultCode)."); // <-- Should be `status`
    }
    

    and similarly for the update and delete.

    Here is a comprised version of your sample code with the necessary dispatch to a background queue for retrieving the keychain item. (Of course UI updates must be dispatched back to the main queue.)

    It worked as expected in my test on an iPhone with Touch ID: the Touch ID popup appears, and the keychain item is only retrieved after a successful authentication.

    Touch ID authentication does not work on the iOS Simulator.

    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).")
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题