My app supports Touch ID to allow the user to log in using a saved credential (password).
When the user launches the app, they are immediately prompted to touch the home button to log in.
A successful touch retrieves the credential and uses it to log in.
We have been seeing sporadic problems under iOS 8.3.
Upon launch the system will *not* prompt the user to touch, it will instead directly retrieve the correct credential and log in.
I set the credential using the following code (some details shortened for readability):
-(void)enrollTouchForUser:(NSString *)username
{
NSString *password = @"passwordFromInterface";
NSData *encodedPassword = [NSData encodePassword:password forUsername:username];
NSData *encryptionKeyData; // obtained from outside process - code not needed for this sample
NSData *encryptedPassword = [NSData encryptDataWithKey:encryptionKeyData withData:encodedPassword];
// add the key to the keychain
CFErrorRef error = NULL;
SecAccessControlRef sacObject;
// Should the secret be invalidated when passcode is removed? YES.
// So use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
// kSecAccessControlUserPresence REQUIRES a touch to enable reading the stored key
sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecAccessControlUserPresence, &error);
if(sacObject == NULL || error != NULL)
{
NSLog(@"can't create sacObject: %@", error);
// show a descriptive error about how to proceed
return;
}
// we want the operation to fail if there is an item which needs authentication so we will use
// kSecUseNoAuthenticationUI
NSDictionary *attributes = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService:@"AServiceName",
(__bridge id)kSecAttrAccount:username,
(__bridge id)kSecValueData:encryptedPassword,
(__bridge id)kSecUseNoAuthenticationUI: @YES,
(__bridge id)kSecAttrAccessControl: (__bridge id)sacObject
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void)
{
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
switch (status)
{
case errSecSuccess:
{
// successfully enrolled
dispatch_async(dispatch_get_main_queue(), ^{
// do all the success things
});
}
break;
// - 25299
case errSecDuplicateItem:
{
// an item already exists for this username so update it
NSDictionary *query = @{
(__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService:@"AServiceName",
(__bridge id)kSecAttrAccount:username,
(__bridge id)kSecUseOperationPrompt: @"Updating previously saved credentials"
};
NSDictionary *changes = @{
(__bridge id)kSecValueData:encryptedPassword
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void)
{
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)changes);
switch (status)
{
case errSecSuccess:
{
// successfully enrolled
dispatch_async(dispatch_get_main_queue(), ^{
// do the success things
});
}
break;
default:
{
dispatch_async(dispatch_get_main_queue(), ^{
// do the failure things
});
}
break;
}
});
}
break;
default:
{
dispatch_async(dispatch_get_main_queue(), ^{
// do the failure things
});
}
break;
}
});
}
I read the credential using the following code:
-(void)performTouchLoginForUsername:(NSString *)username
{
// user has tapped the login button
NSString *prompt = @"Touch to login";
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"AServiceName",
(__bridge id)kSecAttrAccount:username,
(__bridge id)kSecReturnData: @YES,
(__bridge id)kSecUseOperationPrompt:prompt,
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
switch (status)
{
case noErr:
{
// we got a payload - no error
dispatch_async(dispatch_get_main_queue(), ^{
// do the success stuff
});
}
break;
default:
{
// do the error stuff
}
break;
}
});
}
In trying to troubleshoot the problem I performed an "Erase All Content and Settings". This should remove all keys marked as kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
After setting up Touch ID and installing the app, the same problem occurred. No prompt to touch, and it logged in successfully. This means the previously saved credential was still available.
A complete operating system re-install has returned the device to operating as expected.
This has shown two problems:
- The API call to prompt for a touch is failing
and
- kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly flagged credential was still available after Erase All Content and Settings was performed
Has anyone seen this problem? Is my code incorrect?
I have one other device (an iPad) that is exhibiting this problem. All other devices I have do not show the problem.
Any help/pointers will be appreciated. This is a hard one to submit to DTS as it seems to be related to the state of specific devices.
Phil