We have an app that stores sensitive data. We've enabled file protection, but that only has an effect if the user has set a passcode. If the user hasn't set a passcode, we need to show an alert telling the user to do that, and then not to load the rest of the app.
Basically we're in the exact situation as in this question, and my question is exactly their question. But the accepted answer there is "enable file protection", which is not an answer to that question, or to this one; I'm already enabling file protection and it doesn't tell me whether they've set a passcode or not.
So is it possible to check, and if so, how? Ideally we'd like to check whether the user has set a long passcode or a simple one, and if they've only set a simple one we would warn them to set a proper one.
There is an official answer to this question with iOS 9:
LAContext *myContext = [[LAContext alloc] init];
NSError *authError = nil;
if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError])
{
// Device has either passcode enable (or passcode and touchID)
}
else
{
// Device does not have a passcode
// authError can be checked for more infos (is of type LAError)
}
A link to Apple's LAContext class
A note: LAPolicyDeviceOwnerAuthentication is a constant available in iOS 9 only. iOS 8 had LAPolicyDeviceOwnerAuthenticationWithBiometrics available. It can be used to achieve some results but these do not answer your question.
With iOS 8, there is now a way to check that the user has a passcode set. This code will crash on iOS 7.
Objective-C:
-(BOOL) deviceHasPasscode {
NSData* secret = [#"Device has passcode set?" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *attributes = #{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: #"LocalDeviceServices", (__bridge id)kSecAttrAccount: #"NoAccount", (__bridge id)kSecValueData: secret, (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly };
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
if (status == errSecSuccess) { // item added okay, passcode has been set
SecItemDelete((__bridge CFDictionaryRef)attributes);
return true;
}
return false;
}
Swift:
func deviceHasPasscode() -> Bool {
let secret = "Device has passcode set?".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let attributes = [kSecClass as String:kSecClassGenericPassword, kSecAttrService as String:"LocalDeviceServices", kSecAttrAccount as String:"NoAccount", kSecValueData as String:secret!, kSecAttrAccessible as String:kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly]
let status = SecItemAdd(attributes, nil)
if status == 0 {
SecItemDelete(attributes)
return true
}
return false
}
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.
In my app when touch ID is enabled I save the username and password in keychain and add it to the available fingerprint added on device. So, user can login to the app with touch from next time onwards. Now when the password is changed, and user tries to login with touch ID it gives an error which is correct, now when the user enters the new password manually I need to update the keychain so that the touch login would work from next time onwards. But the problem is when I call the update method it gives me the touch ID prompt. Is there a way to update it without showing the prompt? Below is my current update method
+(BOOL) updatePassword:(NSString *)password{
NSDictionary *query = #{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrService : #"passwordService",
(id)kSecUseOperationPrompt : NSLocalizedString(#"Update Prompt", #"")
};
NSData *newPassword = [password dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *changes = #{
(id)kSecValueData : newPassword
};
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)changes);
if (status == errSecSuccess) {
return YES;
}
else{
return NO;
}
}
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 successfully able to implement TouchID with keychain as well as Keychain Sharing (syncing keychain items between multiple devices) separately. When I try to do them both, I get an error “-50“ which is invalid parameters.
From the below code, removing either kSecAttrAccessControl or kSecAttrSynchronizable works as expected.
Based on my experience (read - a few days of frustration) so far, and based on the capabilities of some keychain API simplification tools like UICKeychainStore, it seems like if I use Touch ID Authentication, Keychain Sharing wouldn’t work and vice versa. I’m looking for an Apple documentation that would state that, but unable to find it.
I’ve gone through Apple’s SecItem.h page, and a useful info I found states the following about kSecAttrAccessible and kSecAttrSynchronizable:
“If both attributes are specified on either OS X or iOS, the value for the kSecAttrAccessible key may only be one whose name does not end with “ThisDeviceOnly", as those cannot sync to another device.” However, I'm not using "ThisDeviceOnly" (I'm currently using kSecAttrAccessibleAlways for testing purposes)
Can you help in pointing out if and where Apple has documented this limitation? That would help me document it for the records, and move on. Thanks.
- (void)addKeychainItemWithIdentifier:(NSString *)identifier andData:(NSData *)data {
CFErrorRef error = NULL;
SecAccessControlRef sacObject;
sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleAlways,
kSecAccessControlUserPresence, &error);
if(sacObject == NULL || error != NULL)
{
NSString *msg0 = [NSString stringWithFormat:NSLocalizedString(#"SEC_ITEM_ADD_CAN_CREATE_OBJECT", nil), error];
[self printResultWithMessage:msg0];
return;
}
NSDictionary *attributes = #{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecValueData: data,
(__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleAlways,
(__bridge id)kSecAttrService: identifier,
(__bridge id)kSecAttrSynchronizable:(__bridge id)kCFBooleanTrue,
(__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacObject
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
NSError *statuserror = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
[self printResultWithMessage:[self keychainErrorToString:status]];
});
}
I think I may have found the answer to this
In WWDC 2014 video 711, the following is mentioned at 31:48
ACL Protected Items - No Synchronization, No Back up
Thus Touch ID authentication cannot be used for Keychain Sharing between devices as those items are Device-Only
This example project might help, the title is KeychainTouchID: Using Touch ID with Keychain and LocalAuthentication:
https://developer.apple.com/library/ios/samplecode/KeychainTouchID/Introduction/Intro.html
This might be limited to local though, no sharing.
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);
}
}