Android 使用 sqlcipher 加密数据库

Posted 福州-司马懿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 使用 sqlcipher 加密数据库相关的知识,希望对你有一定的参考价值。

SQLite 支持的数据类型

sqlite 支持 5 种数据类型

  • null,当某个项未被赋值时,类型为空;赋值后,类型即为建表时为该列指定的类型了。
    但由于SQLite采用的是动态数据类型,会根据存入值自动判断,因此在创建表的时候,字段类型是允许设为null的。但是在首次存入值之后,会根据该值修改实际类型。
  • integer,整型(布尔值会被整型,true->1, false->0)
  • float,浮点型
  • string,字符串类型
  • blob,binary large object,是用来存储二进制的大的对象的字段类型(实际中很少用到)

为什么要加密数据库

对于 Root 过的安装手机,可以随意访问 /data/data/<package_name>/databases 目录下的任意文件,在这里就可以查看到数据库中存储的所有数据。对于一般数据没啥问题,但如果涉及到一些账号密码,或者是聊天内容的时候,程序显然就面临着严重的安全漏洞和隐患了,因此这时候,对数据库进行加密就显得尤为重要。

sqlcipher

官网

greendao 支持数据库加密官网:http://greenrobot.org/greendao/documentation/database-encryption/

SQLCipher 官网:https://www.zetetic.net/sqlcipher/

sqlcipher的特性

  • 开源且继承自 sqlite
  • 透明,使用 256位 AES 加密
  • 防篡改设计
  • 跨平台且零配置

依赖

android 上添加 SQLCipher 依赖:https://www.zetetic.net/sqlcipher/sqlcipher-for-android/

在 build.gradle 中的 dependencies 里添加

dependencies 
    //数据库加密
    implementation 'net.zetetic:android-database-sqlcipher:4.2.0'

使用方式

sqlcipher 在使用上和 android 自带的数据库并没有什么区别,唯一的区别就是打开的时候,需要提供密码。如果密码传 null,则等价于数据库不加密

引用 sqlcipher 类

import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteException;

打开数据库

sqlcipher 是对整个数据库进行加密,open时进行解密,所以应尽量减少打开的次数。

/**
* 初始化并打开数据库
 * @param context 上下文
 * @param dbName 数据库名称(不用加后缀)
 * @param password 加密数据库密码(密码为null表示不加密)
 */
public void open(Context context, String dbName, String password) 
    this.context = context;
    this.dbName = dbName + ".db";
    // 数据库会存放在 /data/data/<包名>/databases/ 目录下面
    File dbFile = context.getDatabasePath(this.dbName);
    File dir = dbFile.getParentFile();
    //如果目录不存在,则递归创建
    if (!dir.exists()) dir.mkdirs();
    
    //加载用于加密的so库
    SQLiteDatabase.loadLibs(context);
    //打开或者创建数据库,通过密码对数据库进行解密,如果password为null,则等价于原生的未加密的数据库
    db = SQLiteDatabase.openOrCreateDatabase(dbFile, password, null);

关闭数据库

    /**
     * 关闭数据库
     */
    public void close() 
    	//仅在数据库已创建,且已打开的情况下,关闭数据库
        if(db != null && db.isOpen()) db.close();
    

执行SQL语句

/**
 * 执行脚本(仅在error非空的时候,返回结果才有效)
 *
 * @param sql 待执行的SQL脚本
 * @return 执行成功返回true,否则返回false
 */
public boolean execSQL(String sql) 
	error = null;
    try 
        //如果有多条相关的语句同时执行,则最好加上事务,以便出错时能回滚到初始状态
	 // db.beginTransaction();
        db.execSQL(sql);
	 // db.endTransaction();
        return true;
     catch (SQLiteException e) 
        e.printStackTrace();
        error = e.toString();
        return false;
    

查询

/**
* 查询数据,"colName":[],"type":[],"value":[[],[]](仅在error非空的时候,返回结果才有效)
 */
public JSONObject queryToJson(String sql) 
    error = null;
    Cursor cursor = null;
    JSONObject jsonObject = new JSONObject();
    try 
        cursor = db.rawQuery(sql, null);
        //判断游标是否为空
        //cursor的初始位置是从下标为-1的地方开始,一定要先moveFirst才能使用
        if (cursor != null && cursor.moveToFirst()) 
            int columnCount = cursor.getColumnCount();
            if (columnCount > 0) 
            	//列名
                JSONArray colNames = new JSONArray();
                //每一列的类型
                JSONArray typeNames = new JSONArray();
                //列的值
                JSONArray values = new JSONArray();

                for (int i = 0; i < columnCount; i++) 
                    int type = cursor.getType(i);
                    String name = cursor.getColumnName(i);
                    colNames.put(name);
                    switch (type) 
                        case Cursor.FIELD_TYPE_NULL:
                        	//通常情况下列的类型不为空。但由于SQLite采用的是动态数据类型,会根据存入值自动判断,因此在创建表的时候,字段类型是允许设为null的
                            typeNames.put("NULL");
                            break;
                        case Cursor.FIELD_TYPE_INTEGER:
                            typeNames.put("INTEGER");
                            break;
                        case Cursor.FIELD_TYPE_FLOAT:
                            typeNames.put("FLOAT");
                            break;
                        case Cursor.FIELD_TYPE_STRING:
                            typeNames.put("STRING");
                            break;
                        case Cursor.FIELD_TYPE_BLOB:
                            typeNames.put("BLOB");
                            break;
                    
                
                for (; !cursor.isAfterLast(); cursor.moveToNext()) 
                    JSONArray array = new JSONArray();
                    for (int i = 0; i < columnCount; i++) 
                        int type = cursor.getType(i);
                        try 
                            switch (type) 
                                case Cursor.FIELD_TYPE_NULL:
                                    //当值为空时,走的是这里
                                    array.put(null);
                                    break;
                                case Cursor.FIELD_TYPE_INTEGER:
                                    array.put(cursor.getInt(i));
                                    break;
                                case Cursor.FIELD_TYPE_FLOAT:
                                    array.put(cursor.getFloat(i));
                                    break;
                                case Cursor.FIELD_TYPE_STRING:
                                    array.put(cursor.getString(i));
                                    break;
                                case Cursor.FIELD_TYPE_BLOB:
                                    //json不能存byte[],因此只能用bcd编码或base64编码来存
                                    array.put(BcdUtil.toString(cursor.getBlob(i)));
                                    break;
                            
                         catch (JSONException e) 
                            e.printStackTrace();
                        
                    
                    values.put(array);
                

                jsonObject.put("colName", colNames);
                jsonObject.put("type", typeNames);
                jsonObject.put("value", values);
             else 
                // 表的列为空
                error = context.getString(R.string.table_column_empty);
            
         else 
            //获取游标失败
            error = context.getString(R.string.get_cursor_fail);
        
     catch (SQLiteException e) 
        e.printStackTrace();
        error = e.getMessage();
     catch (JSONException e) 
        e.printStackTrace();
        error = e.getMessage();
     finally 
        if (cursor != null) 
            cursor.close();
        
    
    return jsonObject;

