Sensitive data encrypted at rest
Why it matters
Storing auth tokens, API keys, or PII in AsyncStorage writes plaintext to the device filesystem with no access controls beyond the app sandbox. On a rooted or jailbroken device — a realistic threat model, not an edge case — any process can read AsyncStorage without authentication. CWE-311 (Missing Encryption of Sensitive Data) and CWE-312 (Cleartext Storage of Sensitive Information) directly describe this defect. OWASP Mobile Top 10 M9 and GDPR Art. 32 both mandate encryption for personal data at rest. A compromised device that exposes a plaintext refresh token enables full account takeover; the attacker does not need the user's password.
Severity rationale
Critical because plaintext token storage in AsyncStorage exposes credentials to any process on a rooted device, enabling account takeover without requiring the user's password.
Remediation
Move all tokens, API keys, and PII out of AsyncStorage and into platform-secure storage (iOS Keychain, Android Keystore) via expo-secure-store or react-native-keychain. Never call AsyncStorage.setItem with a token or secret.
// npm install expo-secure-store
import * as SecureStore from 'expo-secure-store';
export const Auth = {
async saveTokens(access: string, refresh: string) {
await Promise.all([
SecureStore.setItemAsync('access_token', access),
SecureStore.setItemAsync('refresh_token', refresh),
]);
},
async getAccessToken() {
return SecureStore.getItemAsync('access_token');
},
async clearTokens() {
await Promise.all([
SecureStore.deleteItemAsync('access_token'),
SecureStore.deleteItemAsync('refresh_token'),
]);
},
};
Search your codebase for AsyncStorage.setItem calls and audit every stored key against your sensitive-data inventory. Any key storing a token, credential, or PII field must migrate to SecureStore.
Detection
-
ID:
encryption-at-rest -
Severity:
critical -
What to look for: Examine what data is stored locally and how. Look for encryption of sensitive data (auth tokens, passwords, PII). Check for use of encrypted storage libraries like expo-secure-store or encrypted SQLite/MMKV.
-
Pass criteria: Enumerate all sensitive data types stored locally (tokens, API keys, PII). Sensitive data (tokens, API keys, PII) must be encrypted at rest using a platform-provided secure storage or encryption library. AsyncStorage (if used) must only contain non-sensitive data. At least 1 secure storage library (expo-secure-store, Keychain, Keystore) must be used for credentials. Report even on pass: "Found X sensitive data items, all stored via secure storage. Y non-sensitive items in AsyncStorage."
-
Fail criteria: Sensitive data is stored in plain text in AsyncStorage, shared preferences, or local files without encryption.
-
Skip (N/A) when: Never — encryption is essential for security.
-
Cross-reference: The Security Headers audit (
security-headers) checks server-side security that complements client-side encryption at rest. -
Detail on fail:
"Authentication tokens stored in plain text in AsyncStorage. Rooted devices or app backups could expose user credentials." -
Remediation: Use secure storage for sensitive data:
// Install: npm install expo-secure-store import * as SecureStore from 'expo-secure-store'; export async function storeAuthToken(token: string) { try { await SecureStore.setItemAsync('authToken', token); } catch (error) { console.error('Failed to store token:', error); } } export async function getAuthToken(): Promise<string | null> { try { return await SecureStore.getItemAsync('authToken'); } catch (error) { console.error('Failed to retrieve token:', error); return null; } } export async function deleteAuthToken() { try { await SecureStore.deleteItemAsync('authToken'); } catch (error) { console.error('Failed to delete token:', error); } }For React Native (non-Expo), use react-native-keychain:
// Install: npm install react-native-keychain import * as Keychain from 'react-native-keychain'; export async function storeAuthToken(token: string) { await Keychain.setGenericPassword('authToken', token); } export async function getAuthToken(): Promise<string | null> { const credentials = await Keychain.getGenericPassword(); return credentials ? credentials.password : null; }Never store sensitive data in AsyncStorage:
// ❌ DON'T DO THIS await AsyncStorage.setItem('authToken', token); // Insecure! // ✅ DO THIS INSTEAD await SecureStore.setItemAsync('authToken', token); // Secure
External references
- cwe · CWE-312 — Cleartext Storage of Sensitive Information
- cwe · CWE-311 — Missing Encryption of Sensitive Data
- owasp:2021 · A02 — Cryptographic Failures
- gdpr · Art. 32 — Security of processing
- nist:rev5 · SC-28 — Protection of Information at Rest
Taxons
History
- 2026-04-18·v1.0.0·Initial import from mobile-offline-storage·automated