android Jetpack Room之数据库加密

数据加密

  Android平台自带的SQLite有一个致命的缺陷:不支持加密。这就导致存储在SQLite中的数据可以被任何人用任何文本编辑器查看到。如果是普通的数据还好,但是当涉及到一些账号密码,或者聊天内容的时候,我们的应用就会面临严重的安全漏洞隐患。

加密方案

第一种方案

 在数据存储之前进行加载,在加载数据之后再进行解密,这种方法大概是最容易想的到,而且也不能说这种方式不好,就是有些比较繁琐。 如果项目有特殊需求的话,可能还需要对数据库的表明,列明也进行加密。

第二种方法

 对数据库整个文件进行加密,好处就是就是无需在插入之前对数据加密,也无需在查询数据之后再解密。比较出名的第三方库就是SQLCipher,它采用的方式就是对数据库文件进行加密,只需在打开数据库的时候输入密码,之后的操作更正常操作没有区别。

Room 加密数据

方式一

  前面说了,加密的方式一比较繁琐的地方是需要在存储数据之前加密,在检索数据之后解密,那么是否有一种方式在Room操作数据库的过程中,自动对数据加密解密,答案是有的。

  这里依然一前面章节的代码为例子

@Entity(tableName = "table_cache")
class CacheTest {
    @PrimaryKey(autoGenerate = false)
    @NonNull
    var key: String = ""
    @ColumnInfo(defaultValue = "default value")
    var name: String? = null
}
@Dao
interface CacheDaoTest {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun saveCache(cache: CacheTest): Long
}

编译之后生成的代码如下

  @Override
  public long saveCache(final CacheTest cache) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
    //核心代码,绑定数据
      long _result = __insertionAdapterOfCacheTest.insertAndReturnId(cache);
      __db.setTransactionSuccessful();
      return _result;
    } finally {
      __db.endTransaction();
    }
  }
    public final long insertAndReturnId(T entity) {
        final SupportSQLiteStatement stmt = acquire();
        try {
          //bind 是一个抽象方法,子类需要实现
            bind(stmt, entity);
            return stmt.executeInsert();
        } finally {
            release(stmt);
        }
    }

__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 的创建过程。

final SupportSQLiteStatement stmt = acquire();
    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框架中间。但是这种有两个个缺陷

  1. 加密解密的过程中不可以改变数据的类型,也就是整型在加密之后还必须是整型,整型在解密之后也必须是整型。同时有些字段可能不需要加密也不需要解密,例如自增长的整型的primary key。其实这种方式也比较好解决,可以规定key 为整数型,其余的数据一律是字符串。这样所有的树数字类型的数据都不需要参与加密解密的过程。
  2. 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>

下面的这个查询条件key=‘123’ 是写死在sql语句中的,自然不会通过bind 动态绑定参数,此时也就不会被加密。

方式二

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 需要一个用于加密与解密的密码。

关于FrameworkSQLiteOpenHelperFactory 大家可以查看前面的文章。
在这里插入图片描述
我们首先需要定义一个自己的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 实现的,关于这个OpenHelper 在前面的文章也有介绍。

继续研究,最终我们需要代理的类包括SQLCipher 框架 里面的SQLiteDatabase,SQLiteOpenHelper,Program,Statement这几个类。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

由于这里涉及的代码量实在太大就不在这里把所有的代码都贴出来来了,感兴趣的同学可以在下方的链接去下载研究。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,下面是关于Android Room数据库的简单介绍和使用方法: Android Room是一种SQLite数据库的对象映射库,它提供了一种简单的方法来访问SQLite数据库,而无需编写复杂的SQL查询。它是Google推出的一种Jetpack组件,旨在帮助Android开发者更加简便地使用SQLite数据库。 下面是使用Android Room数据库的基本步骤: 1. 添加依赖项:在app/build.gradle文件中添加以下依赖项: ``` dependencies { def room_version = "2.3.0" // Room components implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" // optional - Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" // optional - RxJava support for Room implementation "androidx.room:room-rxjava2:$room_version" // optional - Guava support for Room, including Optional and ListenableFuture implementation "androidx.room:room-guava:$room_version" // Test helpers testImplementation "androidx.room:room-testing:$room_version" } ``` 2. 创建实体:创建一个Java类或Kotlin数据类,用于表示数据库表中的一行数据。示例代码: ``` @Entity(tableName = "users") data class User( @PrimaryKey val id: Int, @ColumnInfo(name = "user_name") val userName: String, @ColumnInfo(name = "user_age") val userAge: Int ) ``` 3. 创建DAO:创建一个接口,其中包含用于访问数据库的方法。示例代码: ``` @Dao interface UserDao { @Query("SELECT * FROM users") fun getAllUsers(): List<User> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertUser(user: User) @Delete fun deleteUser(user: User) } ``` 4. 创建数据库:创建一个继承自RoomDatabase的抽象类,并在其中声明用于访问DAO的抽象方法。示例代码: ``` @Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } ``` 5. 初始化数据库:在应用程序的入口点处初始化数据库,并使用它来获取DAO实例。示例代码: ``` val db = Room.databaseBuilder( applicationContext, AppDatabase::class.java, "database-name" ).build() val userDao = db.userDao() ``` 6. 使用DAO:使用DAO中定义的方法来访问数据库。示例代码: ``` val allUsers = userDao.getAllUsers() userDao.insertUser(user) userDao.deleteUser(user) ``` 这些是使用Android Room数据库的基本步骤。当然,还有许多其他的高级用法和配置选项,可以根据具体需求进行学习和使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值