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;
}
}
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 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.
Im digging into Apple's Touch ID, more precisely the Local Authenticator.
The documentation as of now is pretty sparse.
Its mainly just this:
LAContext *myContext = [[LAContext alloc] init];
NSError *authError = nil;
NSString *myLocalizedReasonString = <#String explaining why app needs authentication#>;
if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
[myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:myLocalizedReasonString
reply:^(BOOL success, NSError *error) {
if (success) {
// User authenticated successfully, take appropriate action
} else {
// User did not authenticate successfully, look at error and take appropriate action
}
}];
} else {
// Could not evaluate policy; look at authError and present an appropriate message to user
}
as taken from https://developer.apple.com/documentation/localauthentication
The idea of using your fingerprint for authentication is nice. But I can add fingerprints in the device if I know the passcode. And its very easy to get the passcode, like you sit in the train next to ur victim and watch him/her enter the passcode.
I want to use the fingerprint as a way of secure authentication but want to be able to detect if new fingerprints were added since the last time I requested the fingerprint.
Apple is doing this for the AppStore. If you want to authenticate a transaction in the AppStore and have added a new Fingerprint since your last transaction, the AppStore requests your AppleId-Password. This is sane behaviour, because the phone might have been taken by someone else who knows the passcode and added his own fingerprint to buy something expensive.
My Question: Can I detect if a new fingerprint was added since the last time that I used Local Authenticator?
This is now possible in iOS9. The property evaluatedPolicyDomainState has been added to LAContext.
If the fingerprint database is modified (fingers were added or removed), the data returned by evaluatedPolicyDomainState will change. The nature of the changes cannot be determined but by comparing data of evaluatedPolicyDomainState after different evaluatePolicy calls you can detect that the set of fingerprints has been modified.
Note that this property is set only when evaluatePolicy is called and a succesful Touch ID authentication was performed, or when canEvaluatePolicy succeeds for a biometric policy.
As Keith stated, in iOS 9 it's possible. You should do it like this.
let context = LAContext()
context.canEvaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, error: nil)
if let domainState = context.evaluatedPolicyDomainState
where domainState == oldDomainState {
// Enrollment state the same
} else {
// Enrollment state changed
}
Every time you add or delete a fingerprint, the domain state changes. You need to call canEvaluatePolicy for evaluatedPolicyDomainStateto be updated.
In short; no.
In a bit more detail; the LocalAuthentication framework is a tightly-guarded black box. The information you get back from it is very limited. Your interaction with it goes something like this:
Ask it if it's able to authenticate for some type of policy (there is only 1 available at time of writing - Biometrics (Touch ID))
If it can, ask it to actually do it
The system takes over for the actual authentication
It lets you know if the authentication was successful or not (if not, it tells you why)
You have no concept of the actual authentication process (which finger was used, for example). This, of course, is by design. Apple does not want, nor need, to give you access to such information.
I would recommend to store the evaluatedPolicyDomainState value into keychain instead of storing it in NSUserDefault.
You can convert the data value of evaluatedPolicyDomainState into string, which is a 44 character string. Below is the code to convert the evaluatedPolicyDomainState data value into string -
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
if let domainState = context.evaluatedPolicyDomainState {
let bData = domainState.base64EncodedData()
if let decodedString = String(data: bData, encoding: .utf8) {
print("Decoded Value: \(decodedString)")
}
}
}
Now if the device owner made any change in Touch ID like adding a new finger Id; then this data value will be changed and you can take necessary steps to handle the change based on your project needs.
This is the solution to verify if a fingerprint was added or removed, and the difference between the Swift and ObjC solution is that canEvaluatePolicy just verify if something changes, while evaluatePolicy opens the modal verification.
Swift 5.2
let context = LAContext()
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
let defaults = UserDefaults.standard
let oldDomainState = defaults.object(forKey: "domainTouchID") as? Data
if let domainState = context.evaluatedPolicyDomainState, domainState == oldDomainState {
// Enrollment state the same
print("nothing change")
} else {
// Enrollment state changed
print("domain state was changed")
}
// save the domain state for the next time
defaults.set(context.evaluatedPolicyDomainState, forKey: "domainTouchID")
Objective-C
- (void)evaluatedPolicyDomainState {
LAContext *context = [[LAContext alloc] init];
__block NSString *message;
// show the authentication UI with reason string
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:#"Unlock access to locked feature" reply:^(BOOL success, NSError *authenticationError) {
if (success) {
// load the last domain state from touch id
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *oldDomainState = [defaults objectForKey:#"domainTouchID"];
NSData *domainState = [context evaluatedPolicyDomainState];
// check for domain state changes
if ([oldDomainState isEqual:domainState]) {
message = #"nothing change";
} else {
message = #"domain state was changed";
}
// save the domain state that will be loaded next time
oldDomainState = [context evaluatedPolicyDomainState];
[defaults setObject:oldDomainState forKey:#"domainTouchID"];
[defaults synchronize];
} else {
message = [NSString stringWithFormat:#"evaluatePolicy: %#", authenticationError.localizedDescription];
}
[self printMessage:message inTextView:self.textView];
}];
}
I would like to add something,
-(BOOL)hasFingerPrintChanged
{
BOOL changed = NO;
LAContext *context = [[LAContext alloc] init];
[context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:nil];
NSData *domainState = [context evaluatedPolicyDomainState];
// load the last domain state from touch id
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *oldDomainState = [defaults objectForKey:#"domainTouchID"];
if (oldDomainState)
{
// check for domain state changes
if ([oldDomainState isEqual:domainState])
{
NSLog(#"nothing changed.");
}
else
{
changed = YES;
NSLog(#"domain state was changed!");
NSString *message = #"Your Touch ID is invalidated, because you have added or removed finger(s).";
}
}
// save the domain state that will be loaded next time
[defaults setObject:domainState forKey:#"domainTouchID"];
[defaults synchronize];
return changed;
}
It's better to store user password etc in keychain.
I'm using https://github.com/reidmain/FDKeychain
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
}
I am working with authenticating user to use the google account he is associated with. The problem is that everytime the user logs in through my app, the "Allow Access" always appears on the Google's authentication view even I had clicked the Allow Access already from previous test. Is this normal or am I doing my codes wrong? Please help me guys.
I used the following codes for loggin in an out:
- (IBAction)signIn:(id)sender {
if(!isSignedIn){
[self signOutFromAll];
NSString *keychainItemName = nil;
// save keychain
keychainItemName = kKeychainItemName;
NSString *scope = #"https://www.googleapis.com/auth/plus.me";
NSString *clientID = kClientID;
NSString *clientSecret = kClientSecret;
SEL finishedSel = #selector(viewController:finishedWithAuth:error:);
GTMOAuth2ViewControllerTouch *viewController;
viewController = [GTMOAuth2ViewControllerTouch controllerWithScope:scope
clientID:clientID
clientSecret:clientSecret
keychainItemName:keychainItemName
delegate:self
finishedSelector:finishedSel];
[[self navigationController]pushViewController:viewController animated:YES];
} else {
[self displayAlertWithMessage:#"Currently Signed in."];
} }
- (IBAction)signOut:(id)sender {
[self signOutFromAll];
[self displayAlertWithMessage:#"Signed out."]; }
This is for the delegate:
- (void)viewController:(GTMOAuth2ViewControllerTouch *)viewController
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error{
if(error != nil){
// Authentication failed...
NSLog(#"Authentication error: %#", error);
NSData *responseData = [[error userInfo] objectForKey:#"data"];
if([responseData length] > 0)
NSLog(#"%#", [[[NSString alloc]initWithData:responseData encoding:NSUTF8StringEncoding]autorelease]);
self.auth = nil;
} else {
// Authentication succeeded...
isSignedIn = YES;
self.auth = auth;
}
}
And awakeFromNib:
- (void)awakeFromNib{
// Fill in the Client ID and Client Secret text fields
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// First, we'll try to get the saved Google authentication, if any, from the keychain
// Normal applications will hardcode in their client ID and client secret,
// But the sample app allows the user to enter them in a text field, and saves them in the preferences
NSString *clientID = [defaults stringForKey:kGoogleClientIDKey];
NSString *clientSecret = [defaults stringForKey:kGoogleClientSecretKey];
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:kKeychainItemName
clientID:clientID
clientSecret:clientSecret];
if (auth.canAuthorize) {
// There is saved google authentication
// self.serviceSegments.selectedSegmentIndex = 0;
}
// Save the authentication object, which holds the auth tokens
self.auth = auth;
[self setAuth:auth];
isSignedIn = self.auth.canAuthorize;
}
By the way my reference for these codes is on this link: http://code.google.com/p/gtm-oauth2/wiki/Introduction#Using_the_OAuth_2_Controllers
from the docs:
The keychain item name is used to save the token on the user’s keychain, and should identify both your application name and the service name(s). If keychainItemName is nil, the token will not be saved, and the user will have to sign in again the next time the application is run.
http://code.google.com/p/gtm-oauth2/wiki/Introduction
So, from your code, it depends on what kKeychainItemName is set to.
Just thought I'd comment on this as I was reading the docs.
Use this method when you get the oauth object to save into keychain
[GTMOAuth2ViewControllerTouch saveParamsToKeychainForName:YOUR_KEYCHAIN_ITEM_NAME authentication:auth];
and
before making a call to api just check and retrieve the oauth object using this
GTMOAuth2Authentication * auth = [GTMOAuth2ViewControllerTouch
authForGoogleFromKeychainForName:YOUR_KEYCHAIN_ITEM_NAME
clientID:GOOGLE_CLIENT_KEY
clientSecret:GOOGLE_CLIENT_SECRET];
and make sure it's oauth object is authentic with using this
if(![GTMOAuth2ViewControllerTouch authorizeFromKeychainForName:YOUR_KEYCHAIN_ITEM_NAME authentication:auth])
I know this is an old question, but I encountered the same issue so I'm writing my solution, it might help somebody else in the future.
Turns out it's not sufficient to only set self.auth, you also need to set the self.analyticsService.authorizer variable
if ([self.auth canAuthorize])
{
self.analyticsService.authorizer = self.auth;
[self getAnalyticsData];
return;
}
This did the trick for me, the user is no longer asked to enter the credentials.
Put the below code to logout / sign out from Google SDK.
- Call below function from where you want:
static NSString *const kKeychainItemName = #"MY_APP";
- (void)logoutFromGoogleDrive {
[GTMOAuth2SignIn revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)self.driveService.authorizer];
[GTMOAuth2ViewControllerTouch saveParamsToKeychainForName:kKeychainItemName authentication:nil];
}
[Note: Above code works, if you have used GTMOAuth2SignIn for sign in user for google access like,
GTMOAuth2Authentication * auth = [GTMOAuth2ViewControllerTouch
authForGoogleFromKeychainForName:YOUR_KEYCHAIN_ITEM_NAME
clientID:GOOGLE_CLIENT_KEY
clientSecret:GOOGLE_CLIENT_SECRET];
]
From my experience, this behavior is normal.
Are you having doubts because facebook only asks the user once if the user wants to grant the app privileges to access the user's profile?