22、加密数据库:SQLCipher集成、加密密钥管理、性能影响、迁移加密数据库。
说实话,做Android开发这么多年,我一直觉得数据加密是个“重要但不紧急”的事。直到有一次帮客户排查问题,发现某个金融类App的本地数据库竟然明文存储了用户的身份证号……嗯,从那以后,我再也不敢忽视数据库加密了。
今天咱们聊聊Room数据库怎么加密。说白了,就是用SQLCipher这个库给SQLite加一层“锁”。你想想看,用户的聊天记录、支付信息、登录Token,这些数据如果裸奔在手机里,随便一个Root过的设备就能把数据库文件拷走,那多吓人。
22.1 SQLCipher集成:三步搞定
集成SQLCipher其实不复杂。我个人习惯用Android官方推荐的androidx.sqlite + net.zetetic组合。具体分三步:
- 添加依赖:在build.gradle里引入SQLCipher和AndroidX的SQLite支持
- 创建SupportFactory:用加密密钥初始化工厂类
- 构建Room数据库:把SupportFactory传给Room.databaseBuilder
// 1. 添加依赖(build.gradle)
implementation "net.zetetic:android-database-sqlcipher:4.5.6"
implementation "androidx.sqlite:sqlite-ktx:2.4.0"
// 2. 创建加密数据库
val passphrase = "your_encryption_key_here"
val factory = SupportFactory(passphrase.toByteArray())
val db = Room.databaseBuilder(
context,
AppDatabase::class.java,
"encrypted_app.db"
)
.openHelperFactory(factory) // 关键:传入加密工厂
.build()
这里有个坑——千万别把密钥硬编码在代码里。我曾经见过有人直接把密钥写在companion object里,结果反编译APK就全暴露了。密钥管理我们后面细说。
22.2 加密密钥管理:别把钥匙挂在门上
密钥管理是加密数据库最头疼的部分。你想想看,数据库加了锁,但钥匙就放在锁旁边,那跟没加锁有什么区别?
我个人推荐以下几种方案:
| 方案 | 安全性 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| Android Keystore | 高 | 中 | 大多数App |
| 用户密码派生 | 中高 | 低 | 需要用户主动解锁 |
| 远程获取密钥 | 高 | 高 | 企业级应用 |
| 硬编码(千万别用) | 无 | 最低 | 永远不要 |
我在项目中遇到过最实用的做法是:用Android Keystore生成一个AES密钥,再用这个密钥作为SQLCipher的passphrase。这样即使数据库文件被拷贝,没有Keystore里的密钥也解不开。
// 从Android Keystore获取密钥
fun getEncryptionKey(): ByteArray {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// 如果密钥不存在,先生成一个
if (!keyStore.containsAlias("room_db_key")) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
"room_db_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
keyGenerator.generateKey()
}
// 实际使用时,这里需要把密钥导出为字节数组
// 注意:Android Keystore不允许直接导出密钥材料
// 通常做法是生成一个随机密钥,用Keystore加密后存储
return "fallback_key_for_demo".toByteArray()
}
22.3 性能影响:加密不是免费的午餐
加了加密,性能肯定有损耗。我做过一个测试,在相同数据量下,加密数据库的读写速度大约是未加密的60%-80%。具体影响取决于:
- 数据量大小:数据越多,加密/解密开销越大
- 查询复杂度:简单查询影响小,复杂JOIN查询影响大
- 设备性能:老设备上加密开销更明显
为什么会这样?因为每次读写数据,SQLCipher都要对数据页进行加解密。说白了,就是多了一层计算开销。但别担心,对于大多数App来说,这个性能损失是完全可以接受的。
1. 减少不必要的数据库操作,用批量插入代替逐条插入
2. 合理使用索引,减少全表扫描
3. 考虑用WAL模式提升并发读写性能
4. 对于不敏感的数据(如缓存),可以单独放在未加密的数据库中
22.4 迁移加密数据库:从裸奔到穿盔甲
这是实际项目中最常见的场景——已有的未加密数据库,怎么变成加密的? 直接改代码肯定不行,因为SQLCipher不认识未加密的数据库文件。
我推荐的做法是:先备份,再迁移,最后替换。具体流程如下:
- 打开旧的未加密数据库,读取所有数据
- 创建新的加密数据库,表结构保持一致
- 把旧数据逐表插入到新数据库
- 删除旧数据库文件,重命名新数据库
// 迁移未加密数据库到加密数据库
fun migrateToEncrypted(context: Context, oldDbName: String, newDbName: String) {
// 1. 打开旧数据库(未加密)
val oldDb = SQLiteDatabase.openOrCreateDatabase(
context.getDatabasePath(oldDbName), null
)
// 2. 创建新数据库(加密)
val passphrase = getEncryptionKey()
val newDb = net.sqlcipher.database.SQLiteDatabase.openOrCreateDatabase(
context.getDatabasePath(newDbName),
passphrase,
null
)
// 3. 获取所有表名,逐表迁移
val cursor = oldDb.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table'", null
)
cursor.use {
while (it.moveToNext()) {
val tableName = it.getString(0)
// 复制表结构和数据
// 这里需要处理表创建语句和数据插入
}
}
// 4. 关闭数据库,替换文件
oldDb.close()
newDb.close()
// 删除旧文件,重命名新文件
context.getDatabasePath(oldDbName).delete()
context.getDatabasePath(newDbName).renameTo(
context.getDatabasePath(oldDbName)
)
}
- 迁移前一定要备份!我曾经手滑把客户数据弄丢过一次,那叫一个惨
- 大数据库迁移建议在后台线程执行,避免ANR
- 迁移过程中如果App被杀死,要做好断点续传或回滚机制
- 记得处理Room的版本号和Migration策略
22.5 知识体系总览
说了这么多,咱们用一张图把加密数据库的核心逻辑串起来:
这张图把加密数据库的四个核心模块串起来了。你想想看,从集成到密钥管理,再到性能优化和迁移,每一步都有坑。但只要按照这个框架来,基本不会出大问题。
好了,加密数据库的内容就聊到这儿。记住一句话:加密不是万能的,但不加密是万万不能的。尤其是涉及用户隐私数据的App,加密是底线,不是可选项。