This issue has been bugging me for a while, and I hope someone has insight as to the cause of this. Essentially, I have a small percentage of users who are unable to save/update items to the keychain. The problematic flow of control is as follows:
We check for the existence of the item using SecItemCopyMatching
. This returns the error code errSecItemNotFound
We then try to add the item via SecItemAdd
, but this then returns errSecDuplicateItem
.
Because of this, we have some users who are unable to update a subset of keychain items at all, requiring them to restore their device to clear the keychain. This is obviously an unacceptable workaround. It seemed to work for them before, but have now got into this non-updatable cycle.
After researching, I've seen issues regarding the search query used in SecItemCopyMatching
not being specific enough, but my code uses a common search query wherever possible.
+ (NSMutableDictionary*)queryForUser:(NSString*)user key:(NSString*)key
{
if (!key || !user) { return nil; }
NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier];
NSString* prefixedKey = [NSString stringWithFormat:@"%@.%@", bundleId, key];
NSMutableDictionary* query = [NSMutableDictionary dictionary];
[query addEntriesFromDictionary:@{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword}];
[query addEntriesFromDictionary:@{(__bridge id)kSecAttrAccount : user}];
[query addEntriesFromDictionary:@{(__bridge id)kSecAttrService : prefixedKey}];
[query addEntriesFromDictionary:@{(__bridge id)kSecAttrLabel : prefixedKey}];
[query addEntriesFromDictionary:@{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly}];
return query;
}
The code to do the updating/adding is as follows (sorry for the verbosity):
// Setup the search query, to return the *attributes* of the found item (for use in SecItemUpdate)
NSMutableDictionary* query = [self queryForUser:username key:key];
[query addEntriesFromDictionary:@{(__bridge id)kSecReturnAttributes : (__bridge id)kCFBooleanTrue}];
// Prep the dictionary we'll use to update/add the new value
NSDictionary* updateValues = @{(__bridge id) kSecValueData : [value dataUsingEncoding:NSUTF8StringEncoding]};
// Copy what we (may) already have
CFDictionaryRef resultData = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef*)&resultData);
// If it already exists, update it
if (status == noErr) {
// Create a new query with the found attributes
NSMutableDictionary* updateQuery = [NSMutableDictionary dictionaryWithDictionary:(__bridge NSDictionary*)resultData];
[updateQuery addEntriesFromDictionary:@{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword}];
// Update the item in the keychain
status = SecItemUpdate((__bridge CFDictionaryRef)updateQuery, (__bridge CFDictionaryRef)updateValues);
if (status != noErr) {
// Update failed, I've not seen this case occur as of yet
}
}
else {
// Add the value we want as part of our original search query, and add it to the keychain
[query addEntriesFromDictionary:updateValues];
[query removeObjectForKey:(__bridge id)kSecReturnAttributes];
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
if (status != noErr) {
// Addition failed, this is where I'm seeing errSecDuplicateItem
}
}
We tried using SecItemDelete
instead of checking/updating, but this also returned errSecItemNotFound
with SecItemAdd
failing straight after. The delete code is:
+ (BOOL)deleteItemForUser:(NSString *)username withKey:(NSString *)itemKey {
if (!username || !itemKey) { return NO; }
NSString * bundleId = [[NSBundle mainBundle] bundleIdentifier];
NSString * prefixedItemKey = [NSString stringWithFormat:@"%@.%@", bundleId, itemKey];
NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys: (__bridge id)kSecClassGenericPassword, kSecClass,
username, kSecAttrAccount,
prefixedItemKey, kSecAttrService, nil];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query);
if (status != noErr) {
// Failed deletion, returning errSecItemNotFound
}
return (status == noErr);
}
Whilst we have defined 2 keychain access groups for the application, affected keychain items do not have an access group assigned as an attribute (which by the documentation, means searching will be done for all access groups). I've yet to see any other error code other than errSecItemNotFound
and errSecDuplicateItem
.
The fact that only a small set of users get into this condition really confuses me. Are there any other considerations I need to take into account regarding the keychain that could be causing this, regarding multithreading, flushing, background access etc…?
Help much appreciated. I'd rather stick with using the Keychain Services API instead of using a 3rd party library. I'd like to understand the fundamental problem here.
The unique key for kSecClassGenericPassword
is composed of;
kSecAttrAccount
kSecAttrService
To check for its existence, query the keychain store with only these attributes (including kSecReturnAttributes
flag).
Including kSecAttrLabel
and kSecAttrAccessible
will exclude any existing item with the same unique key, but with different attributes.
Once you have confirmed its (non)existence, add the additional attributes and Add or Update.