Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential solution to the background access problem #78

Open
RamblinWreck77 opened this issue Apr 9, 2018 · 17 comments
Open

Potential solution to the background access problem #78

RamblinWreck77 opened this issue Apr 9, 2018 · 17 comments

Comments

@RamblinWreck77
Copy link

RamblinWreck77 commented Apr 9, 2018

  • 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!

@evgenyneu
Copy link
Owner

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.

@RamblinWreck77
Copy link
Author

RamblinWreck77 commented Apr 9, 2018

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.

@evgenyneu
Copy link
Owner

evgenyneu commented Apr 9, 2018

@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. :)

@RamblinWreck77
Copy link
Author

@evgenyneu this is interesting, this guy:

kishikawakatsumi/KeychainAccess#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
Copy link
Author

RamblinWreck77 commented Apr 30, 2018

@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

@evgenyneu
Copy link
Owner

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?

@dmavromatis
Copy link

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

@evgenyneu
Copy link
Owner

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

@yoavziv
Copy link

yoavziv commented May 25, 2019

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

@evgenyneu
Copy link
Owner

evgenyneu commented May 26, 2019

@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.

@grEvenX
Copy link

grEvenX commented Jun 11, 2019

@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.

@enoelboostmi
Copy link

@grEvenX what did it do in the end?

@grEvenX
Copy link

grEvenX commented Jul 19, 2019

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

@evgenyneu
Copy link
Owner

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

@enoelboostmi
Copy link

@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?

@robertbarclay
Copy link

robertbarclay commented Nov 2, 2019

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.

@garyhooper
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants