本文整理自网络,侵删。
本文实例为大家分享了Android Room之数据库加密的具体实现,供大家参考,具体内容如下
一、需求背景
Android平台自带的SQLite有一个致命的缺陷:不支持加密。这就导致存储在SQLite中的数据可以被任何人用任何文本编辑器查看到。如果是普通的数据还好,但是当涉及到一些账号密码,或者聊天内容的时候,我们的应用就会面临严重的安全漏洞隐患。
二、加密方案
1、在数据存储之前进行加密,在加载数据之后再进行解密,这种方法大概是最容易想的到,而且也不能说这种方式不好,就是有些比较繁琐。 如果项目有特殊需求的话,可能还需要对数据库的表明,列明也进行加密。
2、对数据库整个文件进行加密,好处就是就是无需在插入之前对数据加密,也无需在查询数据之后再解密。比较出名的第三方库就是SQLCipher,它采用的方式就是对数据库文件进行加密,只需在打开数据库的时候输入密码,之后的操作更正常操作没有区别。
三、Hook Room实现方式
前面说了,加密的方式一比较繁琐的地方是需要在存储数据之前加密,在检索数据之后解密,那么是否有一种方式在Room操作数据库的过程中,自动对数据加密解密,答案是有的。
Dao编译之后的代码是这样的:
@Override public long saveCache(final CacheTest cache) { ? __db.assertNotSuspendingTransaction(); ? __db.beginTransaction(); ? try { ? //核心代码,绑定数据 ? ? long _result = __insertionAdapterOfCacheTest.insertAndReturnId(cache); ? ? __db.setTransactionSuccessful(); ? ? return _result; ? } finally { ? ? __db.endTransaction(); ? } }
__insertionAdapterOfCacheTest 是在CacheDaoTest_Impl 的构造方法里面创建的一个匿名内部类,这个匿名内部类实现了bind 方法
public CacheDaoTest_Impl(RoomDatabase __db) { ? this.__db = __db; ? this.__insertionAdapterOfCacheTest = new EntityInsertionAdapter<CacheTest>(__db) { ? ? @Override ? ? public String createQuery() { ? ? ? return "INSERT OR REPLACE INTO `table_cache` (`key`,`name`) VALUES (?,?)"; ? ? } ? ? @Override ? ? public void bind(SupportSQLiteStatement stmt, CacheTest value) { ? ? ? if (value.getKey() == null) { ? ? ? ? stmt.bindNull(1); ? ? ? } else { ? ? ? ? stmt.bindString(1, value.getKey()); ? ? ? } ? ? ? if (value.getName() == null) { ? ? ? ? stmt.bindNull(2); ? ? ? } else { ? ? ? ? stmt.bindString(2, value.getName()); ? ? ? } ? ? } ? }; }
关于SQLiteStatement 不清楚的同学可以百度一下,简单说他就代表一句sql语句,bind 方法就是绑定sql语句所需要的参数,现在的问题是我们可否自定义一个SupportSQLiteStatement ,然后在bind的时候加密参数呢。
我们看一下SupportSQLiteStatement 的创建过程。
public SupportSQLiteStatement acquire() { ? ? ?assertNotMainThread(); ? ? ?return getStmt(mLock.compareAndSet(false, true)); ?} ? ?private SupportSQLiteStatement getStmt(boolean canUseCached) { ? ? ?final SupportSQLiteStatement stmt; ? ? ?//代码有删减 ? ? ? ? stmt = createNewStatement(); ? ? ?return stmt; ?} kotlin ?private SupportSQLiteStatement createNewStatement() { ? ? ?String query = createQuery(); ? ? ?return mDatabase.compileStatement(query); ?}
可以看到SupportSQLiteStatement 最终来自RoomDataBase的compileStatement 方法,这就给我们hook 提供了接口,我们只要自定义一个SupportSQLiteStatement 类来代理原来的SupportSQLiteStatement 就可以了。
encoder 就是用来加密数据的。
加密数据之后剩余的就是解密数据了,解密数据我们需要在哪里Hook呢?
我们知道数据库检索返回的数据一般都是通过Cursor 传递给用户,这里我们就可以通过代理数据库返回的这个Cursor 进而实现解密数据。
@Database(entities = [CacheTest::class], version = 3) abstract class TestDb : RoomDatabase() { ? ? abstract fun testDao(): CacheDaoTest ? ? companion object { ? ? ? ? val MIGRATION_2_1: Migration = object : Migration(2, 1) { ? ? ? ? ? ? override fun migrate(database: SupportSQLiteDatabase) { ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? val MIGRATION_2_3: Migration = object : Migration(2, 3) { ? ? ? ? ? ? override fun migrate(database: SupportSQLiteDatabase) { ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? val MIGRATION_3_4: Migration = object : Migration(3,4) { ? ? ? ? ? ? override fun migrate(database: SupportSQLiteDatabase) { ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? val MIGRATION_2_4: Migration = object : Migration(2, 4) { ? ? ? ? ? ? override fun migrate(database: SupportSQLiteDatabase) { ? ? ? ? ? ? } ? ? ? ? } ? ? } ? ? private val encoder: IEncode = TestEncoder() ? ? override fun query(query: SupportSQLiteQuery): Cursor { ? ? ? ? var cusrosr = super.query(query) ? ? ? ? println("开始查询1") ? ? ? ? return DencodeCursor(cusrosr, encoder) ? ? } ? ? override fun query(query: String, args: Array<out Any>?): Cursor { ? ? ? ? var cusrosr = super.query(query, args) ? ? ? ? println("开始查询2") ? ? ? ? return DencodeCursor(cusrosr, encoder) ? ? } ? ? override fun query(query: SupportSQLiteQuery, signal: CancellationSignal?): Cursor { ? ? ? ? println("开始查询3") ? ? ? ? return DencodeCursor(super.query(query, signal), encoder) ? ? } }
我们这里重写了RoomDatabase 的是query 方法,代理了原先的Cursor 。
class DencodeCursor(val delete: Cursor, val encoder: IEncode) : Cursor { //代码有删减 ? ? override fun getString(columnIndex: Int): String { ? ? ? ? return encoder.decodeString(delete.getString(columnIndex)) ? ? } }
如上,最终加密解密的都被hook在了Room框架中间。但是这种有两个个缺陷
加密解密的过程中不可以改变数据的类型,也就是整型在加密之后还必须是整型,整型在解密之后也必须是整型。同时有些字段可能不需要加密也不需要解密,例如自增长的整型的primary key。其实这种方式也比较好解决,可以规定key 为整数型,其余的数据一律是字符串。这样所有的树数字类型的数据都不需要参与加密解密的过程。
sql 与的参数必须是动态绑定的,而不是在sql语句中静态指定。
@Query("select * from table_cache where `key`=:primaryKey") fun getCache(primaryKey: String): LiveData<CacheTest>
@Query("select * from table_cache where `key`= '123' ") fun getCache(): LiveData<CacheTest>
四、SQLCipher方式
SQLCipher 仿照官方的架构自己重写了一套代码,官方提供的各种数据库相关的类在SQLCipher 里面也是存在的而且名字都一样除了包名不同。
相关阅读 >>
Sqlite数据库常用语句及mac上的Sqlite可视化工具meassqllite使用方法
Sqlitestudio优雅调试android手机数据库Sqlite(推荐)
更多相关阅读请进入《Sqlite》频道 >>
数据库系统概念 第6版
本书主要讲述了数据模型、基于对象的数据库和XML、数据存储和查询、事务管理、体系结构等方面的内容。