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];
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With