Expose touchIDAuthenticationAllowableReuseDuration setting for SecureEnclaveValet
Like apple doc says.
This bypasses a scenario where the user unlocks the device and then is almost immediately prompted for another fingerprint.
Some times that scenario is really annoying.
Seems like something that could be added to SinglePromptSecureEnclaveValet‘s initializer.
That said, I’m not convinced this API is necessary yet: I need to better understand the use case. Are you avoiding keeping the secret you need access to in memory? Would love to get a sense of the exact flow you’re solving for here – it can help us design an API that works
I'd like to set touchIDAuthenticationAllowableReuseDuration with accessControl: .userPresence
The scenario I want to avoid is like this:
- User unlocks their device by touch/face ID or pin code.
- User launch our app immediately.
At step 2, there is no point to ask user for touch/face ID again because they just unlocked the device a few seconds before.
Got it. So this is for the first keychain access your application makes. Seems like a reasonable addition to the API, though we’ll want the naming to reflect that this API works across all user presence tests (we’ll need to test if it works with passcode and Face ID).
Btw, I don’t see us adding this to SecureEnclaveValet, but I can see this API on SinglePromptSecureEnclaveValet. Does that work for you?
It can be added to SecureEnclaveValet too, Just add a LAContext to keychainQuery in:
private init(identifier: Identifier, accessControl: SecureEnclaveAccessControl)
Would be nice if both SecureEnclaveValet & SinglePromptSecureEnclaveValet can config this property.
My thinking there is that SecureEnclaveValet is built for always prompting, while SinglePromptSecureEnclaveValet is built for prompting less aggressively. Applying the reusable duration to both APIs would make the difference between the two less clear. Also worth remembering that SinglePromptSecureEnclaveValet has a method to require that the next keychain access prompts, so you can make it behave more like a SecureEnclaveValet when desired.
If you think SecureEnclaveValet also needs this API despite the above, I’d love to hear the use case that motivates that thinking.
OK, I see your point.
Also worth remembering that ‘SinglePromptSecureEnclaveValet‘ has a method to require that the next keychain access prompts, so you can make it behave more like a ‘SecureEnclaveValet‘ when desired.
Thanks for the tip!
@the-pear did you ever get touchIDAuthenticationAllowableReuseDuration to work on a Face ID device?
On my Face ID device, I've tried the following configurations:
-
SinglePromptSecureEnclaveValetwithuserPresenceaccessControl. When I try to access the value within the reuse direction, I get aerrSecAuthFailed. As soon as the reuse duration expires, queries work again (but prompt for Face ID). -
SinglePromptSecureEnclaveValetwithbiometricAnyaccessControl. When I try to access the value within the reuse duration, I still get a Face ID prompt. -
SinglePromptSecureEnclaveValetwithdevicePasscodeaccessControl. When I try to access the value within the reuse duration, I still get a passcode prompt.
It seems possible that this reuse identifier only works on Touch ID devices, and would need to be left off on Face ID devices. I won't have access to a Touch ID device for a couple weeks, so any help you could provide would be great.
Unfortunately I don't have any touch-id device at hand...
SinglePromptSecureEnclaveValet with userPresence accessControl. When I try to access the value within the reuse direction, I get a errSecAuthFailed. As soon as the reuse duration expires, queries work again (but prompt for Face ID).
I am not using Valet now, but working with plain keychain api. touchIDAuthenticationAllowableReuseDuration works fine on Face ID device.
This code works as expected on my iPhone X iOS 13.3.
var query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName]
let context = LAContext()
context.touchIDAuthenticationAllowableReuseDuration = gracePeriod
query[kSecUseAuthenticationContext as String] = context
query[kSecAttrAccessControl as String] = access
Thank you for the information! That helped me track down what was causing my local failures: in order to retrieve a value with touchIDAuthenticationAllowableReuseDuration, the value must have been set with a LAContext that had a non-default touchIDAuthenticationAllowableReuseDuration value.
In other words, the following write works:
var writeQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "ReuseDurationTest"]
let writeContext = LAContext()
writeContext.touchIDAuthenticationAllowableReuseDuration = 10.
writeQuery[kSecUseAuthenticationContext as String] = writeContext
writeQuery[kSecAttrAccessControl as String] = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, SecAccessControlCreateFlags.userPresence, nil)
writeQuery[kSecAttrAccount as String] = username
writeQuery[kSecValueData as String] = Data(stringToSet.utf8)
print("Delete: \(SecItemDelete(writeQuery as CFDictionary))")
print("Set: \(SecItemAdd(writeQuery as CFDictionary, nil))")
But the following write will fail when read during the reuse duration:
var writeQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "ReuseDurationTest"]
let writeContext = LAContext()
writeQuery[kSecUseAuthenticationContext as String] = writeContext
writeQuery[kSecAttrAccessControl as String] = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, SecAccessControlCreateFlags.userPresence, nil)
writeQuery[kSecAttrAccount as String] = username
writeQuery[kSecValueData as String] = Data(stringToSet.utf8)
print("Delete: \(SecItemDelete(writeQuery as CFDictionary))")
print("Set: \(SecItemAdd(writeQuery as CFDictionary, nil))")
Similarly, the below write will also fail to be read:
var writeQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "ReuseDurationTest"]
writeQuery[kSecAttrAccessControl as String] = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, SecAccessControlCreateFlags.userPresence, nil)
writeQuery[kSecAttrAccount as String] = username
writeQuery[kSecValueData as String] = Data(stringToSet.utf8)
print("Delete: \(SecItemDelete(writeQuery as CFDictionary))")
print("Set: \(SecItemAdd(writeQuery as CFDictionary, nil))")
Here's my read code for reference:
var readQuery: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "ReuseDurationTest"]
let readContext = LAContext()
readContext.touchIDAuthenticationAllowableReuseDuration = 10 // this value does not need to match the value set above
readQuery[kSecUseAuthenticationContext as String] = readContext
readQuery[kSecAttrAccessControl as String] = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, SecAccessControlCreateFlags.userPresence, nil)
readQuery[kSecAttrAccount as String] = username
readQuery[kSecMatchLimit as String] = kSecMatchLimitOne
readQuery[kSecReturnData as String] = true
var result: AnyObject? = nil
print("Query: \(SecItemCopyMatching(readQuery as CFDictionary, &result))")
print("Result: \(result)")
This discovery means that in order to ensure forwards compatibility, I'd likely need to create a new Valet type to support this functionality. Otherwise, values set prior to enabling this functionality wouldn't be readable during the reuse duration, which isn't great.