flutter_secure_storage
flutter_secure_storage copied to clipboard
Saved data on iOS is not secure
Recently, one of the apps I've been working on underwent a penetration testing review. The pentest discovered (with demonstrated proof) that local storage data on iOS devices can be accessed in plaintext. Since our app uses flutter_secure_storage, this finding was surprising to me.
Initially, I suspected that we might have misconfigured the plugin. Upon reviewing our implementation, I confirmed that we are using the most restrictive Keychain option provided by the plugin:
final _storage = const FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.unlocked_this_device,
));
Upon further investigation, I found this page in Apple's documentation, stating that Apple uses two different AES-256-GCM keys for encryption. However, it appears that data encryption occurs only just before writing to disk and is immediately decrypted when accessed.
Thus, while data on the physical disk is indeed encrypted, any jailbroken iOS device can easily retrieve all keychain-stored values in plaintext once the device is unlocked. Notably, the penetration testers neither had access to our source code nor knew the keys used for storing data in the keychain; yet, they were still able to show us all plaintext data.
Their recommendation was to enhance security by generating an asymmetric key stored in the Secure Enclave and using it alongside Apple's built-in encryption mechanisms to achieve true security for keychain-stored data.
Given that flutter_secure_storage already utilizes a similar approach for Android, I'm curious if a similar solution could be implemented for iOS as well.
Thanks!
@zigapovhe thanks for starting the discussion, I arrived here after also hearing about pen tester concerns.
I think there are another couple of possible options too that we should explore
One note: the most restrictive option is KeychainAccessibility.passcode. From reading the details (https://developer.apple.com/documentation/security/restricting-keychain-item-accessibility), the vulnerability might have been that A device without a passcode is considered to always be unlocked. Do you know if the pen test results would have been the same with passcode option used? The plugin here does set kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly for the passcode option.
Additionally, iOS offers a Demand User Presence mode. It appears already to be available in the latest version of flutter_secure_storage (though I haven't tried it yet) as I see a reference to a userPresence accessControlFlag, so maybe this would be secure enough to pass a pen-test too?
Hi @jeffsheets! Thank you for your answer! You are right that passcode is the most restrictive option, although I don't believe it would make a difference in the pentest. To my knowledge, the pentesters are using a jailbroken device, which allows them to see the stored values in plaintext once they are unlocked by iOS. If we compare the unlocked_this_device setting with the passcode setting, both restrict access to values when the device is locked, but once the user unlocks the device—whether by passcode, swipe, Face ID, fingerprint, etc.—the values become accessible in both cases - and this is the moment when pentesters can access the values in plaintext.
It looks like the userPresence flag was added in the latest beta version (v10.0.0-beta.4) of the package, so it might not yet be stable enough for production use. However, as discussed here, it could help with the pentest review.
The main concern with requiring user presence is whether this would force users to authenticate every time local storage is accessed. This could result in a really poor UX—or even make the app unusable—unless the local storage access code is rewritten to fetch everything at once to minimize the number of required authentications. But then again, would that actually make things worse, as plaintext values would be sitting in RAM even if not immediately needed?
Probably the easiest solution for now would be to prevent the app from running on jailbroken devices. The best long-term solution would be to add AES encryption to the flutter_secure_storage package, so that values are encrypted by default.
⚠️ This issue has been marked as stale because it has been open for 60 days with no activity.
If this issue is still relevant, please comment to keep it active. Otherwise, it will be closed in 60 days.
The issue is still relevant :) @juliansteenbakker
I faced the exact same issue during our security audit! Our pen testers were able to extract all keychain data in plaintext on jailbroken devices, even with the most restrictive KeychainAccessibility.unlocked_this_device setting.
We solved this by implementing Secure Enclave key wrapping. We use ECIES to wrap our encryption keys with a Secure Enclave key before storing them in the keychain. This way, the original keys never leave the Secure Enclave hardware, making them impossible to extract even on jailbroken devices. Only the wrapped keys are stored in the keychain, providing true hardware-based security.
@ahmeddhus Thank you for the detailed answer, and congratulations on getting this working. Storing encryption keys in the Secure Enclave indeed looks like the only right solution for this. It would be great to have this implemented directly in flutter_secure_storage, since this plugin was created to solve exactly this kind of security problem out of the box—so we don’t all have to re-implement it individually.
If you’re open to it, would you be willing to explore adding this to the plugin? I’m not deeply experienced in iOS native development, but I’m happy to help however I can—especially with hands-on testing and API/implementation discussions. It would be great if @jeffsheets could also weigh in or collaborate given his experience.
@zigapovhe I've since moved to a different project with different requirements, so I'm not completely certain... The Secure Enclave approach does sound promising though I'm not sure on the complexities. Does it add a lot more developer setup? Perhaps userPresence would be enough but we'd have to have someone try it out to see what the user experience is like.
I would like to do a little R&D on this
@ahmeddhus Thank you for the detailed answer, and congratulations on getting this working. Storing encryption keys in the Secure Enclave indeed looks like the only right solution for this. It would be great to have this implemented directly in flutter_secure_storage, since this plugin was created to solve exactly this kind of security problem out of the box—so we don’t all have to re-implement it individually.
If you’re open to it, would you be willing to explore adding this to the plugin? I’m not deeply experienced in iOS native development, but I’m happy to help however I can—especially with hands-on testing and API/implementation discussions. It would be great if @jeffsheets could also weigh in or collaborate given his experience.
@zigapovhe Thanks for your feedback! I’ve just submitted a PR implementing Secure Enclave support. Let’s see where this leads us.
@ahmeddhus Great work on the Secure Enclave support. As far is I can tell it looks solid!
Before merging, we’ll also need an iOS migration so existing data can be encrypted. That's what I would suggest: • add a one-time “hasMigratedToSecureEnclave” flag • read current keychain entries (readAll()), decrypt with the old path, re-encrypt with the SE-backed key • preserve accessibility, access group, and synchronizable settings • mirror Android’s counters/delete-on-failure behavior and fall back when SE isn’t available
The Android implementation in FlutterSecureStorage.java:152-205 provides a good template for the overall flow and error handling patterns. Is there anything else that needs to be added/changed before merging this?