测试语句

public void testDatabase() 
	String createTable = "CREATE TABLE IF NOT EXISTS person ( id integer primary key autoincrement, name varchar(20), age integer, other null )";
    String insertInto = "INSERT INTO person ( name, age, other ) VALUES ( '%s', %d, %s )";
	String insertInto2 = "INSERT INTO person ( name, age, other ) VALUES ( 'bird', NULL, NULL )";
	String insertInto3 = "INSERT INTO person ( name, age, other ) VALUES ( NULL, 5, NULL )";
	String selectFrom = "SELECT * FROM person";

	//sqlite 不支持 drop column,会抛出异常
	String alertTable = "ALTER TABLE person DROP COLUMN age";
	String updateItem = "UPDATE person set age = 99 where upper(name) = upper('bob')";
	String dropTable = "DROP TABLE IF EXISTS person";

	String[] names = "Tina", "Bob", "Tom";
	int[] ages = 18, 30, 24;
	Object[] others = "'other'", 11, true;

	dialog = DialogUtil.show(activity, data, "", null);

	new Thread(()->
	//执行结果
	boolean sqlResult;
	JSONObject sqlSelect = null;

	sb = new StringBuilder();

	SQLiteHelper sqLiteHelper = SQLiteHelper.getInstance(activity, "test", null);
	sqLiteHelper.execSQL(createTable);
	sqlResult = sqLiteHelper.execSQL(createTable);
	StringUtil.appendLine(sb, "创建表:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "创建后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());

	for (int i = 0; i < names.length; i++) 
		sqlResult = sqLiteHelper.execSQL(String.format(insertInto, names[i], ages[i], others[i]));
	
	StringUtil.appendLine(sb, "插入数据:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "插入后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());

	sqlResult = sqLiteHelper.execSQL(insertInto2);
	StringUtil.appendLine(sb, "插入name=NULL的数据:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "插入name=NULL后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());

	sqlResult = sqLiteHelper.execSQL(insertInto3);
	StringUtil.appendLine(sb, "插入age=NULL的数据:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "插入age=NULL后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());

	sqlResult = sqLiteHelper.execSQL(updateItem);
	StringUtil.appendLine(sb, "更新数据:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "跟新数据后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());

	sqlResult = sqLiteHelper.execSQL(alertTable);
	StringUtil.appendLine(sb, "修改表结构:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "修改表结构后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());

	sqlResult = sqLiteHelper.execSQL(dropTable);
	StringUtil.appendLine(sb, "删除表:" + sqlResult);
	setDialogMessage(activity, sb.toString());
	sqlSelect = sqLiteHelper.queryToJson(selectFrom);
	StringUtil.appendLine(sb, "删除表后的结果:" + sqlSelect);
	setDialogMessage(activity, sb.toString());
	).start();

测试结果

从测试结果中可以看出几点

  • sqlite 不支持 drop
  • 如果值为空,则值的类型为 null
  • 创建表时,可以设置列的类型为null,sqlite会根据第一个存进去的值动态修改列的类型
  • sqlite允许存储和类类型不一致的值

将代码片段 Object[] others = "'other'", 11, true; 替换为 Object[] others = 11, "'other'", true; 后,卸载程序,然后再次安装运行可以发现,类型从 string 变为 integer,这说明,如果在创建表的时候将类型设为 null,那么它会根据第一个存进去的值,来修改列的类型。但不会改变 .schema 命令打印出的创建表的语句,对于存入的非列类型的值也不会报错。

查看数据库

https://blog.csdn.net/chy555chy/article/details/101511609

以上是关于Android 使用 sqlcipher 加密数据库的主要内容,如果未能解决你的问题,请参考以下文章

Android数据库加密与破解(Xposed hook SQLCipher 密码)

Android 使用 sqlcipher 加密数据库

正确使用sqlcipher for Android

ORMLite整合SQLCipher

ANDROID:SQLCipher 中的 .jar 文件在哪里

SQLCipher加密数据库打开工具