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"

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.

Are you accessing this keychain entry in a Notification Service Extension? If so, have you tested what happens if you turn the device off/on, then send a push to it without having unlocked the device?

We try to avoid executing code prior to first unlock, but NSEs (and voip pushes) are one of the exceptions to this, since the only alternative would be to drop or (possibly) defer the notification until unlock, both of which are quite problematic.

Also, highlighting one MAJOR advantage of the keychain:

What error comes back in that case?

Unlike file protection, the keychain errors let you differentiate between "the object doesn't exist" and "you can't access it right now".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Right. And to be specific, “you can't access it right now” comes back as errSecInteractionNotAllowed (-25308).

Share and Enjoy

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

Keychain Access kSecAttrAccessibleAfterFirstUnlock
 
 
Q