Android Room数据库加密详解
本文实例为大家分享了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 实现的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。
原文出处:https://blog.csdn.net/weixin_42600398/article/details/120545
相关文章
- 操作类就是把一些常用的一系列的数据库或相关操作写在一个类中,这样调用时我们只要调用类文件,如果要执行相关操作就直接调用类文件中的方法函数就可以实现了,下面整理了...2016-11-25
- 本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
- 这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
- 有时为了网站安全和版权问题,会对自己写的php源码进行加密,在php加密技术上最常用的是zend公司的zend guard 加密软件,现在我们来图文讲解一下。 下面就简单说说如何...2016-11-25
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- 这篇文章主要介绍了Intellij IDEA连接Navicat数据库的方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借价值,需要的朋友可以参考下...2021-03-25
- 在开发过程中,我们经常会将日期时间的毫秒数存放到数据库,但是它对应的时间看起来就十分不方便,我们可以使用一些函数将毫秒转换成date格式。 一、 在MySQL中,有内置的函数from_unixtime()来做相应的转换,使用如下: 复制...2014-05-31
- C#使用System.IO中的文件操作方法在Windows系统中处理本地文件相当顺手,这里我们还总结了在Oracle中保存文件的方法,嗯,接下来就来看看整理的C#操作本地文件及保存文件到数据库的基本方法总结...2020-06-25
- 通过内网连另外一台机器的mysql服务, 确发现速度N慢! 等了大约几十秒才等到提示输入密码。 但是ping mysql所在服务器却很快! 想到很久之前有过类似的经验, telnet等一些服务在连接请求的时候,会做一些反向域名解析(如果...2015-10-21
- 某些时候,例如为了搭建一个测试环境,或者克隆一个网站,需要复制一个已存在的mysql数据库。使用以下方法,可以非常简单地实现。假设已经存在的数据库名字叫db1,想要复制一份,命名为newdb。步骤如下:1. 首先创建新的数据库newd...2015-10-21
- mysqldump命令的用法1、导出所有库系统命令行mysqldump -uusername -ppassword --all-databases > all.sql 2、导入所有库mysql命令行mysql>source all.sql; 3、导出某些库系统命令行mysqldump -uusername -ppassword...2015-10-21
Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 1005:创建表失败1006:创建数据库失败1007:数据库已存在,创建数据库失败1008:数据库不存在,删除数据库失败1009:不能删除数据库文件导致删除数据库失败1010:不能删除数据目录导致删除数据库失败1011:删除数据库...2013-09-23
- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 这篇文章主要介绍了vue接口请求加密实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-12
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 这篇文章主要介绍了node.js如何操作MySQL数据库,帮助大家更好的进行web开发,感兴趣的朋友可以了解下...2020-10-29
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20