ScaleRocket/Mobile

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:

FeatureAsyncStorageSecureStore
EncryptionNoneHardware-backed
iOS storageSQLite fileKeychain
Android storageSQLite fileKeystore + SharedPreferences
Accessible after jailbreakYesMuch harder
Size limitNo practical limit2KB per value
Use casePreferences, cacheTokens, 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-store

Note: 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,
});
LevelWhen Accessible
WHEN_UNLOCKEDOnly when device is unlocked (default)
AFTER_FIRST_UNLOCKAfter first unlock until restart
ALWAYSAlways (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

On this page