SQLCipher: Encrypting SQLite Databases
Local database storage is a common requirement in mobile apps. But SQLite stores data in plaintext by default. Once a device is rooted, an attacker can easily read the data inside. SQLCipher solves this security problem through transparent encryption.
SQLCipher is an open-source library developed by Zetetic that adds transparent AES-256 encryption to SQLite. It is a security extension of SQLite, and all encryption operations are completely transparent to the application layer. You can continue using the standard SQLite API without modifying existing code.
| Feature | Description |
|---|---|
| Encryption algorithm | AES-256-CBC |
| Key derivation | PBKDF2 |
| Encryption unit | Page-level encryption (each page independently) |
| Performance overhead | About 5-15% |
| Authentication | HMAC-SHA256 |
Encryption principles
Key derivation
The user only needs to provide a password, and SQLCipher converts it into an encryption key through PBKDF2 (Password-Based Key Derivation Function 2).
// SQLCipher 密钥派生简化流程
PBKDF2(
password, // 用户口令
salt, // 随机盐值(数据库生成)
256000, // 迭代次数(SQLCipher 默认)
SHA256, // 伪随机函数
32, // 输出密钥长度(256 bits)
-> aes_key // AES-256 加密密钥
)
Security note: A random salt is generated every time a database is created. This means that even if two users use the same password, the derived keys will be different.
Page-level encryption
SQLCipher does not encrypt the entire database file. Instead, it encrypts page by page:
graph TD
A[数据库文件] --> B[页1<br/>明文]
A --> C[页2<br/>明文]
A --> D[页3<br/>明文]
B --> B1[加密]
C --> C1[加密]
D --> D1[加密]
B1 --> F[AES-256-CBC]
C1 --> F
D1 --> F
F --> G[页1<br/>密文]
F --> H[页2<br/>密文]
F --> I[页3<br/>密文]
- Default page size: 4096 bytes
- Each page uses an independent IV (initialization vector)
- Decrypts the entire page on read, encrypts the entire page on write
Message authentication: tamper-proofing
Each encrypted page ends with an HMAC-SHA256 message authentication code:
// 写入时的处理流程
1. 生成随机 IV
2. AES-256-CBC 加密页数据
3. 计算密文的 HMAC-SHA256
4. 将 IV + 密文 + HMAC 写入磁盘
HMAC is verified on read; decryption is refused if the data has been tampered with.
Encryption comparison
Use the strings command to inspect SQLite vs SQLCipher files:
Plain SQLite database (plaintext is readable):
SQLite format 3...
users
id
name
email
admin
test@example.com
SQLCipher encrypted database (completely unreadable):
¬í™²±…º§æ®™²±…º§æ®™²±…º§æ®™²±…º§æ
홲±…º§æ®™²±…º§æ®™²±…º§æ®™²±…º§æ
Android integration
Add dependencies
// build.gradle (app)
dependencies {
implementation 'net.zetetic:android-database-sqlcipher:4.5.4'
implementation 'androidx.sqlite:sqlite:2.4.0'
}
Using Room + SQLCipher
// 1. 创建加密的数据库
val passphrase = getOrCreateDatabaseKey()
val factory = SupportFactory(passphrase)
// 2. 配置 Room
val room = Room.databaseBuilder(
applicationContext,
MyDatabase::class.java,
"encrypted.db"
)
.openHelperFactory(factory)
.build()
// 3. 获取 DAO
val userDao = room.userDao()
Storing keys securely
❌ Practices to avoid:
// 硬编码密钥 - 极不安全
val key = "my-secret-key".toByteArray()
✅ Recommended approach:
// 使用 Android Keystore 加密密钥
val secureKey = EncryptedSharedPreferences
.create(context, "db_key_prefs", masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 存储密钥
secureKey.edit().putString("db_key", Base64.encodeToString(key, Base64.DEFAULT))
Performance optimization
Configure page size appropriately
// SQLite 页大小与性能
// 较小页:查询灵活,但加密开销高
// 较大页:顺序读性能好,加密开销低
PRAGMA page_size = 4096; // 默认,适合大多数场景
PRAGMA page_size = 8192; // 大型顺序读场景
Use WAL mode
// 启用 WAL 模式提升并发性能
val db = room.openHelper.writableDatabase
db.execSQL("PRAGMA journal_mode = WAL")
Cache optimization
// 调整缓存大小(单位:页)
db.execSQL("PRAGMA cache_size = 10000")
Migration guide
Migrating from plaintext SQLite
// 使用 SQLCipher 附带的迁移工具
val sourceDb = SQLiteDatabase.openDatabase(
"plain.db", null, SQLiteDatabase.OPEN_READWRITE
)
val destDb = SQLiteDatabase.openDatabase(
"encrypted.db",
SupportFactory(key).apply { loadLibs(context) },
SQLiteDatabase.CREATE_IF_NECESSARY
)
// 执行迁移
sourceDb.rawExecSQL("ATTACH DATABASE '${destDb.path}' AS encrypted KEY 'your-password'")
sourceDb.rawExecSQL("SELECT sqlcipher_export('encrypted')")
sourceDb.rawExecSQL("DETACH DATABASE encrypted")
FAQ
- What if I forget the key?
Unrecoverable. SQLCipher is designed so that even the developer cannot decrypt the data. Be sure to back up the key or use a recoverable key storage scheme.
- How do I verify the database is encrypted?
// 检查文件头
val file = File(dbPath)
val header = ByteArray(16)
FileInputStream(file).use { it.read(header) }
// 明文 SQLite: "SQLite format 3"
// SQLCipher: 随机字节
val isEncrypted = !String(header).contains("SQLite")
- Which platforms are supported?
| Platform | Support status |
|---|---|
| Android | ✅ Full support |
| iOS | ✅ Full support |
| macOS | ✅ Full support |
| Windows | ✅ Full support |
| Linux | ✅ Full support |