SecItemAdd() Succeeds with kSecAttrAccessibleWhenUnlocked But Fails with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
Asked Answered
W

2

3

The Story So Far

Four months ago, I posted this question because the upgrade to iOS 13 was breaking my keychain-related code.

My code stores the user's password in the keychain using class kSecClassGenericPassword and access attribute kSecAttrAccessibleWhenUnlocked. As explained in my own answer to that question, I finally got my code to work also on iOS 13 by cleaning up the query dictionaries a bit.

The Current Problem

A few weeks ago, I was asked to disable backing up of the password data to enhance security, so I changed the access attribute to kSecAttrAccessibleWhenUnlockedThisDeviceOnly (unlike kSecAttrAccessibleWhenUnlocked, the password in the keychain is not transferred to another device during backups).

Now, my code fails and the user has to enter their password every time. (tested on iOS 13.0, iPhone 8 Plus)

When the user logs in using their password, my code first deletes any previously stored password using SecItemDelete(), and then proceeds to store the entered password using SecItemMatch().

Since changing the access attribute to kSecAttrAccessibleWhenUnlockedThisDeviceOnly, SecItemDelete() "succeeds" with errSecItemNotFound (i.e., "Nothing to delete"), but SecItemAdd() fails with errSecDuplicateItem!

Note, this isn't an issue of trying to retrieve a password previously stored with kSecAttrAccessibleWhenUnlocked using kSecAttrAccessibleWhenUnlockedThisDeviceOnly (i.e., different query dictionaries for store and load); I deleted the app from the device and tried from the start with the new code, and SecItemAdd() always fails.

What's Going On?

Witchery answered 15/11, 2019 at 1:29 Comment(0)
T
4

I assume you've already figured that out, but for the sake of completeness:

Note, this isn't an issue of trying to retrieve a password previously stored with kSecAttrAccessibleWhenUnlocked using kSecAttrAccessibleWhenUnlockedThisDeviceOnly (i.e., different query dictionaries for store and load); I deleted the app from the device and tried from the start with the new code, and SecItemAdd() always fails.

This is exactly what is happening to you. Keychain contents survive app deletion and re-install. To fix the issue you need to come up with a migration strategy that will remove items saved with the same primary keys, but different access control setting before trying to save new ones

Trifacial answered 19/10, 2020 at 10:11 Comment(1)
Thank you, that is exactly what I'm doing now. I was under the impression that keychain data surviving installs (which is not documented, because it's apparently an implementation detail, not part of the spec) had been 'fixed' in a failry recent release of iOS (can't remember which... 11?). So yes, now I am retrieving attempting both values, and storing the one I really want (migration).Witchery
P
2

If you're using the deprecated accessible keys like: 'kSecAttrAccessibleAlwaysThisDeviceOnly' or 'kSecAttrAccessibleAlways', then the best way to deal with the items saved in your keychain is to have a migration strategy to covert those keychain items saved with these deprecated key to convert/save/copy them with the new key like 'kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly'.

There's no better way since the keychain items always survives the app deletion and reinstalls.

Here's the code that works: Note: It should be triggered for all the keys(For each key/value pair)

+ (void)updateNewchainDataForKey:(NSString*)key {
    
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];

NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];

//Our OLD keychain items were saved with 'kSecAttrAccessibleAlwaysThisDeviceOnly'(DEPRECATED) accessible key
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];

[dict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];


CFDictionaryRef resultDataRef = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict, (CFTypeRef *)&resultDataRef);
NSDictionary *resultDict = (__bridge_transfer NSDictionary *)resultDataRef;

NSLog(@"resultDict %@", resultDict);

if( status != errSecSuccess) {
    NSLog(@"Unable to fetch item for key %@ with error:%d",key,(int)status);
    return;
}

if (status == errSecSuccess && resultDict) {
    
    // Check if we have the old attribute type(s)
    if ([[[resultDict objectForKey:(__bridge id)kSecAttrAccessible] copy] isEqualToString:(__bridge NSString *)(kSecAttrAccessibleAlways)]
        || [[[resultDict objectForKey:(__bridge id)kSecAttrAccessible] copy] isEqualToString:(__bridge NSString *)(kSecAttrAccessibleAlwaysThisDeviceOnly)]) {
        
        // Update the deviceID attribute to kSecAttrAccessibleAlwaysThisDeviceOnly
        NSMutableDictionary *updateQuery = [NSMutableDictionary dictionary];
        
        //Our keychain items are now being saved with 'kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly' accessible key
        // Set the new attribute
        [updateQuery setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
        
        NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
        [dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        
        NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
        [dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
        [dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
        
        // Perform the update
        OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)dict, (__bridge CFDictionaryRef)updateQuery);
        if (status != errSecSuccess) {
            NSLog(@"status failed %d", (int)status);
        } else {
            NSLog(@"status PASS %d", (int)status);
        }
        
    }
    
  }
}

Note: Once you execute the above code, the key/value pair saved with those old/deprecated accessible key would be deleted and only the key/value pair updated with the new accessible key would exist in the keychain

Hope, it helps save someone's day!

Pohai answered 2/5, 2021 at 6:59 Comment(1)
Thank you; I wasn’t aware of the deprecation!Witchery

© 2022 - 2024 — McMap. All rights reserved.