Keychain returns errSecNotAvailable when using access control - ios

SecItemCopyMatching returns errSecNotAvailable when trying to read a kSecClassIdentity Keychain item on a physical device (iPhone 6S) when the item is saved with kSecAccessControlUserPresence.
When I run the code, the device asks me to authenticate using TouchID. I use my finger and the prompt goes away, but then it takes a relatively long time for SecItemCopyMatching to return, and when it does, it gives errSecNotAvailable.
This is weird, because TouchID works when I use LocalAuthentication (without using Keychain). Retrieving the certificate also works if I save it without the access control attribute. But I want to use kSecAccessControlUserPresence. Any idea why I get the error?
Adding certificate:
- (BOOL)keychainAddIdentity:(SecIdentityRef)identity withLabel:(NSString *)label {
CFErrorRef error = NULL;
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence, &error);
NSLog(#"SecAccessControlCreateWithFlags error: %#", error); // always null
NSDictionary *attributes = #{
(id)kSecAttrLabel: label,
(id)kSecValueRef: (__bridge id)identity,
(id)kSecAttrAccessControl: (__bridge id)sacObject
};
OSStatus status = SecItemAdd((CFDictionaryRef)attributes, NULL);
[self printOSStatus:status]; // errSecSuccess
return status == errSecSuccess;
}
Reading certificate:
- (SecIdentityRef)keychainGetIdentityWithLabel:(NSString *)label userPromptMessage:(NSString *)message {
NSDictionary *query = #{
(id)kSecClass: (id)kSecClassIdentity,
(id)kSecAttrLabel: label,
(id)kSecReturnRef: #YES,
(id)kSecUseOperationPrompt: message
};
SecIdentityRef identity = NULL;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef *)&identity);
[self printOSStatus:status]; // errSecNotAvailable
return identity;
}
Test code:
SecIdentityRef identity = [... load certificate file ...];
BOOL certSaved = [self saveCertificate:identity]; // YES
SecIdentityRef cert = [self loadCertificate]; // (null)
So when adding the certificate without sacObject, everything works fine, but with it, I get errSecNotAvailable. Why?

It appears that you're missing the key/value for kSecClass: kSecClassIdentity attribute when saving the identity to the keychain. Without specifying the class on the way in I don't think there's any way to read it later.

To my background, keychain data should be able to use the keychain after 200ms after launch of the app. So, when you want to reach this data? Do you have enough time to fetch this data.
Also keychain data can tie to your TouchID or faceID. But, maybe iphone6s's sensor is not good(we had one formerly, It'd not been working perfectly). To be ensure, you should try to reach this data with normal login.
And I'm sure, apple do not update keychain data documentation when It's changed. I met some scenario related to keychain, and sometimes apple documents has wrong info. (for example wrong keychain info in Apple document)
As a result, your job is not easy, please try above suggestions. If it doesn't work try another way to accomplish your requirements.

Related

iOS Firebase CrashlyticsKit not setting user ID

