If your Expo or React Native app stores an auth token, a Supabase refresh token, or any kind of API credential locally, the question of whether SecureStore actually protects it has been quietly debated in Expo discussions since SDK 38. The honest answer is yes, on Android, with a few caveats most builders never hear about. AsyncStorage, on the other hand, is plain text and should never see a credential.
Short answer
On Android, Expo SecureStore stores values inside SharedPreferences but wraps them in AES encryption whose key is held by Android Keystore. Per the Expo SecureStore documentation, this is the same Keystore subsystem documented in the Android Keystore system reference, which on most modern devices is backed by the Trusted Execution Environment. AsyncStorage is unencrypted and would be flagged by an OWASP MASTG scanner under MASTG-TEST-0287 if it holds anything resembling a credential.
What you should know
- SecureStore on Android is AES on top of SharedPreferences. The values land in the same XML directory as any other preference, but the bytes themselves are ciphertext.
- The encryption key lives in Android Keystore. On API 23 and above, SecureStore uses a symmetric AES key. On older Android the library falls back to an asymmetric key pair with hybrid encryption.
- AsyncStorage is unencrypted by design. Values are written to a SQLite database in the app sandbox, readable on a rooted device or via ADB backup.
- Hardware backing depends on the device. Keystore is hardware backed on most phones since 2018 (StrongBox or TEE), software backed on older or low-end Android. Expo SecureStore does not let you require hardware backing.
- Uninstall destroys SecureStore data. When the app is removed, the Keystore entry goes with it, so encrypted values cannot be decrypted on reinstall.
- OWASP MASVS treats this as MSTG-STORAGE-1. Sensitive data must be stored encrypted at rest, with keys held outside the app sandbox.
- The 2048 byte ceiling matters. SecureStore is for credentials, not blobs. Larger payloads should be encrypted with a key from SecureStore and written to Expo FileSystem instead.
How does Expo SecureStore actually encrypt data on Android?
The short version is that SecureStore is a thin wrapper. Behind the API call, the library writes a JSON object into a private SharedPreferences file under your app data directory, with the value field encrypted using AES and the key held in Android Keystore. The encryption scheme used is stored in the JSON record itself, so the library knows how to decrypt older entries after an SDK upgrade.
On devices running API 23 (Android 6.0) or higher, Keystore supports symmetric AES keys directly, and SecureStore uses an AES/GCM/NoPadding scheme with the key never leaving Keystore. On older Android, the platform Keystore only supported asymmetric keys, so SecureStore generates an RSA key pair and uses hybrid encryption: a per-value AES key encrypted with the RSA public key, then both stored together. The source for this lives in the SecureStoreModule.java file in the expo/expo monorepo.
The limit on this is that SecureStore does not pin you to hardware backing. If the device's Keystore is software-only, the protection is real against casual file access but weak against a determined attacker with root. The Android Keystore documentation describes how to check KeyInfo.isInsideSecureHardware(), but SecureStore does not surface that flag.
What is the actual security difference between SecureStore and AsyncStorage?
AsyncStorage is a key-value store backed by SQLite in the app sandbox. The bytes you write are the bytes on disk. On a non-rooted device, the Android sandbox prevents other apps from reading them, but anyone with adb backup, a rooted phone, a forensic image, or a malicious app exploiting a separate vulnerability can read AsyncStorage as plain text.
SecureStore changes the threat model. The value on disk is ciphertext. To decrypt, an attacker needs either the running app process (a runtime hook with Frida, for example) or root plus a software-only Keystore. Against the common attacks that scanning tools test for, file inspection, ADB backup extraction, and MASTG storage tests, SecureStore moves you out of scope.
| Property | AsyncStorage | Expo SecureStore (Android) |
|---|---|---|
| Encryption at rest | No | AES with key in Android Keystore |
| Hardware backing | None | TEE or StrongBox on most modern devices |
| Survives adb backup? | Yes, readable | Ciphertext, not decryptable off-device |
| Size limit | 6 MB default on Android | Approximately 2048 bytes per value |
| Auth tokens | Fails MSTG-STORAGE-1 | Passes MSTG-STORAGE-1 |
| Cross-app sandbox isolation | Yes (Android sandbox) | Yes, plus encryption |
| Survives uninstall | No | No, Keystore entry deleted |
| Performance | Fast | Slightly slower per call |
For an Expo app shipping to either store, the rule is simple: anything that authenticates the user, identifies a session, or signs requests should be in SecureStore. Anything else (preferences, cached UI state, draft form values) can live in AsyncStorage or MMKV.
When does SecureStore stop being enough?
Three situations break the SecureStore guarantee. The first is a rooted device with a software-only Keystore. On low-end or older Android, Keystore runs in normal-world memory, and a root-level attacker can read the key material. The OWASP MASTG storage chapter treats this case explicitly and recommends additional defenses for high-sensitivity apps, including remote attestation and key import via secure enrollment.
The second is runtime extraction. Even on hardware-backed Keystore, if your app process can decrypt a value, an attacker with a Frida hook into the same process can decrypt it too. SecureStore is at-rest protection, not in-memory protection. Fintech and identity apps usually layer on a hooking detection library and certificate pinning to raise that bar.
The third is what gets called the bigger blob problem. SecureStore is not designed for storing entire encrypted databases or large signed payloads. The documented per-value limit is around 2048 bytes, and even below that the synchronous Keystore call is slower than a plain disk write. The correct pattern, used by apps storing offline data or cached encrypted documents, is to store a single per-app data encryption key in SecureStore and use it to encrypt larger files written to Expo FileSystem.
What does an OWASP MASTG scan actually flag here?
The specific test is MASTG-TEST-0287, which checks whether sensitive data sits unencrypted in SharedPreferences or comparable Android key-value stores. A scanner running this test will examine the decompiled APK, parse the SharedPreferences XML written during a runtime trace, and flag any string that matches credential patterns: JWT structure, OAuth token prefixes, Supabase or Firebase API keys, basic-auth pairs.
What a static APK scan cannot do reliably is tell SecureStore apart from a homegrown encryption layer based solely on the XML file. Both produce blobs of unreadable bytes. The MASTG approach is to combine static signal (decompiled classes referencing expo.modules.securestore versus AsyncStorage) with runtime behavior (a Frida script that calls each API and watches what hits disk). Scanners aligned with MASVS-STORAGE-1 typically run both.
For builders who want an automated read of their compiled APK or AAB before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with OWASP MASVS for no-code and vibe-coded apps. Catching a credential in AsyncStorage before Google Play does, or before a bug bounty hunter does, is the case the scan exists for.
How should you migrate tokens from AsyncStorage to SecureStore in an existing app?
Most teams discover the gap mid-flight, usually after a security questionnaire or a pentest report. The cleanest migration path has three phases. First, write a one-time migration step on app start that reads the suspect AsyncStorage keys, writes them to SecureStore with the same logical names, and then calls AsyncStorage.removeItem for each one. Wrap the whole step in a try-catch and log to a remote sink so you can confirm the migration ran for real users.
Second, change every read site to call SecureStore first. If your codebase has a single getToken helper, this is a one-file change. If reads are scattered, a search for AsyncStorage.getItem plus a careful review of which keys are credentials versus preferences gives you the list. Anything that smells like a token, refresh token, signed key, password, or session identifier should move.
Third, plan for the rollback. Because uninstalling the app destroys SecureStore data on Android, users who clear app data or reinstall need a way back. The standard pattern is to treat missing SecureStore values as an expired session and route the user back through your login flow, which generates a new token, which lands in SecureStore.
What to watch out for
The first trap is assuming SecureStore is hardware backed everywhere. On 2026 mid-tier and entry-level Android, software-only Keystore still ships. If your app has a regulatory requirement for hardware-backed key storage (some PSD2 or HIPAA-adjacent contexts), you need an explicit attestation check, not a hope.
The second trap is mixing SecureStore with Auto Backup. By default, Android's Auto Backup feature copies SharedPreferences to Google's servers and restores them on a new device. Restored SecureStore values are useless because the Keystore entry that decrypts them did not come with the backup, and worse, they can clutter the file and produce silent decryption errors. Recent Expo versions exclude SecureStore from Auto Backup automatically, but if you have a custom backup_rules.xml, verify the exclusion is present.
The third trap is forgetting that SecureStore is at-rest only. A token in SecureStore that your app reads into a JavaScript variable and then logs to a third-party analytics SDK is no longer protected. The five-gate pre-submission framework (Permissions, SDKs, Storage, Network, Build) treats the SDK gate as the place to catch that leak, not the Storage gate.
Key takeaways
- Expo SecureStore on Android is real encryption, not security theater: AES with keys held in Android Keystore, hardware backed on most devices made since 2018.
- AsyncStorage is for preferences and cached UI state, not credentials. Any token in AsyncStorage fails OWASP MASTG test MASTG-TEST-0287 and counts as an MSTG-STORAGE-1 violation.
- SecureStore is per-value and small, around 2048 bytes. For larger encrypted artifacts, store the key in SecureStore and the ciphertext in Expo FileSystem.
- Some teams outsource the pre-submission audit of their compiled APK or IPA, including which values actually hit disk in cleartext, to platforms like PTKD.com (https://ptkd.com) so that the storage layer Google Play and App Review are about to see is already understood.


