I'm working on a peace of code that should do the following:
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");
}
});
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?
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.
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).")
}
}
}