A plaintext SQLite database file is readable by anyone with filesystem access to the device — a physical attacker, a forensic tool, or malware with elevated privileges. CWE-311 (Missing Encryption of Sensitive Data) and CWE-922 (Insecure Storage of Sensitive Information) both apply. OWASP Mobile Top 10 M9 and GDPR Art. 32 require encryption for stored personal data. Unlike AsyncStorage, a SQLite file is a structured, easily-parsed binary that can be opened in any standard tool. A single extracted database file exposes the entire local data model with no further attack required.
High because an unencrypted database file can be extracted from a compromised device and parsed trivially, exposing all stored personal data with no additional attack.
Enable database-level encryption using SQLCipher, MMKV's built-in encryption, or Realm's encryption configuration. Derive the encryption key from platform-secure storage — never hardcode it.
// MMKV with encryption (npm install react-native-mmkv)
import { MMKVLoader } from 'react-native-mmkv';
const storage = new MMKVLoader()
.setEncryption(true)
.initialize();
// Realm with encryption (npm install realm)
import Realm from 'realm';
import * as SecureStore from 'expo-secure-store';
async function openEncryptedRealm() {
const keyHex = await SecureStore.getItemAsync('realm_key');
const encryptionKey = hexToUint8Array(keyHex!);
return Realm.open({
schema: [YourSchema],
encryptionKey, // 64-byte Uint8Array
});
}
Store the database encryption key in expo-secure-store or Keychain — not in source code and not in AsyncStorage. Rotate the key on first launch if none exists.
ID: mobile-offline-storage.storage-security.database-encryption
Severity: high
What to look for: If the app uses SQLite or another database, check whether database-level encryption is enabled. Look for SQLCipher integration, MMKV encryption settings, or Realm encryption configuration.
Pass criteria: Count all local database files or database initialization calls. At least 1 database encryption mechanism must be active (SQLCipher, MMKV encryption, or Realm encryption). No more than 0 database files should be readable without the encryption key.
Fail criteria: Database is stored in plain text with no encryption. Could be accessed if device is compromised.
Skip (N/A) when: App does not use a local database (only AsyncStorage for non-sensitive data is acceptable without encryption).
Detail on fail: "SQLite database stored in plain text without encryption. Database file could be extracted and examined if device is compromised."
Remediation: Use encrypted SQLite (SQLCipher) or encrypted alternatives:
// For Expo, use expo-sqlite with encryption:
import * as SQLite from 'expo-sqlite';
export async function initEncryptedDB() {
// Expo SQLite uses SQLCipher under the hood on native
const db = SQLite.openDatabase('encrypted.db');
return new Promise((resolve) => {
db.transaction(tx => {
// For Expo, encryption is automatic on iOS/Android
// For advanced encryption, consider native modules
tx.executeSql('CREATE TABLE IF NOT EXISTS secure_data (id INTEGER PRIMARY KEY, data TEXT);');
resolve(db);
});
});
}
For react-native-mmkv (has built-in encryption):
// Install: npm install react-native-mmkv
import { MMKVLoader } from 'react-native-mmkv';
const storage = new MMKVLoader()
.setEncryption(true)
.initialize();
For Realm (has built-in encryption):
// Install: npm install realm
import Realm from 'realm';
const config: Realm.Configuration = {
schema: [YourSchema],
encryptionKey: new Uint8Array(64), // Use a proper key from secure storage
};
const realm = await Realm.open(config);