Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ios storing login token in KeyChain fails to retrieve, rarely and randomly, but consistently

I'm using the ios keychain (keychainItemWrapper / SSKeychain) to store my app's login token and maintain logged in state. Currently I store a simple NSDictionary in the keychain containing my token, a token expiry and a refresh token. I serialize it to NSData and storing using kSecValueData. I also set the kSecAttrAccount and kSecAttrService, but don't use those for auth.

This works great, about 95% of the time. The problem is that that randomly, unpredictably and sporadically, the keychain does not return data when I request it to retrieve the token. It is usually after a moderate time away from the app, when reopening it. It doesn't have to be from in background, or after any specific delay though.

It fails specifically when asking for my NSData below and returns <> instead of <ABCD EFGH IJKL ....>. I think it is nil. Thus the code thinks the user isn't logged in and drops them immediately on my App's Signup/Login landing page, without logout error, token expiry error, etc. If I minimize the app, then reopen, it almost always gets the correct keychain info and the user is logged in again.

This creates a confusing experience when encountered. It also means the user can't maintain this true 100% logged in state, with occasionally being randomly logged out. I've been unable to predict it or debug it and changing keychain libraries, as shown below, hasn't fixed it for me. It happens for me, and several TestFlight users, and in our production app currently.

Any suggestions how to maintain keychain integrity and loading 100% of time? We're about ready to implement an NSUserDefaults backup storage on the token to use in these cases, something I really don't want to do to store an auth token.

Storing:

// load keychain
KeychainItemWrapper *keychainItem = [KeychainItemWrapper keyChainWrapperForKeyID:kcIdentifier];
NSString *firstLaunch = [keychainItem objectForKey: (__bridge id)(kSecAttrAccount)];
if (firstLaunch == nil){
    // initialize if needed
    [keychainItem setObject:email forKey: (__bridge id)(kSecAttrAccount)];
    [keychainItem setObject:kcIdentifier forKey: (__bridge id)kSecAttrService];
    [keychainItem setObject:(id)kSecAttrAccessibleAfterFirstUnlock forKey:(id)kSecAttrAccessible];
}

// serialize "auth" NSDictionary into NSData and store
NSString *error;
NSData *dictionaryData = [NSPropertyListSerialization dataFromPropertyList:auth format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
[keychainItem setObject:dictionaryData forKey:(id)kSecValueData];

Loading:

// after similar KeychainItemWrapper initialization as above
NSData *dictionaryData = [keychainItem objectForKey:(id)kSecValueData];
NSString *error;

NSDictionary *auth = [NSPropertyListSerialization propertyListFromData:dictionaryData mutabilityOption:NSPropertyListImmutable format:nil errorDescription:&error];
NSString *token = auth[@"access_token"];

I have also tried using the SSKeychain library CocoaPod that is widely available, and a wrapper around the keychain logic. It is a cleaner access but fails with the same issue. Here I'm just storing NSString values since there was no direct way to store NSData in the lib.

// store in keychain
[SSKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
[SSKeychain setPassword:auth[@"access_token"] forService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_TOKEN];
[SSKeychain setPassword:auth[@"expires_at"] forService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_EXPIRES_AT];
[SSKeychain setPassword:auth[@"refresh_token"] forService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_REFRESH_TOKEN];

// load from keychain
[SSKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
NSString *token = [SSKeychain passwordForService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_TOKEN];
NSString *expires_at = [SSKeychain passwordForService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_EXPIRES_AT];
NSString *refresh_token = [SSKeychain passwordForService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_REFRESH_TOKEN];
like image 463
Miro Avatar asked May 16 '16 12:05

Miro


1 Answers

Keychain does have issues at the moment, and for quite a while really. It sounds like you're getting off lightly as usually when it breaks a force-quit of the app is required to bring it back to life.

One thing that helps is to access the keychain just once on the first request and then cache the result in memory, if it's already in memory then just return it from there.

If you can observe a specific error when this happens then trap it and retry or, as is the current case for some unfortunate apps, kill the app. Killing the app is actually the current guidance from Apple if you raise a tech ticket to discuss the issue with them.

The only other real solution is to encrypt the data and store it in a file, but then you have issues with encryption keys so this is little better than obfuscation against a keen attacker.

like image 103
Wain Avatar answered Sep 29 '22 14:09

Wain