I am facing a weird issue. Quite similar to one asked here, but not answered: Read from keychain results in errSecItemNotFound 25300
My code saves a string password in the iOS keychain to be accessed later on. It works just fine most of the times and I am able to fetch the password back after reinstallation or device restart or both.
Problem: Sometimes which is actually rare and hard to reproduce, it does not return the password and instead it returns null and error status:-25300(errSecItemNotFound). Another thing is that this problem got prominent after iOS 9 update. Happening on iOS 9.1 too.
Now, I have been searching the web for a solution. Found the following, which somehow relate to the issue, but do not address to my scenario:
iOS Keychain Data Lost Upon iPhone Memory Pressure?
https://forums.developer.apple.com/thread/4743
iOS KeyChain not retrieving values from background
Has anyone got any ideas why this is happening? Many thanks.
Updated
Code for setting:
NSMutableDictionary *query = [self _queryForService:service account:account];
[query setObject:password forKey:(__bridge id)kSecValueData];
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
if (status != errSecSuccess && error != NULL) {
*error = [NSError errorWithDomain:kAppKeychainErrorDomain code:status userInfo:nil];
}
return (status == noErr);
Final query:
{
acct = user;
class = genp;
svce = "myBundleIdentifier";
"v_Data" = <36314541 38463339 2d363737 462d3445 34372d42 4339452d 31324633 46463937 35374546>;}
Code for fetching:
CFTypeRef result = NULL;
NSMutableDictionary *query = [self _queryForService:service account:account];
[query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess && error != NULL) {
*error = [NSError errorWithDomain:kAppKeychainErrorDomain code:status userInfo:nil];
return nil;
}
return (__bridge_transfer NSData *)result;
Final query:
{
acct = user;
class = genp;
"m_Limit" = "m_LimitOne";
"r_Data" = 1;
svce = "myBundleIdentifier";}
I can see that the question is old, but I have recently almost gone mad trying to solve a similar issue with Keychain, so I will share it in case anyone faces it.
The problem was that the app would randomly crash when writing to the keychain in the background. And the reason is that when the user has a passcode on their phone and selected access level is the safest, iOS will not allow your application to make changes in the keychain while it's protected with a passcode.
Related
I have this method
- (NSString *)bundleSeedID:(NSError **)error __attribute__((annotate("oclint:suppress"))) {
NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge NSString *)kSecClassGenericPassword, (__bridge NSString *)kSecClass,
BUNDLE_SEED_ID, kSecAttrAccount,
EMPTY_STRING, kSecAttrService,
(__bridge NSString *)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, (__bridge NSString *)kSecAttrAccessible,
(id)kCFBooleanTrue, kSecReturnAttributes,
nil];
CFDictionaryRef result = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
if (status == errSecItemNotFound) {
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
}
if (status != errSecSuccess) {
if (error) *error = [ErrorUtils bundleSeedErrorWithCode:(int)status];
return nil;
}
NSString *accessGroup = [(__bridge NSDictionary *)result objectForKey:(__bridge NSString *)kSecAttrAccessGroup];
NSArray *components = [accessGroup componentsSeparatedByString:#"."];
NSString *bundleSeedID = [[components objectEnumerator] nextObject];
CFRelease(result);
return bundleSeedID;
}
and I don't understand why sometimes result is null.
Note: This code is always being executed in main thread without any asynchronous call
Can you please help me understand why is this happening? This code is only executed when I open the app.
I know I could check if result is null and then I won't CFRelease(result); but it's not expected to happen this. Once the app crashes, if I open it again, everything is ok.
Thanks in advance 🙏
You are very likely launching in the background when the device is locked. While the device is locked, you don't have access to certain protected data in the keychain; you should see this in the error message.
You do appear to be passing kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly to the creation, it's possible that you added kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly to your app after creating the keychain items (or that you don't pass this during creation elsewhere).
Your code checks for errSecItemNotFound, but you should be checking for errSecDataNotAvailable if I recall correctly to detect this problem. (You can also check UIApplication's `isProtectedDataAvailable.) You shouldn't try to create an item in that case of course; you just need an elegant way to fail.
It's fairly rare to be launched before first unlock, but that's also possible, and the keychain won't be available. Pending Bluetooth connections, for example, can cause this as I recall. I don't believe that push notifications can launch the app in that state, but it's possible.
In any case, you need to be checking for unexpected errors and handling a nil value.
I'm trying to add the functionality of touchIDAuthenticationAllowableReuseDuration to my app. I use Touch ID to authenticate a user into the app and at the same time recover an item from the keychain. Before I tried to add this there was no problem, it always asked for Touch ID or if not available for the device passcode. Now so far I've managed to make it so that it does the same thing, and when opening the app within the specified timeout it doesn't show the Touch ID prompt as it should, if all I was doing was authenticating the user I'd be done, but the problem that I'm having is that I also want to recover an item from the keychain, and when the prompt is bypassed with success, but once I call SecItemCopyMatching(…) I don't get the item back, instead I keep getting errSecAuthFailed.
I've looked online everywhere and the best thing I've found yet is Apple's sample code KeychainTouchID, but again, it doesn't do both authentication and getting an item from the keychain at the same time, I tried to add that to their code and I kept getting the same error as well.
Has anyone tried something like this before? How did you make it work? This is the code I have right now:
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDAny, nil);
NSString *localizedReason = NSLocalizedString(#"Authenticate to access app", nil);
LAContext *context = [[LAContext alloc] init];
context.touchIDAuthenticationAllowableReuseDuration = 5;
[context evaluateAccessControl:sacObject operation:LAAccessControlOperationUseItem localizedReason:localizedReason reply:^(BOOL success, NSError *error) {
if (success) {
NSDictionary *query = #{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: PASSCODE_KEY,
(__bridge id)kSecReturnData: #YES,
(__bridge id)kSecUseOperationPrompt: localizedReason,
(__bridge id)kSecUseAuthenticationUI: (__bridge id)kSecUseAuthenticationUIAllow,
(__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacObject,
(__bridge id)kSecUseAuthenticationContext: context
};
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
// This works when using Touch ID / passcode, but I get errSecAuthFailed when the prompt isn't shown because of the reuse duration.
if (status == errSecSuccess) {
NSData *resultData = (__bridge_transfer NSData *)dataTypeRef;
NSString *result = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
self.recoveredString = result;
} else {
self.recoveredString = #"";
}
} else {
self.recoveredString = #"";
CFRelease(sacObject);
}
}];
Don't create LAContext object each time. Just hold onto the LAContext object on which evaluateAccessControl has succeeded. That way you don't need to set touchIDAuthenticationAllowableReuseDuration. If you call evaluateAccessControl on a LAContext object on which evaluateAccessControl has already succeeded then reply callback is called immediately with success without user being asked to auth again
And when you want user to auth again, just invalidate the LAContext object.
I've been using KeychainItemWrapper just fine. But since I've updated my phone to iOS 9, it doesn't store the sessionID for some reason.
+ (BOOL)createKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier
{
NSMutableDictionary *dictionary = [self setupSearchDirectoryForIdentifier:identifier];
NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:valueData forKey:(__bridge id)kSecValueData];
// Protect the keychain entry so it's only valid when the device is unlocked at least once.
[dictionary setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
// **THIS LINE OF CODE RETURNS -34108**
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dictionary, NULL);
// If the addition was successful, return. Otherwise, attempt to update existing key or quit (return NO).
if (status == errSecSuccess) {
return YES;
} else if (status == errSecDuplicateItem){
return [self updateKeychainValue:value forIdentifier:identifier];
} else {
return NO; **The call returns here...**
}
}
Anybody know whats going on?
EDIT
Weirdest thing: it only happens from time to time and always in debug mode.
EDIT2
As this only occurs in debug mode, there are two work arounds that I usually do depending on the type of variable:
- Always keep the last valid variable loaded from the keychain locally (for instance a sessionID) and use it as a backup when in debug mode
- Ignore invalid value(s) if possible when in debug (in this case I would add an additional control variable to allow/disallow these invalid value(s))
(use #ifdef DEBUG to check if you're in debug mode)
According to Apple there is no solution right now.
https://forums.developer.apple.com/thread/4743
How can I detect if an iOS app is installed for the first time or was deleted and re-installed?
I do use NSUserDefaults when the app is running. However, when the app is deleted, I believe all associated data is deleted.
My goal is to do SMS verification of the users phone number only when the app was installed for the first time on the device.
If for some reason, the app was deleted and re-installed, I want to avoid redoing the SMS verification.
What else can I do? Can I store some metadata related to my app which is not deleted when the app itself is deleted on the device?
Any standard pattern to follow?
You can do this by storing a value in the user's keychain. It will persist even if the app is deleted and thus you can tell if the app is a new install or a reinstall. Add another value to the user defaults for comparison, if both values exist the app has been installed and executed at least once. If neither values exist, it's a new, first time install. If just the keychain value exists, it's a fresh reinstall.
You can use keychain and keychain does store values after we uninstall the app.
Apple has provided KeyChainItemWrapper class in their GenericKeyChain (sample code)
Try this code of storing your device code in keychain.....and keychain value remains the same even if app is deleted:
static NSString *serviceName = #"com.mycompany.myAppServiceName";
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSData *passwordData = [self searchKeychainCopyMatching:#"device--code"];
if (passwordData) {
NSString *password = [[NSString alloc] initWithData:passwordData
encoding:NSUTF8StringEncoding];
NSLog(#"pasword:=%#",password);
}
else{
[self createKeychainValue:devicecode forIdentifier:#"device--code"];
}
//[self deleteKeychainValue:#"device--code"];
return YES;
}
- (void)deleteKeychainValue:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
SecItemDelete((CFDictionaryRef)searchDictionary);
}
- (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init];
[searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
NSData *encodedIdentifier = [identifier dataUsingEncoding:NSUTF8StringEncoding];
[searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrGeneric];
[searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrAccount];
[searchDictionary setObject:serviceName forKey:(id)kSecAttrService];
return searchDictionary;
}
- (NSData *)searchKeychainCopyMatching:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
// Add search attributes
[searchDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
// Add search return types
[searchDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
NSData *result = nil;
// OSStatus status =
SecItemCopyMatching((CFDictionaryRef)searchDictionary,(__bridge CFTypeRef *)&result);
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)searchDictionary,
(void *)&result);
return result;
}
- (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
NSMutableDictionary *dictionary = [self newSearchDictionary:identifier];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:passwordData forKey:(id)kSecValueData];
OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL);
if (status == errSecSuccess) {
return YES;
}
return NO;
}
Source
There is no way to accomplish this with iOS alone, but you could achieve this by having a server with a database, and the first time the app is run you send the device's UDID to your server; if the server already has the UDID, then the app was already installed.
Of course this solution does require the user have an internet connection.
I think, you send UDID to icloud Apple to store it. If app install first time. Icloud iphone not found, else It will have UDID. You need't server.
I'm using this method in order to retrieve a saved value (and using SecItemAdd to add it originally):
+ (NSData *)passwordDataForService:(NSString *)service
account:(NSString *)account error:(NSError **)error {
CFTypeRef result = NULL;
NSMutableDictionary *query = [self _queryForService:service account:account];
[query setObject:(__bridge id)kCFBooleanTrue
forKey:(__bridge id)kSecReturnData];
[query setObject:(__bridge id)kSecMatchLimitOne
forKey:(__bridge id)kSecMatchLimit];
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != noErr && error != NULL) {
*error = [NSError errorWithDomain:kSSKeychainErrorDomain code:status
userInfo:nil];
return nil;
}
return (__bridge_transfer NSData *)result;
}
This code is working fine for most users, but a small percentage of my users (< 1%) are experiencing results indicating that either the read or write here is failing. My code unfortunately swallows any errors (i.e. doesn't log them anywhere when they occur) so I can't tell why it's failing out in the world, and I can't reproduce the problem at all on any of my development devices.
Does anyone know of any security/permissions settings that can be enabled on an iOS device that could cause SecItemAdd or SecItemCopyMatching to fail? I've tried turning on passcode locking, but that seems to have no effect.