一、需求背景

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) {@Overridepublic String createQuery() {return "INSERT OR REPLACE INTO `table_cache` (`key`,`name`) VALUES (?,?)";}@Overridepublic 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;}kotlinprivate 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(): CacheDaoTestcompanion 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}*/@Overridepublic SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {return(create(configuration.context, configuration.name,configuration.callback));}public SupportSQLiteOpenHelper create(Context context, String name,SupportSQLiteOpenHelper.Callback callback) {//创建一个Helperreturn(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}*/@Overridesynchronized public String getDatabaseName() {return delegate.getDatabaseName();}/*** {@inheritDoc}*/@Override@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)synchronized public void setWriteAheadLoggingEnabled(boolean enabled) {delegate.setWriteAheadLoggingEnabled(enabled);}@Overridesynchronized 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*/@Overridepublic SupportSQLiteDatabase getReadableDatabase() {return(getWritableDatabase());}/*** {@inheritDoc}*/@Overridesynchronized 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() {@Overridepublic void preKey(SQLiteDatabase database) {if (options!=null && options.preKeySql!=null) {database.rawExecSQL(options.preKeySql);}}@Overridepublic void postKey(SQLiteDatabase database) {if (options!=null && options.postKeySql!=null) {database.rawExecSQL(options.postKeySql);}}}, new DatabaseErrorHandler() {@Overridepublic 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}*/@Overridepublic void onCreate(SQLiteDatabase sqLiteDatabase) {callback.onCreate(getWrappedDb(sqLiteDatabase));}/*** {@inheritDoc}*/@Overridepublic void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {migrated = true;callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion);}/*** {@inheritDoc}*/@Overridepublic void onConfigure(SQLiteDatabase db) {callback.onConfigure(getWrappedDb(db));}/*** {@inheritDoc}*/@Overridepublic void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {migrated = true;callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion);}/*** {@inheritDoc}*/@Overridepublic 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}*/@Overridepublic synchronized void close() {super.close();dbRef[0] = null;}}

这里的OpenHelper 完全是仿照Room 框架下的OpenHelper 实现的

Android Room之数据库加密相关推荐

  1. 利用SQLChiper对Android SQLite数据库加密

    利用SQLChiper对Android SQLite数据库加密 前言: 上篇文章讲了Android studio+SQLCipher加密SQLite数据库的几个坑,跳过这几个坑,那么SQLCipher ...

  2. android sqlcipher 加密,Android 数据库加密 SQLCipher使用方法

    android sqlcipher使用方法 最近在做数据库加密,遇到了些问题,特此记录 greendao 支持数据库加密 网址https://greenrobot.org/greendao/docum ...

  3. android数据库文件是否加密存储,详解Android数据存储之SQLCipher数据库加密

    前言: 最近研究了Android Sqlite数据库以及ContentProvider程序间数据共享,我们清晰的知道Sqlite数据库默认存放位置data/data/pakage/database目录 ...

  4. Android 数据库加密

    一 一个简短的引论   SQLite是一个轻量的.跨平台的.开源的数据库引擎.它的读写效率.资源消耗总量.延迟时间和总体简单性上具有的优越性,使其成为移动平台数据库的最佳解决方式(如Android.i ...

  5. 利用Android手机破解微信加密数据库EnMicroMsg.db文件

    要先批评一下微信,居然用开源的数据库加密方式,这不是一破解一个准吗? 如果你的模拟器或者真机已经获得了root权限,就可以直接将记录聊天记录的数据库文件拷贝出来,数据库文件具体存放位置如下: 在/da ...

  6. 直接利用Android手机破解微信加密数据库EnMicroMsg.db

    ※首先,简单介绍一下微信加密数据库EnMicroMsg.db的破解方法: 要先批评一下微信,居然用开源的数据库加密方式,这不是一破解一个准吗... 如果你的模拟器或者真机已经获得了root权限,就可以 ...

  7. android sqlite加密数据库,Android Sqlite数据库加密

    Android使用的是开源的SQLite数据库,数据库本身没有加密,加密思路通常有两个: 1. 对几个关键的字段使用加密算法,再存入数据库 2. 对整个数据库进行加密 SQLite数据库加密工具: 收 ...

  8. android sqlcipher github,Android应用开发Android 数据库加密 SQLCipher使用方法

    Android 数据库加密 SQLCipher使用方法 最近在做数据库加密,遇到了些问题,特此记录 greendao 支持数据库加密 网址https://greenrobot.org/greendao ...

  9. android sqlite数据库加密,(转)SQLite数据库的加密

    1.创建空的SQLite数据库. //数据库名的后缀你可以直接指定,甚至没有后缀都可以 //方法一:创建一个空sqlite数据库,用IO的方式 FileStream fs = File.Create( ...

最新文章

  1. HTML--猫眼电影---浮动练习
  2. 数据蒋堂 | 怎样看待存储过程的移植困难
  3. 使用EasyUI Tree异步加载JSON数据 生成树
  4. AVL添加平衡二叉树,是一种二叉排序树,其中每个结点的左子树和右子树的高度差至多等于1。-icoding-数据结构-C-typedef struct node{ int val;
  5. 关于一些html和css2的知识点
  6. 云栖专辑 | 阿里开发者们的第3个感悟:从身边开源开始学习,用过才能更好理解代码
  7. 【C/C++】异或操作巧妙实现两个数的交换操作
  8. 从内存中窃取未加密的SSH-agent密钥
  9. 计算机辅助设计实训报告范文,cad室内实训报告范文
  10. Atitit code for biz lst idx项目分析法,包括模块分析,与模块位置idx数据库分析 数据表的分类 日志表不断增长(包括用户表,订单表等)。。元数据表表 基本不增长。。。
  11. 成功要素:富兰克林的13条必要美德! 与 狗熊掰棒子
  12. 如何利用turtle制作一个滑稽表情
  13. 水文勘测工比赛计算机基本应用,水文勘测工技能大赛参赛感言
  14. node.js版本不同,跑不起项目(个人笔记)
  15. 避免localStorage被更改 让localStorage禁止修改值
  16. MySQL中的max_connections和max_user_connections 及 MySQL服务器最大连接数的合理设置
  17. 英文SEO移动端搜索优化指南
  18. LVM(logical volume manager) 逻辑卷管理器
  19. Java 上机题 1.9 Guess 简单猜数值游戏
  20. 如何免费申请亚洲诚信TrustAsia通配符证书并部署到宝塔面板

热门文章

  1. QT5百度地图开发学习——qt调用JavaScript函数并传参
  2. Python中的pillow(PIL)
  3. 项目分享|小师弟手把手教你用蓝牙模块
  4. linux常用命令 cp命令的使用和介绍
  5. 转:适用于虚拟桌面的 Windows 10 企业版
  6. 基于FPGA的密码锁开发——(1)密码锁驱动
  7. 常用英语食品词汇- 蔬菜类
  8. 当 IDENTITY_INSERT 设置为 OFF 时,不能为表 ‘xxx‘ 中的标识列插入显式值。 java解决方案
  9. Oracle中to_char函数的速度问题
  10. 看了他们的遭遇,我居然不想裸辞了