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

Unable to use encrypted storage after restoring from iCloud #269

Open
abejfehr opened this issue Jul 15, 2022 · 29 comments
Open

Unable to use encrypted storage after restoring from iCloud #269

abejfehr opened this issue Jul 15, 2022 · 29 comments

Comments

@abejfehr
Copy link

abejfehr commented Jul 15, 2022

Describe the bug
Either getting or setting in storage with encryption is failing.

For context, this is the wrapper around the library:

import { MMKVLoader } from 'react-native-mmkv-storage';

const SecureStorage = new MMKVLoader()
  .withEncryption()
  .withInstanceID('mySecureStorage')
  .initialize();

export const setItem = (key: string, value: string): void => {
  SecureStorage.setString(key, value);
};

export const getItem = (key: string): string | null | undefined =>
  SecureStorage.getString(key);

and this being the set of actions taken:

SecureStorage.setItem(SOME_KEY, someValue); // this happens and does not throw an error
// ...and even about 1 second later...
SecureStorage.getItem(SOME_KEY); // this does not yield a value

To Reproduce
I have no consistent reproduction steps 😅 it's happening to a lot of people, but only rarely. On devices where this happens, it happens consistently. It's almost like react-native-mmkv-storage is "broken" for that device.

Expected behavior
Setting a value synchronously should allow you to get it right after.

Platform Information:

  • OS: iOS
  • React Native Version 0.68.2
  • Library Version 0.7.6

Additional context
On the current device we have that has this issue, a recent restore from iCloud was performed, but it hasn't been proven that this is the cause.

It's worth noting that we're on version 0.7.6 of this library, which gives us the following dependencies in Podfile.lock:

  ...
  - MMKV (1.2.10):
    - MMKVCore (~> 1.2.10)
  - MMKVCore (1.2.13)
  ...

What's interesting about that is that backup & restore was introduced in MMKV 1.2.11, so our version of MMKVCore would have that functionality, but MMKV does not. Is that an issue?

@abejfehr
Copy link
Author

abejfehr commented Jul 15, 2022

I just managed to reproduce this, here would be the reproduction instructions:

  1. Use this library in a production app on the App Store
  2. Log into the app and set values in an encrypted MMKV store
  3. Back up your device to iCloud (this happened automatically for me)
  4. Get a new iPhone (the bug doesn't happen if you use the same device)
  5. Restore backup from iCloud and wait for the app to re-install
  6. Observe that things can no longer be set in an encrypted store in the restored app

The hand wavey working theory is that the key used to encrypt the contents of the store is somehow device-specific, so when those files are restored they can't be read/written by the app on the new device

@abejfehr abejfehr changed the title Unable to use encrypted storage Unable to use encrypted storage after restoring from iCloud Jul 15, 2022
@ammarahm-ed
Copy link
Owner

@abejfehr Ofcourse the key is device specific since it is auto-generated internally and stored in the Keychain.

So on a new device the only way to decrypt the same storage would be to somehow be able to have access to the same Keychain on the primary device. I am not sure if it is possible to share the Keychain or sync it?

Other than that, the only other way is to use a custom key which you will manage and not the library. If the library has to manage the key then you will have to somehow move the key to the other device when a person logs in etc.

@abejfehr
Copy link
Author

In one of the reproductions the keychain was synced via iCloud and this issue was still present unfortunately 🙁 so based on that it seems like using this library for encrypted storage without a custom key is just expected not to be stable between devices?

@ammarahm-ed
Copy link
Owner

ammarahm-ed commented Jul 18, 2022

@abejfehr It seems that setting the kSecAttrSynchronizable is required to make the values syncable. I don't think we are setting such an attribute currently. Setting that should solve the issue.

https://stackoverflow.com/questions/42903633/store-item-in-ios-keychain-without-icloud-sync

- (BOOL)createKeychainValue:(NSString *)value forIdentifier:(NSString *)identifier options: (NSDictionary * __nullable)options {

@abejfehr
Copy link
Author

abejfehr commented Jul 18, 2022

But that would only resolve the issue for folks who choose to sync their keychain, right?

On my iCloud for example, I have "iCloud Backup" set to "on" and "Keychain" set to "off", so my keychain and my app backups would be out of sync and my app wouldn't survive a backup and restore.

Even besides getting backup & restore working, I'd like to have the following to at least be able to mitigate it:

  • exceptions thrown when something can't be set in the store (like this scenario where the encryption key doesn't match)
  • if an exception does happen because the key we have isn't valid for the file on disk, entirely remove the storage and re-create it (or provide an API for consumers to do that themselves)

On that second point, the most we can do right now is clearStore, and it doesn't do enough to resolve the issue

@abejfehr
Copy link
Author

What about verifying on app load that the key works with the store, and entirely deleting the store if it does not? That way there's not an unusable MMKV store

@abejfehr
Copy link
Author

@ammarahm-ed
Copy link
Owner

I agree. There should be a way to work around this. I will look into it. The simplest way would be to exclude mmkv files from cloud backup and throw some error upon which store can be deleted and recreated possibly.

@ou2s
Copy link

ou2s commented Oct 10, 2022

Des anyone know how to solve this issue ?

@jarnove
Copy link

jarnove commented Jan 17, 2023

@ammarahm-ed , @abejfehr , any solution yet?

@jarnove
Copy link

jarnove commented Jun 22, 2023

Still no idea @ammarahm-ed ? Checking myself, but currently also no solution.

@abejfehr
Copy link
Author

abejfehr commented Jun 26, 2023

@jarnove I have no solution yet 😢

I actually attempt to write/read to the encrypted store 3 times and if it fails I ask clients to uninstall and reinstall the app to get around this. It's not a good UX

@Mookiies
Copy link
Contributor

Mookiies commented Jul 13, 2023

It's my understanding that setting kSecAttrSynchronizable = true would solve this issue only when a) user hasn't opted out of iCloud keychain backup and b) the restore is to the same device.

