Security

    Does Expo SecureStore actually encrypt your data on Android?

    An Android developer inspecting an Expo React Native app, with Android Studio open on SharedPreferences XML files and a terminal showing Keystore output during a pre-submission security review

    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.

    PropertyAsyncStorageExpo SecureStore (Android)
    Encryption at restNoAES with key in Android Keystore
    Hardware backingNoneTEE or StrongBox on most modern devices
    Survives adb backup?Yes, readableCiphertext, not decryptable off-device
    Size limit6 MB default on AndroidApproximately 2048 bytes per value
    Auth tokensFails MSTG-STORAGE-1Passes MSTG-STORAGE-1
    Cross-app sandbox isolationYes (Android sandbox)Yes, plus encryption
    Survives uninstallNoNo, Keystore entry deleted
    PerformanceFastSlightly 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.
    • #expo
    • #react-native
    • #android
    • #securestore
    • #asyncstorage
    • #keystore
    • #owasp-masvs

    Frequently asked questions

    Is Expo SecureStore actually hardware encrypted on Android?
    The encryption key sits inside Android Keystore, which on most devices made after 2018 is backed by the Trusted Execution Environment or a StrongBox secure element. On older or low-end devices the Keystore may be software-only, in which case the protection drops to the level of any other process running with system keys. Expo SecureStore does not pin you to hardware backing, so on a rooted device with software Keystore an attacker can extract values.
    What happens to SecureStore data when the user uninstalls my app?
    It is gone. On Android, uninstalling deletes the SharedPreferences file and the Keystore entry the app created, so the encrypted values can no longer be decrypted. This differs from iOS, where Keychain items can survive uninstall and reinstall under the same bundle identifier. Per the Expo documentation, you should not rely on SecureStore for cross-install persistence on Android.
    Can I just use AsyncStorage and encrypt the values myself?
    You can, but you then need to store the encryption key somewhere, and the only safe somewhere on Android is Android Keystore, which is exactly what SecureStore already does. Rolling your own usually ends with the key bundled in JavaScript or hard-coded in app source, which fails OWASP MASTG test MASTG-TEST-0287 and is trivially recoverable from the APK. SecureStore is the shorter path to the same outcome.
    Does SecureStore work for storing large blobs or files?
    No. The Expo SecureStore documentation flags that values above roughly 2048 bytes can be rejected by the underlying platform, and even when accepted, performance degrades quickly. For larger encrypted blobs the right pattern is encrypting the file with a key stored in SecureStore, then writing the ciphertext to Expo FileSystem. Treat SecureStore as a credential vault, not a database.
    Will the App Store or Google Play reject me for using AsyncStorage with tokens?
    Neither store statically scans your code for AsyncStorage usage. What happens in practice is a Data Safety mismatch on Google Play if you declared encryption at rest but ship plain SharedPreferences, or a Guideline 5.1.1 escalation on iOS if a reviewer notices auth tokens in cleartext. The bigger risk is silent: a pentest, a bug bounty report, or a third-party security scanner flagging MSTG-STORAGE-1 weeks after launch.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free