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 實現的。

以上就是本文的全部內容,希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: