Touch ID credential surviving device reset on iOS 8.3

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

From the relese notes, it is a known issue:


Keychain items created with

kSecAccessControlUserPresence
access control list are using global Touch ID credential for 10 minutes. When an iPhone is unlocked by Touch ID, these items reuse the Touch ID unlock information and do not ask for new authentication. When the iPhone is unlocked using a passcode, the items require Touch ID.

Hi Florin,

That's an iOS9 bug in the first beta. But I did think it might apply to 8.3.

Touch ID credential surviving device reset on iOS 8.3
 
 
Q