92

I am currently storing the username (email) and a salted hash of the email and password in the iOS KeyChain. I'm using the ARC'ified version found here.

KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"MyCustomIdentifier" accessGroup:nil];
[wrapper setObject:APP_NAME forKey:(__bridge id)kSecAttrService];
[wrapper setObject:email forKey:(__bridge id)kSecAttrAccount];
[wrapper setObject:token forKey:(__bridge id)kSecValueData];

This all works fine when I need to pull the token out for my network calls while the app is active. It works for logging in from a clean startup, as well as all the network calls throughout. The trouble starts when the app is in the background.

Keep in mind, this only happens sporadically and I have yet to pin it down to a specific iOS version or device.

The user trips a location (region monitoring) and I want to update the server with their status. I try to pull the token out of the keychain, the same way I do for every other network call, and update the status. But for some users, the value is nil. Without it, I can't update the network stuff. Why would this work for most, but not for a small percentage?

KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"MyCustomIdentifier" accessGroup:nil];
NSString *token = [wrapper objectForKey:(__bridge id)kSecValueData];

I've gone back to the non-ARC version of the keychainwrapper, but I still get the same results. I would appreciate any feedback on this. It is only a small part of my users, but it is an issue I would like to fix and not worry about.

Also, all of my background work is set up in a backgroundTask to prevent things from timing out. I'm not having any issues with the work surrounding the keychain, but I don't let things go forward until my token is filled.

EDIT I've figured out my issue with they keychain not retrieving values from the background. I will post the answer below and accept it as I feel this question may become valuable to others later.

starball
  • 20,030
  • 7
  • 43
  • 238
Bill Burgess
  • 14,054
  • 6
  • 49
  • 86

4 Answers4

114

My question was close to the mark for the reason why, but not quite. After reading through blog after blog, tutorial after tutorial, I finally found one that gave off a hint of what might be happening.

Locked home screens. The keychain tutorials always left the accessibility settings for the keychain blank, so it would default to Apple's lowest/safest access level. This level however doesn't allow keychain access if the user has a passcode on the lock screen. Bingo! This explains the sporadic behavior and why this only happens to a small percentage of users.

One line of code, solves the entire mess.

[wrapper setObject:(__bridge id)kSecAttrAccessibleAlways forKey:(__bridge id)kSecAttrAccessible];

Add this line where I'm setting the username and password values. Works like a charm. Hope this will help someone out there. It confounded me for quite a while until I was able to put the pieces together.

Bill Burgess
  • 14,054
  • 6
  • 49
  • 86
  • 1
    So I just did a walking test of this, and I can confirm that this is the problem in our application as well. I would add that we had used an accessible setting of `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` (for some reason...), and so it was even more intermittent. Phone had to be completely off, wake up in the background BEFORE the user had unlocked their phone in order to replicate. Hell of a bug to find. – OC Rickard May 30 '13 at 18:10
  • 16
    Please avoid `…AccessibleAlways` if at all possible, or store a token that only provides limited privileges (e.g. a token that allows you to read for new feed items but not post). You are explicitly forgoing a level of encryption by doing so. If your app can wait until the first unlock, it would perhaps be best to use `…AfterFirstUnlock` and direct your users to unlock their devices first. – millenomi Oct 10 '13 at 02:37
  • 1
    Using AccessibleAlways has its risks, but that is up to the developer to decide. If you want to truly use the keychain while the device is locked and closed, then this is your only option. And hardly worth the downvote. The question and answer are obviously valuable. – Bill Burgess Oct 10 '13 at 17:52
  • 1
    This is not your only option. If the app has already been loaded you could be storing the information from the Keychain in memory because you shouldn't be constantly going to the Keychain. If the app has not yet been loaded but kSecAttrAccessibleAfterFirstUnlock was used then you are still able to load from the Keychain in the background. kSecAttrAccessibleAlways would only be needed if the phone has not yet been unlocked after a cold reboot which is a very rare cause and is an edge case that could be handled to prevent using this insecure option. – Reid Main Oct 10 '13 at 20:08
  • 14
    This is a really bad idea because it means that this credential data is no longer protected. While a little more work, it is important to create a *derivative credential* than can just be used for the limited access you expect to be required in the background and no more. That limited credential can be expired after some period of time, and a new one creates each time the app is opened, invalidating the old ones. This keeps the user safe in case the derivative credential is compromised. Refer to WWDC 2013 session 204 to hear about this. – Joey Hagedorn Oct 10 '13 at 22:22
  • 7
    echoing @JoeyHagedorn here - listen to WWDC 2013 Session 204 "What's New With Multitasking" at the 44:24 mark and WWDC 2013 Session 709 "Protecting Secrets with the Keychain" at the 25:30 mark. You can see the text content of these talks at http://asciiwwdc.com – Shazron Oct 11 '13 at 23:02
  • In ios9 this "kSecAttrAccessibleAlways" is depricated. HOw to solve the problem now. – Durgaprasad Aug 19 '15 at 05:46
  • 1
    I'm having some success using this approach in Swift with [SwiftKeychainWrapper](https://github.com/jrendel/SwiftKeychainWrapper). `KeychainWrapper.defaultKeychainWrapper().setObject(kSecAttrAccessibleAlways as String, forKey:kSecAttrAccessible as String)` – Matt Bearson Aug 19 '16 at 10:35
72

Use kSecAttrAccessibleAfterFirstUnlock instead of kSecAttrAccessibleAlways.


From Apple's documentation:

kSecAttrAccessibleAfterFirstUnlock
The data in the keychain item cannot be accessed after a restart until the device has been unlocked once by the user.

After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute migrate to a new device when using encrypted backups.

Moshe
  • 57,511
  • 78
  • 272
  • 425
woof
  • 1,288
  • 1
  • 11
  • 13
1

In my case, watchOS2 accesses keychain data on the iOS side.

At the beginning, kSecAttrAccessibleWhenUnlockedThisDeviceOnly is used. I can read the data no matter iPhone is locked or not. It is very confusing to me that I will receive Error when watch is trying to access the keychain: : SecTrustEvaluate [leaf IssuerCommonName SubjectCommonName]

And some case it will become: : SecOSStatusWith error:[-25308] Error Domain=NSOSStatusErrorDomain Code=-25308 "ks_crypt: e00002e2 failed to 'oe' item (class 6, bag: 0) Access to item attempted while keychain is locked." UserInfo={NSDescription=ks_crypt: e00002e2 failed to 'oe' item (class 6, bag: 0) Access to item attempted while keychain is locked.}

I will update my answer if I get more infos.

sktree
  • 101
  • 5
0

This might happen due to Apples data protection policy which is at some level obscure from developers perspective. Workaround is when app's launched check if keychain is accessible or not, if not accessible you might kill your app (with proper popup) depending your app types.

+(BOOL) isKeychainAccessible
{
    NSString *keychainTestKey = @"keychainTestKey";
    NSString *keychainTestValue = @"keychainTestValue";
    [self createKeychainValue:keychainTestValue forIdentifier:keychainTestKey];
    NSString *loadedValue = [self keychainStringFromMatchingIdentifier:keychainTestKey];
    [self deleteItemFromKeychainWithIdentifier:keychainTestKey];
    return ([keychainTestValue isEqualToString: loadedValue]);
}
Sazzad Hissain Khan
  • 37,929
  • 33
  • 189
  • 256