本文实例为大家分享了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 里面也是存在的而且名字都一样除了包名不同。
SQLCipher 与Room的结合方式同上面的情形是类似,也是通过代理的方式实现。由于Room需要的类跟SQLCipher 提供的类包名不一致,所以这里需要对SQLCipher 提供的类进行一下代理然后传递给Room架构使用就可以了。
fun init(context: Context) { val mDataBase1 = Room.databaseBuilder( context.applicationContext, TestDb::class.java, "user_login_info_db" ).openHelperFactory(SafeHelperFactory("".toByteArray())) .build() }
这里主要需要自定义一个SupportSQLiteOpenHelper.Factory也就是SafeHelperFactory 这个SafeHelperFactory 完全是仿照Room架构默认的Factory 也就是FrameworkSQLiteOpenHelperFactory 实现。主要是用户创建一个用于打开数据库的SQLiteOpenHelper,主要的区别是自定义的Facttory 需要一个用于加密与解密的密码。
我们首先需要定义一个自己的OpenHelperFactory
public class SafeHelperFactory implements SupportSQLiteOpenHelper.Factory { public static final String POST_KEY_SQL_MIGRATE = "PRAGMA cipher_migrate;"; public static final String POST_KEY_SQL_V3 = "PRAGMA cipher_compatibility = 3;"; final private byte[] passphrase; final private Options options; public SafeHelperFactory(byte[] passphrase, Options options) { this.passphrase = passphrase; this.options = options; } /** * {@inheritDoc} */ @Override public SupportSQLiteOpenHelper create( SupportSQLiteOpenHelper.Configuration configuration) { return(create(configuration.context, configuration.name, configuration.callback)); } public SupportSQLiteOpenHelper create(Context context, String name, SupportSQLiteOpenHelper.Callback callback) { //创建一个Helper return(new Helper(context, name, callback, passphrase, options)); } private void clearPassphrase(char[] passphrase) { for (int i = 0; i < passphrase.length; i ) { passphrase[i] = (byte) 0; } }
SafeHelperFactory 的create创建了一个Helper,这个Helper实现了Room框架的SupportSQLiteOpenHelper ,实际这个Helper 是个代理类被代理的类为OpenHelper ,OpenHelper 用于操作SQLCipher 提供的数据库类。
class Helper implements SupportSQLiteOpenHelper { private final OpenHelper delegate; private final byte[] passphrase; private final boolean clearPassphrase; Helper(Context context, String name, Callback callback, byte[] passphrase, SafeHelperFactory.Options options) { SQLiteDatabase.loadLibs(context); clearPassphrase=options.clearPassphrase; delegate=createDelegate(context, name, callback, options); this.passphrase=passphrase; } private OpenHelper createDelegate(Context context, String name, final Callback callback, SafeHelperFactory.Options options) { final Database[] dbRef = new Database[1]; return(new OpenHelper(context, name, dbRef, callback, options)); } /** * {@inheritDoc} */ @Override synchronized public String getDatabaseName() { return delegate.getDatabaseName(); } /** * {@inheritDoc} */ @Override @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) synchronized public void setWriteAheadLoggingEnabled(boolean enabled) { delegate.setWriteAheadLoggingEnabled(enabled); } @Override synchronized public SupportSQLiteDatabase getWritableDatabase() { SupportSQLiteDatabase result; try { result = delegate.getWritableSupportDatabase(passphrase); } catch (SQLiteException e) { if (passphrase != null) { boolean isCleared = true; for (byte b : passphrase) { isCleared = isCleared && (b == (byte) 0); } if (isCleared) { throw new IllegalStateException("The passphrase appears to be cleared. This happens by" "default the first time you use the factory to open a database, so we can remove the" "cleartext passphrase from memory. If you close the database yourself, please use a" "fresh SafeHelperFactory to reopen it. If something else (e.g., Room) closed the" "database, and you cannot control that, use SafeHelperFactory.Options to opt out of" "the automatic password clearing step. See the project README for more information."); } } throw e; } if (clearPassphrase && passphrase != null) { for (int i = 0; i < passphrase.length; i ) { passphrase[i] = (byte) 0; } } return(result); } /** * {@inheritDoc} * * NOTE: this implementation delegates to getWritableDatabase(), to ensure * that we only need the passphrase once */ @Override public SupportSQLiteDatabase getReadableDatabase() { return(getWritableDatabase()); } /** * {@inheritDoc} */ @Override synchronized public void close() { delegate.close(); } static class OpenHelper extends SQLiteOpenHelper { private final Database[] dbRef; private volatile Callback callback; private volatile boolean migrated; }
真正操作数据库的类OpenHelper,OpenHelper 继承的SQLiteOpenHelper 是net.sqlcipher.database 包下的
static class OpenHelper extends SQLiteOpenHelper { private final Database[] dbRef; private volatile Callback callback; private volatile boolean migrated; OpenHelper(Context context, String name, final Database[] dbRef, final Callback callback, final SafeHelperFactory.Options options) { super(context, name, null, callback.version, new SQLiteDatabaseHook() { @Override public void preKey(SQLiteDatabase database) { if (options!=null && options.preKeySql!=null) { database.rawExecSQL(options.preKeySql); } } @Override public void postKey(SQLiteDatabase database) { if (options!=null && options.postKeySql!=null) { database.rawExecSQL(options.postKeySql); } } }, new DatabaseErrorHandler() { @Override public void onCorruption(SQLiteDatabase dbObj) { Database db = dbRef[0]; if (db != null) { callback.onCorruption(db); } } }); this.dbRef = dbRef; this.callback=callback; } synchronized SupportSQLiteDatabase getWritableSupportDatabase(byte[] passphrase) { migrated = false; SQLiteDatabase db=super.getWritableDatabase(passphrase); if (migrated) { close(); return getWritableSupportDatabase(passphrase); } return getWrappedDb(db); } synchronized Database getWrappedDb(SQLiteDatabase db) { Database wrappedDb = dbRef[0]; if (wrappedDb == null) { wrappedDb = new Database(db); dbRef[0] = wrappedDb; } return(dbRef[0]); } /** * {@inheritDoc} */ @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { callback.onCreate(getWrappedDb(sqLiteDatabase)); } /** * {@inheritDoc} */ @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { migrated = true; callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion); } /** * {@inheritDoc} */ @Override public void onConfigure(SQLiteDatabase db) { callback.onConfigure(getWrappedDb(db)); } /** * {@inheritDoc} */ @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { migrated = true; callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion); } /** * {@inheritDoc} */ @Override public void onOpen(SQLiteDatabase db) { if (!migrated) { // from Google: "if we've migrated, we'll re-open the db so we should not call the callback." callback.onOpen(getWrappedDb(db)); } } /** * {@inheritDoc} */ @Override public synchronized void close() { super.close(); dbRef[0] = null; } }
这里的OpenHelper 完全是仿照Room 框架下的OpenHelper 实现的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持Devmax。