Save data in Keychain only accessible with Touch ID in Swift 3
Asked Answered
C

1

30

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 Pass Code.

I watched the Keychain and Authentication with Touch ID presentation and understood the following:

If you set the right parameter while adding a new value in to Keychain, next time you'll try to get it out, the system will automatically show the Touch ID popup.

I wrote some code, and my assumption doesn't work. This is what I have written:

    //
    //  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);

At first I thought that the emulator had some issue but no, I was able to check if Touch ID is present or not with the following code:

    //
    //  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");
    }

And I was also able to programmatically show the Touch ID popup with the following code:

    //
    //  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");
            }

    });

To sum it all up

Touch ID works, but saving a value in to Keychain with the flag to force Touch ID by the system itself is not working - what am I missing?

Apples example

The example that Apple provides called KeychainTouchID: Using Touch ID with Keychain and LocalAuthentication also shows inconsistent result and Touch ID is not enforced by the system.

Tech spec

  • Xcode 8.1
  • Swift 3
Combs answered 29/11, 2016 at 18:57 Comment(11)
You can use some wrapper library to make it easier. I use KeychainAccessNoranorah
Where is your SecItemCopyMatching? You have code to store data into keychain but no code to read from it?Noranorah
I'm not interested in a a wrapper, I want to use what dad Apple gives me ;)Combs
Bryan, check the link to GItHub you have, Insert, Select, Updated and Delete.Combs
What do you mean? You can obtaining an item with KeychainAccess. And can check how the library does it: github.com/kishikawakatsumi/KeychainAccess/blob/master/Lib/…Noranorah
@DavidGatti If you read Objective-C, check out the Apple sample project: It should be up to date because it was just updated in September: developer.apple.com/library/content/samplecode/KeychainTouchID/…Alluvium
Matthew, out of curiosity, could you check yourself? No irony hear. I tried it, and man, the result are strange and make no sense. And that is also the issue, I based my cone on what is on that example, and I get the same result in both cases Touch ID doesn't show up. Or I'm hallucinating and are the unluckiest guy on earth, or something I wrong.Combs
@DavidGatti I've looked at the sample code but I can't verify whether it is completely up-to-date. While I have plans of locking Keychain items with Touch ID in the future, my apps do not currently do this. I've using LocalAuthentication to work with Touch ID, and I've stored items in the Keychain, but as of yet I have not associated a keychain item with Touch ID access control. So I too am interested to here an answer to your question.Alluvium
I also just programmatically use Touch ID, to make sure only the user of the phone can access the data, but it would be nice if Keychain Could enforce that part on its own. But it is scary that, you think you can relay on the system to present the Touch ID to the user, and then in reality the system doesn't do that :(Combs
@DavidGatti: Did you have a chance to check the answer? It worked in my test, but it would be nice to know if it solved your problem as well.Earthiness
I will for sure, bit busy right now at work :( I hope the test the code during the weekend.Combs
E
25

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).")
        }
    }
}
Earthiness answered 14/1, 2017 at 19:1 Comment(4)
Your code definitely works. Thank you for that catch with the PDF I complexity missed that crucial peace of information.Combs
At the time when you're doing SecItemAdd should it prompt you with TouchID authentication? Or you can put without authentication, but you can only read after success authentication?Diminution
@AlexZd: The prompt for authentication appears when reading the key (SecItemCopyMatching).Earthiness
Ouch, feels natural to just call it on the main thread (since its UI related), but moving it to the background fixed that mysterious -25293 = errSecAuthFailed, thanks! Strange error and not very descriptive, btw.Footpace

© 2022 - 2024 — McMap. All rights reserved.