Secure Storage
Using expo-secure-store for tokens, why AsyncStorage is unsafe for sensitive data, and platform storage mechanisms.
Secure Storage
Sensitive data like authentication tokens must be stored in encrypted, hardware-backed storage. ScaleRocket Mobile uses expo-secure-store which leverages the iOS Keychain and Android Keystore.
Why Not AsyncStorage?
AsyncStorage stores data as plain text in an SQLite database on the device file system:
| Feature | AsyncStorage | SecureStore |
|---|---|---|
| Encryption | None | Hardware-backed |
| iOS storage | SQLite file | Keychain |
| Android storage | SQLite file | Keystore + SharedPreferences |
| Accessible after jailbreak | Yes | Much harder |
| Size limit | No practical limit | 2KB per value |
| Use case | Preferences, cache | Tokens, secrets |
Rule: If the data could be used to impersonate the user or access their account, use SecureStore.
Setup
expo-secure-store is pre-installed in ScaleRocket Mobile. For new projects:
npx expo install expo-secure-storeNote: SecureStore requires a development build. It does not work in Expo Go.
Basic Usage
import * as SecureStore from "expo-secure-store";
// Store a value
await SecureStore.setItemAsync("auth-token", "eyJhbGciOiJIUzI1NiIs...");
// Retrieve a value
const token = await SecureStore.getItemAsync("auth-token");
// Delete a value
await SecureStore.deleteItemAsync("auth-token");Platform Behavior
iOS — Keychain
On iOS, SecureStore uses the Keychain Services API:
- Data is encrypted with the device's Secure Enclave
- Protected by the user's passcode/biometrics
- Survives app reinstalls (unless explicitly deleted)
- Can be backed up to iCloud Keychain
Configure accessibility level:
await SecureStore.setItemAsync("auth-token", token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED,
});| Level | When Accessible |
|---|---|
WHEN_UNLOCKED | Only when device is unlocked (default) |
AFTER_FIRST_UNLOCK | After first unlock until restart |
ALWAYS | Always (least secure) |
Android — Keystore
On Android, SecureStore uses:
- Android Keystore system for key management
- AES encryption for the stored values
- Keys are bound to the device hardware
- Data does not survive app uninstall
Auth Token Storage Pattern
ScaleRocket Mobile uses SecureStore as the Supabase session storage adapter:
import * as SecureStore from "expo-secure-store";
import { createClient } from "@supabase/supabase-js";
const SecureStoreAdapter = {
getItem: (key: string) => SecureStore.getItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
};
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: SecureStoreAdapter,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});Clearing Sensitive Data
Always clear all secure data on sign out:
async function signOut() {
await SecureStore.deleteItemAsync("auth-token");
await SecureStore.deleteItemAsync("refresh-token");
await SecureStore.deleteItemAsync("user-id");
// Then navigate to login screen
}Size Limitations
SecureStore has a 2KB limit per value. For larger data:
- Split into multiple keys
- Store a reference (ID) in SecureStore and the data in AsyncStorage (if non-sensitive)
- Compress before storing
// If a JWT is too large (rare but possible)
const parts = splitString(largeToken, 2000);
for (let i = 0; i < parts.length; i++) {
await SecureStore.setItemAsync(`token-part-${i}`, parts[i]);
}Testing
- iOS Simulator: SecureStore works normally in the simulator
- Android Emulator: SecureStore works but with software-backed keys
- Expo Go: SecureStore is not available — use a development build