keychain items are included in the backup but they are wrapped with a device-specific key. Thus, they can only be restored to the device that originally backed them up, which means that they get lost when you restore the backup to a different device

https://developer.apple.com/forums/thread/93373?answerId=282724022#282724022

@Mookiies
Copy link
Contributor

@abejfehr @jarnove Did either of you try making code changes to remove MMKV files from backup with isExcludedFromBackup?

@jarnove
Copy link

jarnove commented Jul 13, 2023

@Mookiies , not yet, do you have an idea on how to implement it? And I'm afraid that this will not work for existing users, right?

@Mookiies
Copy link
Contributor

I took a stab at implementing it by excluding the entire mmkv directory,/Library/mmkv, in this commit: Mookiies@165cd18

It's a naive implementation, but I think it would be enough to verify that it would work. I've yet to verify as I don't have a working iCloud backup yet.

@Mookiies
Copy link
Contributor

If it does the trick I imagine at the minimum in order to get a mergable PR ready it would need to be made configurable, as excluding shouldn't be the default especially for unencrypted.

To follow Apple's recommendations 100% these improvements could also be made

  • Set isExcludedFromBackup more frequently? (Apple docs recommend setting it each time a file is changed)
  • Prefix the /mmkv directory with the app's bundle identifier

It's also worth noting that isExcludedFromBackup does not provide a guarantee that a given file or directory won't get backed up.

The isExcludedFromBackup resource value exists only to provide guidance to the system about which files and directories it can exclude; it’s not a mechanism to guarantee those items never appear in a backup or on a restored device.1

Given that, it seems an ideal solution would also include a way to somehow recover if some of the encrypted files still managed to get backed up.

Footnotes

  1. https://developer.apple.com/documentation/foundation/optimizing_your_app_s_data_for_icloud_backup#3928527

@diegolmello
Copy link

Hey. I have a few users with a similar issue, but I'm not sure it's the same.
Is a new device/restore really needed or do you have a simpler way to test this?
#269 (comment)

@abejfehr
Copy link
Author

Hey. I have a few users with a similar issue, but I'm not sure it's the same. Is a new device/restore really needed or do you have a simpler way to test this? #269 (comment)

No, unfortunately I was only able to reproduce this issue with 2 iPhones

@dnlsilva
Copy link

dnlsilva commented Aug 9, 2023

The error today happens because when creating the storage, the library uses this property: kSecAttrAccessibleAfterFirstUnlock

And reading apple docs of this property you have this information:
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.

What happens is:

  • When using withEncryption, the app adds the data to the keychain, with a property saying that the data will be backed up to icloud.
  • However, when recovering data from icloud because the keys are encrypted, the library cannot recover the data, and also cannot change them, in this case the user accesses the app, but does not recover the old data, much less manages to update the new ones, since the lib doesn't have a treatment for this scenario, it simply doesn't save the data and that's it.