I am trying to set a user ID for Firebase Crashlytics reports.
Currently I send a user ID only if it's not sent or is changed (very rare event). And there is no user ID in crash reports.
My code:
+ (void)setCrashlyticsUserData:(User *)user
{
if (user == nil) { return; }
NSString *userIdKey = #"CRASHLYTICS_SENT_USER_ID";
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
NSInteger sentUserId = [userDefaults integerForKey:userIdKey];
if (sentUserId == user.userId) { return; }
[CrashlyticsKit setUserIdentifier:[NSString stringWithFormat:#"%i", user.userId]];
[userDefaults setInteger:user.userId forKey:userIdKey];
}
If this line is commented if (sentUserId == user.userId) { return; } I receive a user ID in crash reports.
Should I call [CrashlyticsKit setUserIdentifier:] every app launch? I can't find any information about it in the documentation.
Custom attributes of Crashlytics (like custom keys or user identifier) works in log-style in per-session basis.
So, you should call setUserIdentifier in each app session as early as possible.
See this link for code example:
https://fabric.io/kits/ios/crashlytics/features
You can use [Crashlytics setUserIdentifier:] to provide an id number, token, or hashed value that uniquely identifies the end-user of your application without disclosing or transmitting any of their personal information. You can also clear the value by setting it to a blank string. This value is displayed right in the Crashlytics dashboard.
[CrashlyticsKit setUserIdentifier:#"123456789"]; //User ID
Or another option is setthe custom key using [CrashlyticsKit setObjectValue:forKey:], See the following example.
[CrashlyticsKit setIntValue:3 forKey:#"userid"];
[CrashlyticsKit setObjectValue:#"logged_in" forKey:#"last_UI_action"];
See this document for more information.
https://docs.fabric.io/apple/crashlytics/enhanced-reports.html

iOS share extension does not receive data from Safari

I have an iOS share extension that needs the URL of the opened web page. Everything works good, especially in a simulator. But on a real device I have around 20-30% cases where the extension does not receive any data i.e.:
NSExtensionItem *inputItem = self.extensionContext.inputItems.firstObject;
NSItemProvider *item = inputItem.attachments.firstObject;
[item loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
// here the error is sometimes not nil and thus the _baseURI ends up nil
_baseURI = [item[NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:#"baseURI"];
}];
The error code is -100 with description "No item available for requested type identifier.". This happens mainly when I open the extension several times in a row without changing/refreshing the web page in the Safari.
In those situations I see a device log saying "iPhone kernel[0] : Sandbox: MobileSafari(7033) deny(1) file-read-data /private/var/containers/Bundle/Application/.../bundle.js" where the bundle.js is the javascript with the ExtensionPreprocessingJS object. The bundle.js declares the ExtensionPreprocessingJS object like this (extracted the relevant part):
ExtensionPreprocessingJS = {
run: function(arguments){
arguments.completionFunction({
"baseURI": document.baseURI
})
},
finalize: function(arguments){
}
}
In this situation, it could some time happen that when the extension is closed the next time opening the share dialog in Safari shows my extension with no icon. This happens on my testing iPhone 5s and iPhone 6 with iOS 9.3.
I think that the missing data is because of the system could not read the extension's JavaScript file, but why could this happen?
If you read the documentation for:
loadItemForTypeIdentifier(_:options:completionHandler:)
You'll see that:
The type information for the first parameter of your completionHandler
block should be set to the class of the expected type. For example,
when requesting text data, you might set the type of the first
parameter to NSString or NSAttributedString. An item provider can
perform simple type conversions of the data to the class you specify,
such as from NSURL to NSData or NSFileWrapper, or from NSData to
UIImage (in iOS) or NSImage (in OS X). If the data could not be
retrieved or coerced to the specified class, an error is passed to the
completion block’s.
Try this code to see what you recieve:
[item loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(id item, NSError *error) {
// here the error is sometimes not nil and thus the _baseURI ends up nil
_baseURI = [item[NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:#"baseURI"];
}];
Note that item is not set to NSDictionary.

removeTracksFromPlaylist not removing tracks with ios spotify sdk

I was testing this method to remove tracks from a playlist. Basically I modified the demo project "simple track playback" provided with the SDK. I wanted to remove the track form the playlist when you hit fastForward. I changed the fastForward method this way but it's not doing anything, and error is nil.
-(IBAction)fastForward:(id)sender {
if([self.player isPlaying] && self.currentPlaylistSnapshot){
SPTAuth *auth = [SPTAuth defaultInstance];
[self.currentPlaylistSnapshot removeTracksFromPlaylist:#[self.player.currentTrackURI]
withAccessToken:auth.session.accessToken
callback:^(NSError *error) {
if (error != nil) {
NSLog(#"*** Failed to remove track : %#", self.titleLabel.text);
return;
}
}];
}
[self.player skipNext:nil];
}
self.currentPlaylistSnapshot is the one I've got from the handleNewSession method.
There's also a static method apparently offering something similar which I have't tried yet.
createRequestForRemovingTracks:fromPlaylist:withAccessToken:snapshot:error:
According to the documentation both options are implemented asynchronously and will take seconds to reflect the results in the server but I'm suspecting that there's either something wrong or I'm just missing to do an actual request to push the changes on the local snapshot maybe?
Documentation:
https://developer.spotify.com/ios-sdk-docs/Documents/Classes/SPTPlaylistSnapshot.html#//api/name/removeTracksWithPositionsFromPlaylist:withAccessToken:callback:
ios sdk:
https://github.com/spotify/ios-sdk
I solved my issue by reseting simulator + adding SPTAuthPlaylistModifyPublicScope (which I fogot to do...)
auth.requestedScopes = #[SPTAuthStreamingScope, SPTAuthPlaylistModifyPublicScope];

Why does iOS get a new identifierForVendor when app updates?

Every time my app is updated from the App Store some small number of the users get a new identifierForVendor for some reason. My users don't sign up or login. They are all anonymous so I need to separate them through their vendor IDs.
I've considered that there could've been insufficient space on some devices, resulting in the app being deleted and reinstalled, but that's not the case since in the last update a friend of mine had over 2GB of empty space.
I know that the identifierForVendor is changed for a user who deletes and reinstalls the app. But that's not the case here, as the app is just updated.
What could the problem be? The weird part is that this hasn't happened for development team devices yet. Or there are still users who haven't experienced this bug after countless app updates, OS updates, etc. This only happens to a small percentage of the users. All users are iOS7+ and it happens for different device models and iOS versions.
I use this code to get their ID:
static let DeviceId = UIDevice.currentDevice().identifierForVendor.UUIDString
Then I save them into NSUserDefaults:
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "User" + DeviceId)
NSUserDefaults.standardUserDefaults().synchronize()
Then I check if the user exists, at every new login:
static func doesUserExist() -> Bool {
var userDefaultValue: AnyObject? = NSUserDefaults.standardUserDefaults().valueForKey("User" + DeviceId)
if defaultValue == true {
println("Userdefaults already has this guy, moving on")
FirstTime = false
return true
} else {
println("First time in the app!")
FirstTime = true
return false
}
}
If the user does exist it starts the login process. If the user does not exist it shows them the signup process. I am using Parse.com as backend and the deviceID is used as a username. When that small amount of users experience this bug, I see a new username and a new account created.
There was a bug affecting the calculation of identifierForVendor when updating an app from app store between May and July. Apple has claimed they have already solved the issue and pushing another update should restore the original value before the critical date.
Reference: https://openradar.appspot.com/22677034
Take note that some users still have observed this issue even updating weeks after July. So it is still possible that bug is still there for some or it could resurface anytime in the future. So if you are using this data as part of your encryption, best is to save this to the keychain.
Save the vendorID in the KeyChain, that will persist after deletion or any update.
-(NSString *)getUniqueDeviceIdentifierAsString
{
NSString *appName=[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleNameKey];
NSString *strApplicationUUID = [SSKeychain passwordForService:appName account:#"incoding"];
if (strApplicationUUID == nil)
{
strApplicationUUID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
[SSKeychain setPassword:strApplicationUUID forService:appName account:#"incoding"];
}
return strApplicationUUID;
}
Disclaimer: I took the code from some other SO answer a while ago, so
I don't remember whom to praise, but in the end it's not me
#Jonny found the source
I'm confused why you're saving the IFV to NSUserDefaults first and then checking to see if that key exists. I think you should...
1) check NSUserDefaults first to see if the IFV key you created exists
2) if NSUserDefaults key does exist, great do what you need with it, this user's profile already exists
3) if key does not exist, get the IFV and save it to NSUserDefaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults valueForKey:#"IFV"]) {
//this user already exists, do what you need to do next with IFV
}
else{
//this is their first time using the app
NSString *ifvString = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
[defaults setValue:ifvString forKey:#"IFV"];
//do what you need to do next with IFV
}

How to detect if a Facebook access token has been refreshed?

in my app I need to detect if an access token has been refreshed so that i can update my API with the new token.
I am having trouble trying to figure out the best way to approach this since the Facebook SDK automatically caches the access token - currently I am comparing the old token (which i save to NSUserDefaults) with the accessToken of the activeSession's accessTokenData.
If the tokens are different I send update my api with the newer token.
here is my code within applicationWillEnterForeground
NSString *newToken = [FBSession activeSession].accessTokenData.accessToken;
NSString *oldToken = [[NSUserDefaults standardUserDefaults]valueForKey:#"accessToken"];
if (oldToken) {
if ([newToken isEqualToString:oldToken]) {
NSLog(#"token was not refreshed");
} else {
NSLog(#"token was refreshed");
[[NSUserDefaults standardUserDefaults]setValue:newToken forKey:#"accessToken"];
/* update API with new token*/
}
} else+
{
//if no token exists, set one with the current token
[[NSUserDefaults standardUserDefaults]setValue:newToken forKey:#"accessToken"];
}
I have to admit it's really hacky, but I cant seem to find a way around it. Is there any better way of approaching it?

Resources