keychain-swift icon indicating copy to clipboard operation
keychain-swift copied to clipboard

Potential solution to the background access problem

Open RamblinWreck77 opened this issue 6 years ago • 17 comments

  • Library setup method: CocoaPods
  • Version of the library. Example: 11.0.0
  • Xcode version. Example: 9.3
  • OS version. Example: iOS 10.3 +

Hello! So I'm seeing anywhere from 5-10% of background wakeup sessions fail to recover a user's information stored in their keychain.

This SO post appears to have solved the issue, could this be implemented in keychain-swift?

https://stackoverflow.com/questions/10536859/ios-keychain-not-retrieving-values-from-background

Thanks for a great library!

RamblinWreck77 avatar Apr 09 '18 22:04 RamblinWreck77

Thanks, @RamblinWreck77. I can see that the the accepted answer suggests using kSecAttrAccessibleAlways:

keychain.set("Hello", forKey: "my key", withAccess: .accessibleAlways)

However, another answer recommends kSecAttrAccessibleAfterFirstUnlock setting instead for background access:

keychain.set("Hello", forKey: "my key", withAccess: .accessibleAfterFirstUnlock)

I think accessibleAlways will work, while accessibleAfterFirstUnlock is a more secure access option. Let me know if either of them work for you.

evgenyneu avatar Apr 09 '18 22:04 evgenyneu

Thanks for the quick reply @evgenyneu ! I am indeed using .accessibleAlways to store these values (they aren't "secrets" so i'm not worried).

digging into the code, I noticed:

var query: [String : Any] = [
      KeychainSwiftConstants.klass       : kSecClassGenericPassword,
      KeychainSwiftConstants.attrAccount : prefixedKey,
      KeychainSwiftConstants.valueData   : value,
      KeychainSwiftConstants.accessible  : accessible
    ]

I'm just spitballing here, but is it possible kSecClassGenericPassword is a culprit here? Maybe password-types get treated differently no matter the accessibility settings? Again this only occurs in the background (after a significant location wakeup event) and for 5-10% of sessions.

RamblinWreck77 avatar Apr 09 '18 22:04 RamblinWreck77

@RamblinWreck77, no idea to be honest. I could not find anything specific about the password item type rather than this from the docs:

kSecClassGenericPassword. The value that indicates a generic password item.

If you are adventurous you can try forking the library and using other class types instead to see if it makes any difference. :)

evgenyneu avatar Apr 09 '18 23:04 evgenyneu

@evgenyneu this is interesting, this guy:

https://github.com/kishikawakatsumi/KeychainAccess/issues/342

claims his problem was a naming issue, where iOS was using the preferences of a previous keychain. That would certainly effect me, since most of my users first interaction with keychain would have had the default (while unlocked) setting... and might explain why chaining it has had no effect.

RamblinWreck77 avatar Apr 30 '18 16:04 RamblinWreck77

@evgenyneu do you think the query generated by keychain.clear() would be enough to nuke the keychain and any preferences/settings, or should I explore an alternative approach?

I'm thinking: -Read old values with old keys -If those old values exist, nuke the keychain with clear + whatever else is needed -write old values with versioned keys + correct access level options from the get-go

RamblinWreck77 avatar Apr 30 '18 16:04 RamblinWreck77

claims his problem was a naming issue, where iOS was using the preferences of a previous keychain.

Sorry, I don't understand what you mean. What is "previous keychain"? Is the the item you saved to keychain previously that for the same key?

That would certainly effect me, since most of my users first interaction with keychain would have had the default (while unlocked) setting... and might explain why chaining it has had no effect.

I don't understand what you mean, sorry.

do you think the query generated by keychain.clear() would be enough to nuke the keychain and any preferences/settings, or should I explore an alternative approach?

I think clear will remove all the keys created by your iOS app from the keychain. You can also use the delete function to remove data for a single key.

I'm thinking: -Read old values with old keys -If those old values exist, nuke the keychain with clear + whatever else is needed -write old values with versioned keys + correct access level options from the get-go

Again, sorry, I don't understand what you are doing here. What do you want to achieve?

evgenyneu avatar May 02 '18 01:05 evgenyneu

I’m having the same issue where in background sometimes fails to get access to the keychain. Was there a solution?

dmavromatis avatar Apr 30 '19 03:04 dmavromatis

@dmavromatis, no there is no reliable solution unfortunately. It is a mess.

evgenyneu avatar Apr 30 '19 07:04 evgenyneu

https://medium.com/@yoav.ziv/userdefaults-value-returns-nil-although-its-shouldn-t-d55ddf832564

yoavziv avatar May 25 '19 12:05 yoavziv

@yoavziv, thanks for the workaround. TLDR:

  1. Use isProtectedDataAvailable to find out if the Keychain data is available.
  2. If it isn't, then wait for protectedDataAvailableNotification.

Please feel free to report here if this workaround works for you.

evgenyneu avatar May 26 '19 01:05 evgenyneu

@evgenyneu We're going to push a fix to an app we have on the AppStore with this workaround soon. Will reply back with our findings.

grEvenX avatar Jun 11 '19 06:06 grEvenX

@grEvenX what did it do in the end?

enoelboostmi avatar Jul 19 '19 04:07 enoelboostmi