There are two solutions:

  1. When initializing the storage pass the property saying not to save the data in icloud, like kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
setAccessibleIOS(IOSAccessibleStates.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY)
or
setAccessibleIOS('AccessibleAfterFirstUnlockThisDeviceOnly')
  1. Use a custom key when creating the storage, so the storage will be encrypted with your custom key, so it will be possible to recover the keys correctly after a backup.
encryptWithCustomKey('your-custom-key')

Unfortunately the documentation is not clear enough, and this error is "ignored" by the library, so if you use encryption you must follow one of these two steps.

@Mookiies
Copy link
Contributor

Mookiies commented Aug 9, 2023

@GleidsonDaniel Have you been able to verify that solution 1, setting AccessibleAfterFirstUnlockThisDeviceOnly, solves this problem for a different device iCloud restore?

It's my understanding that this wouldn't solve the issue because regardless of your keychain settings, the encrypted MMKV data in /Library/mmkv is going to get backed up. Therefore, causing the issue as the library cannot decrypt the backup-restored MMKV data as it no longer has the keys.

@dnlsilva
Copy link

dnlsilva commented Aug 9, 2023

@GleidsonDaniel Have you been able to verify that solution 1, setting AccessibleAfterFirstUnlockThisDeviceOnly, solves this problem for a different device iCloud restore?

It's my understanding that this wouldn't solve the issue because regardless of your keychain settings, the encrypted MMKV data in /Library/mmkv is going to get backed up. Therefore, causing the issue as the library cannot decrypt the backup-restored MMKV data as it no longer has the keys.

Why would you do the backup if you are saying not to do the backup? The backup only happens because, as I said, the keys are saved using the property that says to save the backup.

Here's where the library returns the property:

return kSecAttrAccessibleAfterFirstUnlock;

This is where the library gets the value telling how the property should be saved in the keychain:

CFStringRef accessibleVal = _accessibleValue(options);

Nothing happens "automatically", not all data is uploaded to icloud, this only happens because the lib explicitly returns that this will happen, the data must be saved in the icloud backup, if you change this behavior the backup will not happen, simple as that.
Whether the dev of the lib made this aware or not is another question.
However, the backup of the keychain keys only occurs because of this.
Soon just change the way the keys are going to be saved and the problem is solved.

@Mookiies
Copy link
Contributor

Mookiies commented Aug 9, 2023

The contents of the Library directory (with the exception of the Caches subdirectory) are backed up by iTunes and iCloud

https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

When iCloud Backup is in an enabled state, it periodically creates a backup of the user’s device, including your app’s data.

https://developer.apple.com/documentation/foundation/optimizing_your_app_s_data_for_icloud_backup?language=objc

And this library stores encrypted data in /Library/MMKV.

@mjooms
Copy link

mjooms commented Aug 15, 2023

This issue might not be limited to just iOS/iCloud. We received the same reports from some Android users having a new phone and restored their backups.

My understanding is the key is new but the store coming from the backup is encrypted with the old key. For our use case it would be fine to simply remove and recreate the store if that happens. Is that possible?

@brentlecomte
Copy link

Has anyone got an update or figured this out already? As @mjooms mentioned removing the encrypted store would work fine for my casen as well but that doesn't seem like an ideal fix. (And I don't think we can do that anyway 😞). Seems to me that the only viable solution would be to generate our own encryptionKeys like @GleidsonDaniel mentioned before.

@brentlecomte
Copy link

@Mookiies would you mind if I implement the 2 remaining bullet points for your commit? As this might be the only right way to fix this issue. The only thing we would need is a proper way to validate if this fix works.

@dnlsilva
Copy link

dnlsilva commented Sep 7, 2023

@Mookiies would you mind if I implement the 2 remaining bullet points for your commit? As this might be the only right way to fix this issue. The only thing we would need is a proper way to validate if this fix works.

The best thing is to change the library since its owner has no interest in maintaining it any longer, just seeing that a critical bug like this is completely ignored.
It's in our plans to change the lib and everyone reading this thread should do the same.

We were between using our own keys and completely disabling the backup, but we decided to disable it, given that we will soon be changing libraries.
RocketChat/Rocket.Chat.ReactNative#5165

@mjooms
Copy link

mjooms commented Sep 7, 2023

I'm thinking about moving along too.

@GleidsonDaniel which alternative are you considering to use instead?

@brentlecomte
Copy link

Thanks for letting me know, I will propose it to my team as well and we'll see what we will do.

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

9 participants