I am trying to use standard code of keychain from Apple's sample code KeychainTouchID
Here is snippet of my code,
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecAccessControlUserPresence, &error);
if (sacObject == NULL || error != NULL) {
NSString *errorString = [NSString stringWithFormat:#"SecItemAdd can't create sacObject: %#", error];
self.textView.text = [self.textView.text stringByAppendingString:errorString];
return;
}
// we want the operation to fail if there is an item which needs authentication so we will use
// kSecUseNoAuthenticationUI
NSDictionary *attributes = #{
(id)kSecClass: (id)kSecClassGenericPassword,
(id)kSecAttrService: #"SampleService",
(id)kSecValueData: [#"SECRET_PASSWORD_TEXT" dataUsingEncoding:NSUTF8StringEncoding],
(id)kSecUseAuthenticationUI: (id)kSecUseAuthenticationUIAllow,
(id)kSecAttrAccessControl: (__bridge_transfer id)sacObject
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
NSString *errorString = [self keychainErrorToString:status];
NSString *message = [NSString stringWithFormat:#"SecItemAdd status: %#", errorString];
[self printMessage:message inTextView:self.textView];
});
But key chain always returns error errSecAuthFailed. Same code is code is working on one device iPhone6+ but it is not working on iPhone5 with iOS 9.x.
Tried a lot to find reason but didn't find anything useful. One important thing that I noted is that if I don't set attributed kSecAttrAccessControl then it is working fine on iPhone5 as well. so, I believe it is something related to setting access control attribute.
Any help would be appreciated,
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 am integrating Touch ID access in one of my app. I have successfully integrated it. Here is that code:
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.75 * NSEC_PER_SEC), highPriorityQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
LAContext *context = [[LAContext alloc] init];
isTouchExists = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil];
if (isTouchExists) {
NSString * keychainItemIdentifier;
NSString * keychainItemServiceName;
keychainItemIdentifier = #"fingerprintKeychainEntry";
keychainItemServiceName = [[NSBundle mainBundle] bundleIdentifier];
NSData * pwData = [#"the password itself does not matter" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary * attributes = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
(__bridge id)(kSecClassGenericPassword), kSecClass,
keychainItemIdentifier, kSecAttrAccount,
keychainItemServiceName, kSecAttrService, nil];
CFErrorRef accessControlError = NULL;
SecAccessControlRef accessControlRef = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlUserPresence,
&accessControlError);
if (accessControlRef == NULL || accessControlError != NULL)
{
NSLog(#"Cannot create SecAccessControlRef to store a password with identifier “%#” in the key chain: %#.", keychainItemIdentifier, accessControlError);
}
attributes[(__bridge id)kSecAttrAccessControl] = (__bridge id)accessControlRef;
attributes[(__bridge id)kSecUseNoAuthenticationUI] = #YES;
attributes[(__bridge id)kSecValueData] = pwData;
CFTypeRef result;
OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef)attributes, &result);
if (osStatus != noErr)
{
NSError * error = [[NSError alloc] initWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil];
NSLog(#"Adding generic password with identifier “%#” to keychain failed with OSError %d: %#.", keychainItemIdentifier, (int)osStatus, error);
}
//other my code for success
}
});
});
Now, If I remove all the fingerprints from settings in iPhone, This code will work and ask for passcode. So My question is:
how can I come to know that there is no any fingerprints added for Touch ID?
I don't want to show iOS device passcode screen as I have already built passcode screen for my app security. So is there any option to check device have atleast one fingerprint available for Touch ID access?
Thanks in advance.
======== EDIT 1 ===========
It is working on my side also. The issue is I need to check it each time when I am asking for Touch ID. I need to fetch status in viewWillAppear or in applicationDidBecomeActive each time whenever I want to use Touch ID access in app, as I am removing fingers run time, it may not reflecting in my code so I need to fetch each time.
canEvaluatePolicy:error: will be Error : LAErrorTouchIDNotEnrolled
Authentication could not start because Touch ID has no enrolled
fingers.
APPLE DOC Ref.
Try:
LAContext *context = [[LAContext alloc] init];
NSError *error;
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:#"My Reason" reply:^(BOOL success, NSError * _Nullable error) {
}];
}else{
if (error.code == LAErrorTouchIDNotEnrolled) {
NSLog(#"Error: %#", error.localizedDescription);
}
}
If there are no fingerprints registered, canEvaluatePolicy should return false.
Source : https://developer.apple.com/documentation/localauthentication/lacontext/1514149-canevaluatepolicy?language=objc
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 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);
}
}
I've got a problem with my inter-app communication on iPad which (up to recently) has been working. I'm using standard pasteboard code as found in http://enharmonichq.com/sharing-data-locally-between-ios-apps/ which is an excellent tutorial and works well.
My problem is that now my 'viewer' app is not receiving the pasteboard when it opens.
The code:
+(void)handleSendPasteboardDataURL:(NSURL *)sendPasteboardDataURL
completionHandler:(ENHAppDataSharingHandler)completionHandler;
{
NSString *query = [sendPasteboardDataURL query];
NSString *pasteboardName = [sendPasteboardDataURL fragment];
NSAssert2(([query isEqualToString:kReadPasteboardDataQuery] && pasteboardName),
#"Malformed or incorrect url sent to %#. URL: %#",
NSStringFromSelector(_cmd), sendPasteboardDataURL);
AppDataPackage *dataPackage = nil;
NSError *error = nil;
NSString *pasteboardType = kAppDataPackageUTI;
UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:pasteboardName create:NO];
if (pasteboard)
{
NSData *data = [pasteboard dataForPasteboardType:pasteboardType];
if (data)
{
dataPackage = [AppDataPackage unarchivePackageData:data];
}
else
{
NSDictionary *errorInfoDictionary = #{NSLocalizedDescriptionKey: [NSString stringWithFormat:
#"%# %#", NSLocalizedString(#"No data found on pasteboard with name:", nil),
pasteboardName]};
error = [NSError errorWithDomain:AppDataSharingErrorDomain
code:ENHAppDataSharingErrorTypeNoDataFound
userInfo:errorInfoDictionary];
}
[pasteboard setData:nil forPasteboardType:pasteboardType];
[pasteboard setPersistent:NO];
}
else
{
NSDictionary *errorInfoDictionary = #{NSLocalizedDescriptionKey:
[NSString stringWithFormat:#"%# %#",
NSLocalizedString(#"No pasteboard found for name:", nil), pasteboardName]};
error = [NSError errorWithDomain:AppDataSharingErrorDomain
code:ENHAppDataSharingErrorTypeNoPasteboardForName
userInfo:errorInfoDictionary];
}
completionHandler(dataPackage, error);
}
This has worked well previously on the device and still works well on the simulator. But now when tested on the device, the call...
[UIPasteboard pasteboardWithName:pasteboardName create:NO];
...fails and the pasteboard is nil. (pasteboard names are valid)
As I said this works OK on the simulator but not on the device.
So my question is if anyone else has had experience with the same problem?
Any suggestions as to what it could be?
My source app is working as other pasteboard 'test' apps can receive the data and work on the device. It's just my main receiving app is failing to get the pasteboard in question.
I'm a bit at my whit's end with this one.
This is running iOS7.1.2 and developing on xcode 5.1.1
Thanks