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.
Related
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
I have seen in some touchId apps where on SecItemUpdate the touchId UI screen never pops up and the update still happens. I need similar functionality for my app and based on what I have read from Apple Developer's guide (my understanding maybe wrong) have come up with some options but they don't seem to work. Here's what I have done so far.
Setting kSecUseNoAuthenticationUI to YES, an error code -25308 is returned. Setting kSecUseNoAuthenticationUI to NO, an error code -50 is returned. If I don't include kSecUseNoAuthenticationUI, then the default authentication UI pops up.
NSDictionary *query = #{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: #"SampleService",
(__bridge id)kSecUseNoAuthenticationUI: #YES
};
NSDictionary *changes = #{
(__bridge id)kSecValueData: [#"UPDATED_SECRET_PASSWORD_TEXT" dataUsingEncoding:NSUTF8StringEncoding]
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)changes);
NSString *msg = [NSString stringWithFormat:NSLocalizedString(#"SEC_ITEM_UPDATE_STATUS", nil), [self keychainErrorToString:status]];
[super printResult:self.textView message:msg];
});]
So I am lost at this point. Appreciate if you can give me some pointers on how to disable this touchId UI popup on SecItemUpdate. Thanks
If you take a look at video WWDC 2014 Session 711, the kSecUseNoAuthenticationUI is mentioned around 31:35.
You can look also in "SecItem.h" :
#constant kSecUseNoAuthenticationUI Specifies a dictionary key whose value
is a CFBooleanRef. If provided with a value of kCFBooleanTrue, the error
errSecInteractionNotAllowed will be returned if the item is attempting
to authenticate with UI.
I'm not sure you can both disable the pop-up and perform an update.
What I suppose to understand : setting the kSecUseNoAuthenticationUI option will not display the pop-up. But if you 're trying to access to an item that requires an authentication, it will fail by indicating you that the item by returning a errSecInteractionNotAllowed as the operation result
According to the iOS8 release notes, this supposed is the case and you should delete and re-add your item
if (status == errSecDuplicateItem) { // exists
status = SecItemDelete((__bridge CFDictionaryRef)attributes);
if (status == errSecSuccess) {
status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
}
}
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.