Mobile
Storing sensitive data securely in a mobile app: Keystore and Keychain
Keeping tokens and personal data in SharedPreferences or UserDefaults is rated high risk in a security review. How to use Android Keystore and iOS Keychain properly.
· 6 min read

One of the Android findings in the review of our own app was short and precise: "sensitive data stored unencrypted in internal storage". Severity: high. The app kept its access token in an ordinary preferences file. On a normal phone no other app can read that file, but on a rooted device or through a backup it is visible as plain text.
In this article we look at what to store in a mobile app, where, and how.
The common mistake: "internal storage is private anyway"
Many developers reason like this: the app folder is inside the sandbox, other apps cannot reach it, so encryption is unnecessary. That only holds under ideal conditions. In practice, data in internal storage can leak through:
- Root or jailbreak — a user with the highest privileges can read any app's folder.
- Backups — if
allowBackupis enabled on Android, app data ends up in the backup and can be restored on another device. - Physical access and forensics — if the device falls into the wrong hands, data stored in the clear is easier to extract.
- Another weakness in the app — for example a WebView or content provider with incorrect file access.
That is why the STORAGE group of MASVS requires sensitive data to be kept in the platform's protected storage mechanisms. In the OWASP Mobile Top 10 2024 this is category M9 (Insecure Data Storage).
What counts as sensitive
- Access and refresh tokens, session identifiers.
- API keys and encryption keys.
- Personal data: national ID numbers, passport details, phone numbers, financial figures.
- Data related to biometrics or PIN codes.
The user's password should not be stored on the device at all. Store a server-issued token with a limited lifetime instead.
Platform tools
Android Keystore
Android Keystore is a system for storing cryptographic keys. Its main advantage is that the key never enters the app's memory. On many devices the key is kept in hardware (TEE or StrongBox). The app asks for data to be encrypted with the key but never sees the key itself. The encrypted data can sit in an ordinary file — without the key it is useless.
iOS Keychain
The iOS Keychain is a system-level encrypted store for passwords, tokens and keys. Each item has an access condition. For example:
WhenUnlockedThisDeviceOnly— the data can be read only while the device is unlocked and does not move to another device via backup.AfterFirstUnlockThisDeviceOnly— the data is available after the first unlock following a restart. Handy for background requests.
The ThisDeviceOnly suffix matters: it prevents the item from migrating to another device through an iCloud backup.
In practice with Flutter: flutter_secure_storage
In our Flutter app we used the flutter_secure_storage package. On Android it uses Keystore-backed encryption, on iOS the Keychain, with one codebase for both platforms:
const storage = FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
await storage.write(key: 'access_token', value: token);
final token = await storage.read(key: 'access_token');
Alongside this, the following changes were made:
- All tokens were moved out of
SharedPreferences.SharedPreferencesis now used only for non-sensitive settings: language, theme, whether onboarding was seen. - Backups were disabled. In
AndroidManifest.xml:
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules">
data_extraction_rules controls cloud backup and device-to-device transfer on Android 12 and above.
- Everything is cleared on logout. When the user logs out, the secure storage entries are deleted, and the WebView cache, cookies and stored data are wiped too. Otherwise the next user could find traces of the previous session.
Secure storage protects data on disk. It does not protect what the app holds in memory at runtime. That is why it has to work together with RASP and root detection.
Subtle points to watch
- The iOS Keychain can survive app deletion. If a user deletes and reinstalls the app, an old token may still be found. Fix: check a simple flag on first launch and, if it is missing, delete old Keychain entries.
- No sensitive data in logs. In release builds, tokens, request bodies and personal data must not be logged.
- Cache and temporary files. Downloaded documents, PDF reports and images may be sensitive too. Delete them after use.
- Screenshots. On sensitive screens you can use
FLAG_SECUREon Android and hide content on iOS when the app goes to the background.
Test it
- On a rooted test device, open the app folder (
/data/data/<package>/) and look for plain-text tokens or personal data inshared_prefs, databases and files. - Check whether data leaks through
adb backupor device transfer. - Make sure no session traces remain in the app folder or WebView data after logout.
- On iOS, delete and reinstall the app and check that it does not log in automatically.
Conclusion
Storing sensitive data securely is not hard: the platform provides ready-made tools for it. The real work is finding everywhere data is stored, moving it into Keystore or Keychain, and configuring backups and logout cleanup correctly.
If you want to check how your app stores sensitive data, request a security scan. We will find the gaps and help you close them before the review.


