Keychain Access kSecAttrAccessibleAfterFirstUnlock

Hey guys, first time posting here.

I've been working on a common library that is shared across multiple apps. The purpose of it is to store data in the keychain (with the kSecAttrAccessibleAfterFirstUnlock flag) and that data should be available to be read by any app in the group.

Everything works in normal use cases, like, user switches from an app to another, the data is there and read. But there are some edge cases reported by some devices that the data is not there when being read (via push notification) when the device is locked. Note that this not something that happens 100% of the time.

Here's my write/read/delete code (it's quite objective-c code) :


//
// Internal methods
//
- (NSDictionary *)read:(NSString *)key forGroup:(NSString *)groupId {
    NSMutableDictionary *query = [self queryData:groupId];
    
    query[(__bridge id)kSecAttrAccount] = key;
    query[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
    
    CFDataRef resultData = NULL;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef*)&resultData);
    
    NSDictionary *value;
    if (status == noErr) {
        value = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData*)resultData];
    }
    
    return value;
}

- (BOOL)write:(NSDictionary *)value
       forKey:(NSString *)key
     forGroup:(NSString *)groupId {
    NSMutableDictionary *query = [self queryData:groupId];
    
    query[(__bridge id)kSecAttrAccount] = key;
    query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
    
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject: value];
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
    if (status == noErr){
        query[(__bridge id)kSecMatchLimit] = nil;
        
        NSDictionary *update = @{
            (__bridge id)kSecValueData: data,
            (__bridge id)kSecAttrAccessible:(__bridge id) kSecAttrAccessibleAfterFirstUnlock
        };
        
        status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
        if (status != noErr){
            return false;
        }
    } else {
        query[(__bridge id)kSecValueData] = data;
        query[(__bridge id)kSecMatchLimit] = nil;
        query[(__bridge id)kSecAttrAccessible] = (__bridge id) kSecAttrAccessibleAfterFirstUnlock;
        
        status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
        if (status != noErr){
            return false;
        }
    }
    return true;
}

- (BOOL)delete:(NSString *)key forGroup:(NSString *)groupId {
    NSMutableDictionary *query = [self queryData:groupId];
    
    query[(__bridge id)kSecAttrAccount] = key;
    query[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
    
    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
    return (status == noErr);
}

- (BOOL)deleteAll:(NSString *)groupId {
    NSMutableDictionary *query = [self queryData:groupId];
    
    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
    return (status == noErr);
}

- (NSArray *)readAll:(NSString *)groupId {
    NSMutableDictionary *query = [self queryData:groupId];
    
    query[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue;
    query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
    query[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue;
    
    CFArrayRef resultData = NULL;
    
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef*)&resultData);
    if (status == noErr) {
        NSArray *keychainItems = (__bridge NSArray*)resultData;
        
        NSMutableArray *items = [NSMutableArray new];
        for (NSDictionary *item in keychainItems){
            //            NSString *key = item[(__bridge NSString *)kSecAttrAccount];
            NSDictionary *value = [NSKeyedUnarchiver unarchiveObjectWithData: item[(__bridge NSString *) kSecValueData]];
            [items addObject: value];
        }
        return [items copy];
    }
    
    return @[];
}

- (NSMutableDictionary *)queryData:(NSString *)groupId {
    NSMutableDictionary *query = [NSMutableDictionary new];

    query[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
    query[(__bridge id)kSecAttrService] = KEYCHAIN_SERVICE;

#if !TARGET_OS_SIMULATOR
    if (groupId) {
        query[(__bridge id)kSecAttrAccessGroup] = groupId;

        // Check if data exists for 'app groups'
        // - no data found -> we go for 'keychain groups'
        // - data found -> continue using 'app groups'
        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
        if (status != errSecSuccess) {
            query[(__bridge id)kSecAttrAccessGroup] = 
                [NSString stringWithFormat:@"XXXXXXXX.%@", groupId];
        }
    }
#endif

    return query;
}
code-block

Is there something I'm doing wrong?

But there are some edge cases reported by some devices that the data is not there when being read

What error comes back in that case?

ps I have a whole post explaining my process for investigating problems like this — Investigating hard-to-reproduce keychain problems — but I don’t want to point you in that direction until I have more details.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Keychain Access kSecAttrAccessibleAfterFirstUnlock
 
 
Q