Android?Room數(shù)據(jù)庫(kù)加密詳解
本文實(shí)例為大家分享了Android Room之?dāng)?shù)據(jù)庫(kù)加密的具體實(shí)現(xiàn),供大家參考,具體內(nèi)容如下
一、需求背景
Android平臺(tái)自帶的SQLite有一個(gè)致命的缺陷:不支持加密。這就導(dǎo)致存儲(chǔ)在SQLite中的數(shù)據(jù)可以被任何人用任何文本編輯器查看到。如果是普通的數(shù)據(jù)還好,但是當(dāng)涉及到一些賬號(hào)密碼,或者聊天內(nèi)容的時(shí)候,我們的應(yīng)用就會(huì)面臨嚴(yán)重的安全漏洞隱患。
二、加密方案
1、在數(shù)據(jù)存儲(chǔ)之前進(jìn)行加密,在加載數(shù)據(jù)之后再進(jìn)行解密,這種方法大概是最容易想的到,而且也不能說(shuō)這種方式不好,就是有些比較繁瑣。 如果項(xiàng)目有特殊需求的話,可能還需要對(duì)數(shù)據(jù)庫(kù)的表明,列明也進(jìn)行加密。
2、對(duì)數(shù)據(jù)庫(kù)整個(gè)文件進(jìn)行加密,好處就是就是無(wú)需在插入之前對(duì)數(shù)據(jù)加密,也無(wú)需在查詢數(shù)據(jù)之后再解密。比較出名的第三方庫(kù)就是SQLCipher,它采用的方式就是對(duì)數(shù)據(jù)庫(kù)文件進(jìn)行加密,只需在打開(kāi)數(shù)據(jù)庫(kù)的時(shí)候輸入密碼,之后的操作更正常操作沒(méi)有區(qū)別。
三、Hook Room實(shí)現(xiàn)方式
前面說(shuō)了,加密的方式一比較繁瑣的地方是需要在存儲(chǔ)數(shù)據(jù)之前加密,在檢索數(shù)據(jù)之后解密,那么是否有一種方式在Room操作數(shù)據(jù)庫(kù)的過(guò)程中,自動(dòng)對(duì)數(shù)據(jù)加密解密,答案是有的。
Dao編譯之后的代碼是這樣的:
@Override public long saveCache(final CacheTest cache) { ? __db.assertNotSuspendingTransaction(); ? __db.beginTransaction(); ? try { ? //核心代碼,綁定數(shù)據(jù) ? ? long _result = __insertionAdapterOfCacheTest.insertAndReturnId(cache); ? ? __db.setTransactionSuccessful(); ? ? return _result; ? } finally { ? ? __db.endTransaction(); ? } }
__insertionAdapterOfCacheTest 是在CacheDaoTest_Impl 的構(gòu)造方法里面創(chuàng)建的一個(gè)匿名內(nèi)部類,這個(gè)匿名內(nèi)部類實(shí)現(xiàn)了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()); ? ? ? } ? ? } ? }; }
關(guān)于SQLiteStatement 不清楚的同學(xué)可以百度一下,簡(jiǎn)單說(shuō)他就代表一句sql語(yǔ)句,bind 方法就是綁定sql語(yǔ)句所需要的參數(shù),現(xiàn)在的問(wèn)題是我們可否自定義一個(gè)SupportSQLiteStatement ,然后在bind的時(shí)候加密參數(shù)呢。
我們看一下SupportSQLiteStatement 的創(chuàng)建過(guò)程。
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 最終來(lái)自RoomDataBase的compileStatement 方法,這就給我們hook 提供了接口,我們只要自定義一個(gè)SupportSQLiteStatement 類來(lái)代理原來(lái)的SupportSQLiteStatement 就可以了。
encoder 就是用來(lái)加密數(shù)據(jù)的。
加密數(shù)據(jù)之后剩余的就是解密數(shù)據(jù)了,解密數(shù)據(jù)我們需要在哪里Hook呢?
我們知道數(shù)據(jù)庫(kù)檢索返回的數(shù)據(jù)一般都是通過(guò)Cursor 傳遞給用戶,這里我們就可以通過(guò)代理數(shù)據(jù)庫(kù)返回的這個(gè)Cursor 進(jìn)而實(shí)現(xiàn)解密數(shù)據(jù)。
@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("開(kāi)始查詢1") ? ? ? ? return DencodeCursor(cusrosr, encoder) ? ? } ? ? override fun query(query: String, args: Array<out Any>?): Cursor { ? ? ? ? var cusrosr = super.query(query, args) ? ? ? ? println("開(kāi)始查詢2") ? ? ? ? return DencodeCursor(cusrosr, encoder) ? ? } ? ? override fun query(query: SupportSQLiteQuery, signal: CancellationSignal?): Cursor { ? ? ? ? println("開(kāi)始查詢3") ? ? ? ? return DencodeCursor(super.query(query, signal), encoder) ? ? } }
我們這里重寫(xiě)了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框架中間。但是這種有兩個(gè)個(gè)缺陷
加密解密的過(guò)程中不可以改變數(shù)據(jù)的類型,也就是整型在加密之后還必須是整型,整型在解密之后也必須是整型。同時(shí)有些字段可能不需要加密也不需要解密,例如自增長(zhǎng)的整型的primary key。其實(shí)這種方式也比較好解決,可以規(guī)定key 為整數(shù)型,其余的數(shù)據(jù)一律是字符串。這樣所有的樹(shù)數(shù)字類型的數(shù)據(jù)都不需要參與加密解密的過(guò)程。
sql 與的參數(shù)必須是動(dòng)態(tài)綁定的,而不是在sql語(yǔ)句中靜態(tài)指定。
@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 仿照官方的架構(gòu)自己重寫(xiě)了一套代碼,官方提供的各種數(shù)據(jù)庫(kù)相關(guān)的類在SQLCipher 里面也是存在的而且名字都一樣除了包名不同。
SQLCipher 與Room的結(jié)合方式同上面的情形是類似,也是通過(guò)代理的方式實(shí)現(xiàn)。由于Room需要的類跟SQLCipher 提供的類包名不一致,所以這里需要對(duì)SQLCipher 提供的類進(jìn)行一下代理然后傳遞給Room架構(gòu)使用就可以了。
fun init(context: Context) { ? val ?mDataBase1 = Room.databaseBuilder( ? ? ? ? context.applicationContext, ? ? ? ? TestDb::class.java, ? ? ? ? "user_login_info_db" ? ? ).openHelperFactory(SafeHelperFactory("".toByteArray())) ? ? ? .build() }
這里主要需要自定義一個(gè)SupportSQLiteOpenHelper.Factory也就是SafeHelperFactory 這個(gè)SafeHelperFactory 完全是仿照Room架構(gòu)默認(rèn)的Factory 也就是FrameworkSQLiteOpenHelperFactory 實(shí)現(xiàn)。主要是用戶創(chuàng)建一個(gè)用于打開(kāi)數(shù)據(jù)庫(kù)的SQLiteOpenHelper,主要的區(qū)別是自定義的Facttory 需要一個(gè)用于加密與解密的密碼。
我們首先需要定義一個(gè)自己的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) { ? ? ?//創(chuàng)建一個(gè)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創(chuàng)建了一個(gè)Helper,這個(gè)Helper實(shí)現(xiàn)了Room框架的SupportSQLiteOpenHelper ,實(shí)際這個(gè)Helper 是個(gè)代理類被代理的類為OpenHelper ,OpenHelper 用于操作SQLCipher 提供的數(shù)據(jù)庫(kù)類。
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; }
真正操作數(shù)據(jù)庫(kù)的類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 實(shí)現(xiàn)的。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
最常見(jiàn)的猜拳小游戲Android代碼實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了最常見(jiàn)的猜拳小游戲Android代碼實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08Android自定義圓形倒計(jì)時(shí)進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義圓形倒計(jì)時(shí)進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09android 實(shí)現(xiàn)ScrollView自動(dòng)滾動(dòng)的實(shí)例代碼
這篇文章主要介紹了android 實(shí)現(xiàn)ScrollView自動(dòng)滾動(dòng)的實(shí)例代碼,有需要的朋友可以參考一下2014-01-01Kotlin 封裝萬(wàn)能SharedPreferences存取任何類型詳解
這篇文章主要介紹了Kotlin 封裝萬(wàn)能SharedPreferences存取任何類型詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05Android NDK 開(kāi)發(fā)中 SO 包大小壓縮方法詳解
這篇文章主要為為大家介紹了Android NDK 開(kāi)發(fā)中 SO 包大小壓縮方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09如何在Android中實(shí)現(xiàn)漸顯按鈕的左右滑動(dòng)效果
本篇文章是對(duì)在Android中實(shí)現(xiàn)漸顯按鈕的左右滑動(dòng)效果進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06快速解決Android平臺(tái)移植ffmpeg的一些問(wèn)題
模仿Android的MediaPlayer類實(shí)現(xiàn)了ffmpeg的播放接口,如setDataSource(),setDisplay(),start(), stop(),pause()等,缺點(diǎn)是沒(méi)有實(shí)現(xiàn)seek功能2013-11-11Android緩存之DiskLruCache磁盤(pán)緩存的使用
這篇文章主要介紹了Android緩存之DiskLruCache磁盤(pán)緩存的使用,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08