SecItemCopyMatching Touch ID Dialog

Hi


I have created a private key in the Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. When I attempt to access the key to perform a signing operation, the Touch ID dialog will sometimes appear, but in most cases I have to touch the biometry sensor, then the dialog is displayed. Here's the code I'm using to create the key and access the key in a sign operation.


public static func create(with name: String, authenticationRequired: SecAccessControlCreateFlags? = nil) -> Bool
{
    guard !name.isEmpty else
    {
        return false
    }
    
    var error: Unmanaged<CFError>?
    
    // Private key parameters
    var privateKeyParams: [String: Any] = [
        kSecAttrIsPermanent as String: true,
        kSecAttrApplicationTag as String: name
    ]
    
    // If we are using a biometric sensor to access the key, we need to create an SecAccessControl instance.
    if authenticationRequired != nil
    {
        guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, authenticationRequired!, &error) else
        {
            return false
        }
        
        privateKeyParams[kSecAttrAccessControl as String] = access
    }
    
    // Global parameters for our key generation
    let parameters: [String: Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
        kSecAttrKeySizeInBits as String: 2048,
        kSecPrivateKeyAttrs as String: privateKeyParams
    ]
    
    // Generate the keys.
    guard let privateKey = SecKeyCreateRandomKey(parameters as CFDictionary, &error) else
    {
        return false
    }
    
    // Private key created!
    return true
}

This is the code to sign the data that should prompt for the biometry sensor (Touch ID or Face ID).


public static func sign(using name: String, value: String, localizedReason: String? = nil, base64EncodingOptions: Data.Base64EncodingOptions = []) -> String?
{
    guard !name.isEmpty else
    {
        return nil
    }
    
    guard !value.isEmpty else
    {
        return nil
    }
    
    // Check if the private key exists in the chain, otherwise return
    guard let privateKey: SecKey = getPrivateKey(name, localizedReason: localizedReason ?? "") else
    {
        return nil
    }
    
    let data = value.data(using: .utf8)!
    var error: Unmanaged<CFError>?
    guard let signedData = SecKeyCreateSignature(privateKey,
                                                 rsaSignatureMessagePKCS1v15SHA512,
                                                 data as CFData,
                                                 &error) as Data? else
    {
        return nil
    }
    
    return signedData.base64EncodedString(options: base64EncodingOptions)
}

fileprivate static func getPrivateKey(_ name: String, localizedReason: String) -> SecKey?
{
    let query: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
        kSecAttrApplicationTag as String: name,
        kSecReturnRef as String: true,
        kSecUseOperationPrompt as String : localizedReason
    ]
    
    var item: CFTypeRef? = nil
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    
    guard status == errSecSuccess else
    {
        if status == errSecUserCanceled
        {
            print("\tError: Accessing private key failed: The user cancelled (%@).", "\(status)")
        }
        else if status == errSecDuplicateItem
        {
            print("\tError: The specified item already exists in the keychain (%@).", "\(status)")
        }
        else if status == errSecItemNotFound
        {
            print("\tError: The specified item could not be found in the keychain (%@).", "\(status)")
        }
        else if status == errSecInvalidItemRef
        {
            print("\tError: The specified item is no longer valid. It may have been deleted from the keychain (%@).", "\(status)")
        }
        else
        {
            print("\tError: Accessing private key failed (%@).", "\(status)")
        }
        return nil
    }
    
    return (item as! SecKey)
}

Then in my app, I would simply call

guard let result = sign("mykey", "helloworld") else
{
   print("failed to sign")
   return
}

print(result)

So the getPrivateKey function is the one that calls SecKeyCopyingMatching, the 3 methods are in a helper class; what's the best approach to reliabably display the biometry dialog?


Thanks

Replies

You posted a lot of code (thanks!) but you missed one key point. What value do you pass to the

authenticationRequired
parameter of
create(with:authenticationRequired:)
?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"


WWDC runs Mon, 4 Jun through to Fri, 8 Jun. During that time all of DTS will be at the conference, helping folks out face-to-face.

Typically this would be how I would call the function:


create(with "myKey", authenticationRequired: SecAccessControlCreateFlags.biometryAny)


Thanks for having a look at this

So, to confirm: You’re running this app on a Touch ID device (that is, not Face ID) and the specific problem is that, most of the time, the Touch ID UI does not show up until you actually touch the Home button. Right?

If so, that’s quite weird. I have a couple of suggestions for you here:

  • Try pulling this code out into a separate test app to see if it also has the problem. It’s possible that there’s something else in your main app that’s triggering the issue.

  • Grab a copy of the KeychainTouchID sample code to see if it exhibits the same behaviour.

    Note The sample code does not do the exact operation you’re doing, but you can start with something like that (the Add Item (TouchID Only) test), see if that has the same issue, and then tweak things from there.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"


WWDC runs Mon, 4 Jun through to Fri, 8 Jun. During that time all of DTS will be at the conference, helping folks out face-to-face.

I too am having this issue. Have you managed to get a solution?

And I am having the same issue as well. I call SecItemCopyMatching, and once in a while I get no dialog, and my UI is frozen. If I wait long enough, I do get to see a dialog. Sometimes after 3 or 4 seconds, but sometimes after 20-30 seconds. But always, if I touch the sensor, suddenly the popup pops up and login is successful.
I have a feeling that it's simply a UI issue that the popup is not being shown for a while. But I can't verify it, because it all happens inside SecItemCopyMatching.


I am actually using the SimpleKeychain pod. But that's not much more than a thin wrapper around the Sec calls.


It also looks that if I have switched of connectivity, I can reproduce the issue easier. But still not 100%. Maybe 2 out of every 10 tries.


My app is not doing anything concurrently when SecItemCopyMatching hangs. And SecItemCopyMatching is called on the main thread (in fact, I dispatch_async(dispatch_get_main_queue(),... the function that does the SecItemCopyMatching, and only continue when it returns.
Any clues?

Hi


I spuratically get the delay in the Touch ID dialog appearing or touching the senor and the dialog will appear; maybe 1-2 times out of 15. Not doing anything special and the original code I posted I've been using for almost a year.


Sorry I couldn't be of more help

On an iPhone 7 and 11 it's always working OK and on an iPhone 8 I see this behaviour. All devices are running iOS 13.1. Playing around with execution on the main or background thread and adding a delay of 1 second myself does not influence the strange behaviour. On the main thread the entire interface is blocked but the fingerprint sensor works. On the background thread the fingerprint question shows up the moment I touch the screen.

All devices are running iOS 13.1.

There is a known bug on iOS 13 that results in the Touch ID UI not being visible (r. [49700735][r49700735]). Please retest with the current iOS 13.2 beta seed (17B5059g).

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I'm seeing this again in 14.6. Is it possible that a regression occurred?

Is it possible that a regression occurred?

Well, when it comes to computers pretty much anything is possible. However, I’ve not see other reports of a regression in this space.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"