Platform secure storage (iOS Keychain, Android Keystore) is backed by hardware security modules on modern devices — credentials stored there cannot be extracted even with root access because the decryption key never leaves the secure enclave. CWE-312 (Cleartext Storage of Sensitive Information) and CWE-522 (Insufficiently Protected Credentials) both apply when an app stores tokens outside these APIs. OWASP A02 and NIST SC-28 require encryption at rest for all authentication credentials. Apps without Keychain/Keystore usage have no encrypted credential store by definition — every stored secret is as secure as the device filesystem, which is not secure at all on rooted or cloned devices. This is the class of vulnerability that drives credential-stuffing campaigns after device theft.
Critical because the total absence of Keychain or Keystore usage means no credential is encrypted at rest, making stored tokens recoverable via filesystem access on any rooted or backed-up device — a direct CWE-312 and OWASP A02 violation.
Ensure every sensitive value flows through expo-secure-store or native Keychain/Keystore. Audit all storage calls and confirm zero sensitive values in AsyncStorage, MMKV, or localStorage.
// Expo (wraps Keychain on iOS, EncryptedSharedPreferences on Android)
import * as SecureStore from 'expo-secure-store'
async function storeSession(token: string, userId: string) {
await SecureStore.setItemAsync('session_token', token)
await SecureStore.setItemAsync('user_id', userId)
}
async function clearSession() {
await SecureStore.deleteItemAsync('session_token')
await SecureStore.deleteItemAsync('user_id')
}
For bare React Native, use react-native-keychain: Keychain.setGenericPassword(username, password). For large encrypted blobs, use react-native-mmkv with an encryption key stored in Keychain — not a hardcoded key.
ID: mobile-permissions-privacy.privacy-compliance.keychain-keystore
Severity: critical
What to look for: List all sensitive data items identified (auth tokens, encryption keys, passwords, PII). For each, check whether it is stored via Keychain (iOS), Keystore (Android), or expo-secure-store. Count secure storage calls vs. insecure storage calls for sensitive data.
Pass criteria: At least 100% of sensitive data (auth tokens, keys, passwords, PII) is stored in Keychain (iOS) or Keystore (Android), or via expo-secure-store which abstracts these. Zero sensitive items use insecure storage.
Fail criteria: App has no secure storage mechanism for sensitive data, OR has access to Keychain/Keystore but stores any sensitive data insecurely elsewhere.
Skip (N/A) when: App stores no sensitive data (no auth tokens, passwords, or PII found in storage code).
Detail on fail: Quote the actual storage call. Example: "Auth token stored in AsyncStorage instead of Keychain" or "No Keychain/Keystore usage found; all data stored in AsyncStorage"
Remediation: Use secure storage for all sensitive data. For Expo apps, use expo-secure-store. For bare React Native, use native Keychain/Keystore APIs:
// Expo approach
import * as SecureStore from 'expo-secure-store'
async function storeAuthToken(token) {
await SecureStore.setItemAsync('authToken', token)
}
// Bare React Native (iOS)
import Keychain from 'react-native-keychain'
async function storeCredentials(username, password) {
await Keychain.setGenericPassword(username, password)
}