@evgenyneu @enoelboostmi We have had the fix out for roughly three weeks now and we have not seen any issues after implementing the workaround 👍

grEvenX avatar Jul 19 '19 05:07 grEvenX

@grEvenX thanks for letting us know, this is good news!

evgenyneu avatar Jul 19 '19 07:07 evgenyneu

@grEvenX Just to be sure to understand, you are verifying that if isProtectedDataAvailable is true, then you access the Keychain? Would isProtectedDataAvailable return true when the app is in the background and the phone locked? From Apple's documentation (https://developer.apple.com/documentation/uikit/uiapplication/1622925-isprotecteddataavailable), it doesn't seem like it will return true when the phone is locked:

The value of this property is false if data protection is enabled and the device is currently locked. The value of this property is set to true if the device is unlocked or if content protection is not enabled.

What about the protectedDataAvailableNotification notification? Basically my question is can we reliably use the method described here: https://medium.com/@yoav.ziv/userdefaults-value-returns-nil-although-its-shouldn-t-d55ddf832564 to access the keychain in the background?

enoelboostmi avatar Jul 19 '19 13:07 enoelboostmi

I recently ran into this myself. I store information that must be accessed promptly upon the loading of the application from background during a VOIP pushkit event, so i cannot wait until the isProtectedDataAvailable method returns (new rules on iOS 13). Whenever the application launched while the screen was locked, it would not get the value from the SecItemCopyMatching and would return with a -25308. We are storing a public/private keypair generated for ECDH encryption into the keychain. As we were investigating this issue i came across this tidbit of info:

Essentially, each accessibility constant defines at which state of the phone a particular item is available. Such as an item with "WhenUnlocked" constant is only available when the device is unlocked whereas an item with "AccessibleAlways" constant is available irrespective of the phone is locked or not. The state of the phone and to enforce these constants, a special service is running is inside your phone which monitors the state of the device and provides authority over a requested item. It is either called a "Security Server" or "Security Daemon".

if you tail the console logs for the device you see securityd throw errors to the console when you are trying to access the keychain item and it fails from your application.

When the phone is locked or rebooted for the first time, the OS does not allow the applications to talk to this daemon. Since the application cannot get a response from the daemon, the application does not have the authority over the corresponding item. Hence, the error message -25308.

Well, it has to do with the SecItemCopyMatching(). The function returns errSecSuccess, if all the query criteria is satisfied or fails with appropriate error even if one of the query criteria is not satisfied.

In my original code #380 , "query" was not limited by accessibility constant which means all the items are to be dumped with single call to SecItemCopyMatching(). If the phone is in the unlocked state, all the items were dumped. But, if the phone is locked and then you try to dump it, SecItemCopyMatching() fails because few constants (WhenUnlocked) are not accessible, in which case even the accessible items (AccessibleAlways) are not returned.

The work around for this is to obviously dump items by passing Accessibility Constants individually to SecItemCopyMatching() #390 and #409. Items that are available are appended into an finalResult Array and items that are not available are failed silently #424.

We also discovered that when querying the keychain item, if you do not specify in the query the accessibility level, it defaults to "whenUnlocked" behind the scenes. So i had created my record with a weaker level then my query was allowing for, since it defaulted, and was not available. the when when unlocked is the most secure, and apple is defaulting to the most secure. You need a good reason to use it outside of that. In my case it's a public key, and it's in the public domain, so i do not need it when unlocked. my private key is in the secure enclave and i cannot even read the bytes.

So what we did to fix this in our custom keychain implementation was to (since we are specifying kSecAccessControl object rather than an accessibility constant that was needed for additional controls with Secure Enclave for ECDH keys) include the accessibility in every query dictionary used to query query into SecItemCopyMatching, not just when we were saving it. So if an item was created with a firstUnlockThisDeviceOnly, i need to query it with that same accessibility level. After that, it worked every time, locked and unlocked, in the background, etc.

I'm needing a quick library to do some keychain stuff and do not want to rewrite yet another keychain wrapper in this other project, and you guys seem to have this bug with the background/locked, this might fix your problems. While beginning implementing this pod in a project, i noticed the methods for setting a value had accessibility levels, but the ones for getting values do not, so i can only assume then that it is defaulting to the whenUnlocked default in the query, and you are not getting results from the query while locked in the background. What i propose is to add these parameters to the get methods, so you can be more explicit in the query, and it would likely solve the issue with background and lock. they could be defaulted in the methods to whenUnlocked, so it would not be a breaking change, but would allow for a less strict access control level if you need it when in the locked state. You may still need to use the isProtectedDataAvailable for some things, but that is primarily an event for the DataProtection Api, if you are setting that for your container, to access files that have been flagged as protected that are in the file system in your container. It makes sense why the work around is working, since when it available, you are actually unlocked.

robertbarclay avatar Nov 02 '19 13:11 robertbarclay

Thank you for your detailed explanation, @robertbarclay. Can you confirm one thing based on your findings?

If using kSecAttrAccessibleAfterFirstUnlock to read and write to the keychain, what happens when the phone is rebooted and the user does not immediately unlock it? If your app receives some sort of background event (e.g. a notification) before the first unlock, how do you handle it?

garyhooper avatar Jun 25 '21 06:06 garyhooper