SQLCipher and secure databases for sensitive data in React Native apps
An unencrypted SQLite database on a device can be read in plain text with a file manager or rooted access. SQLCipher closes that gap by transparently encrypting the entire database file without changing the familiar SQL interface. This article covers how it works, key management, and the realistic performance cost.
Table of Contents
- 1. Why an unencrypted SQLite database is a risk
- 2. How SQLCipher technically works
- 3. Integrating into React Native via react-native-sqlcipher-storage
- 4. Key management via Keychain and Android Keystore
- 5. Assessing performance overhead realistically
- 6. Migrating existing unencrypted databases
- 7. Alternative secure database approaches compared
- 8. Common implementation mistakes with encrypted databases
- 9. Checklist for shipping SQLCipher to production
- 10. Summary
- 11. FAQ
1. Why an unencrypted SQLite database is a risk
Standard SQLite stores its database file as plain-text binary data in the app's file system. On a rooted Android device or a jailbroken iOS device with file system access, this file can be copied directly and opened with common SQLite tools without any additional hurdle, exposing every piece of stored content.
For app settings or non-critical cache data that is usually not a problem. But once health data, payment information, private messages, or access tokens get cached locally, protection from the app sandbox alone is no longer enough, especially for lost or stolen devices and for backups that sit outside the app's control.
2. How SQLCipher technically works
SQLCipher is an extension of SQLite that transparently encrypts every database page with AES-256 in CBC mode before it hits disk, and decrypts it again on read. Nothing changes for application code: every familiar SQL statement works identically, only the underlying file is worthless binary noise without the matching key.
On top of pure encryption, SQLCipher uses per-page HMAC checksums to detect unnoticed tampering with the database file. If a page is modified without knowledge of the key, the integrity check fails the next time the database is opened, instead of silently accepting the tampered data.
3. Integrating into React Native via react-native-sqlcipher-storage
In React Native projects, SQLCipher usually gets used through a library like react-native-sqlcipher-storage or a patched op-sqlite variant that provides native SQLCipher bindings for iOS and Android. The API stays nearly identical to the unencrypted SQLite variant, only adding the encryption key when the connection is opened.
It's essential never to store the key as a literal in JavaScript code or a configuration file, since it could otherwise be extracted just by decompiling the bundle. Instead, the key gets loaded from secure native storage on every app start and only ever passed to the database connection at runtime.
import { openDatabase } from 'react-native-sqlcipher-storage';
import * as Keychain from 'react-native-keychain';
async function openEncryptedDatabase() {
const credentials = await Keychain.getGenericPassword({ service: 'db-key' });
if (!credentials) {
throw new Error('No encryption key found in secure storage');
}
return openDatabase({
name: 'secure.db',
key: credentials.password,
location: 'default',
});
}
4. Key management via Keychain and Android Keystore
The encryption key itself has to live outside the database in hardware-backed secure storage for the encryption to make sense at all. On iOS that's the Keychain, on Android the Android Keystore; both isolate the key from regular app storage and additionally protect it via device biometrics or the screen lock code.
On first launch, the app should ideally generate a cryptographically random key, for example through a secure random number generator library, and store it directly in the Keychain or Keystore instead of managing it itself. A user-chosen password as the sole key source is risky, because a weak password can effectively render the entire encryption worthless.
5. Assessing performance overhead realistically
The raw AES-256 encryption and decryption work is barely noticeable on current mobile hardware thanks to hardware acceleration, typically in the low single-digit percentage range compared to unencrypted SQLite for individual read operations. The overhead that actually becomes noticeable more often comes from the per-page HMAC check on every access rather than the cryptography itself.
For very large bulk imports or complex migrations with thousands of write operations in a single transaction, the cumulative overhead can become noticeable, usually in the range of 5 to 15 percent longer runtime. In practice, running a concrete benchmark with realistic data volumes from your own app is worth more than relying on generic vendor figures.
6. Migrating existing unencrypted databases
For apps already running in production with unencrypted SQLite, SQLCipher offers an explicit migration routine that copies an existing plain-text database into a new encrypted file, without requiring changes to application code or the data model. This step ideally runs once in the background on first launch after an app update.
During migration, the app should not block the user but show a loading state instead, particularly for larger databases where the copy operation can take several seconds. After a successful migration, the old unencrypted file must be securely deleted, otherwise it remains on the device as an unprotected copy of the same sensitive data.
7. Alternative secure database approaches compared
Besides SQLCipher, other approaches exist for secure local storage: Realm offers built-in encryption as an alternative for apps already using Realm as an object database, while newer op-sqlite versions also ship with a SQLCipher integration and benefit from the new React Native architecture with JSI.
For individual sensitive values rather than entire databases, pure secure storage via Keychain or Keystore, for example through react-native-keychain, remains the right choice, since it's optimized for small key-value pairs like tokens. SQLCipher becomes worthwhile once structured, relational records actually need to be held locally in encrypted form at scale.
8. Common implementation mistakes with encrypted databases
A common mistake is generating the encryption key securely but then accidentally leaking it in crash reports or debug logs, for example by logging the entire connection object during development. A single overlooked log statement can render the entire encryption practically pointless.
It's equally risky to reuse the same key unchanged across app reinstalls without reliably removing the corresponding Keychain or Keystore entry on uninstall. On iOS, a Keychain entry survives an uninstall by default, which can leave a reinstall with an orphaned database and a mismatched or inconsistent key.
9. Checklist for shipping SQLCipher to production
Before a production rollout, a short, concrete checklist pays off: the key is generated and stored exclusively in the Keychain or Keystore, never in code or environment variables. Migration logic for existing users is tested, including securely deleting the old plain-text file after a successful migration.
It's also worth adding a documented recovery plan for a lost key, for example after a reinstall without a backup, plus a realistic performance test with production-scale data volumes. Without that last step, it stays unclear whether the encryption overhead remains genuinely unnoticeable on weaker devices out in the field.
| Approach | Encryption scope | Typical use case | Main limitation |
|---|---|---|---|
| SQLCipher | Entire database file, AES-256 per page | Structured relational data at scale | Extra overhead on very large bulk writes |
| react-native-keychain | Individual key-value pairs | Tokens, small secrets, credentials | Not suited for structured, queryable records |
| Realm with encryption | Entire Realm file, AES-256 | Apps already using Realm as an object database | Tied to the Realm data model and its ecosystem |
| op-sqlite with SQLCipher | Entire database file via JSI | New projects on the new React Native architecture | Younger ecosystem, fewer established practice reports |
| Unencrypted SQLite | None | Non-critical cache and configuration data | Plain-text access on device or backup compromise |
Mironsoft
React Native app development and Magento integration
A mobile app for the Magento shop that actually runs smoothly?
We build React Native apps cleanly connected to the Magento REST or GraphQL API, from the first line of code to publishing on the App Store and Google Play.
App Concept
Plan the architecture and feature scope of a Magento-connected app together.
Magento API Integration
Cleanly connect product catalog, cart, and checkout to the shop API.
Store Publishing
Guide the App Store and Google Play release process without pitfalls.
10. Summary
Encrypted Local Storage: Key Takeaways
Transparent encryption
SQLCipher encrypts every database page with AES-256 without changing the familiar SQL interface for application code.
Key belongs in Keychain/Keystore
The encryption key must live in hardware-backed secure storage, never in code or configuration files.
Overhead is usually low
On current hardware the performance cost for individual operations stays low, but becomes noticeable on large bulk operations.
Plan migration and deletion cleanly
Existing plain-text databases must be migrated and then securely deleted, not merely copied.