利用 Sqlite 完成数据持久化

读取配置生成数据库、利用BAT加密数据库

Posted by SWZ on April 27, 2020

简介

SQLite 是一个软件库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎。SQLite 源代码不受版权限制。

这里再说明下,SQLite3是SQLite的第三个主要版本,避免大家突然看到Sqlite3不知道什么意思。


Sqlite3的加/解密

首先你得先把Sqlite3的库给下载到本地,项目里引用该库文件,然后就能调用它的Api啦。

加密说明

  • sqlite3_key 是输入密钥,如果数据库已加密必须先执行此函数并输入正确密钥才能进行操作,如果数据库没有加密,执行此函数后进行数据库操作反而会出现 “此数据库已加密或不是一个数据库文件” 的错误。

  • int sqlite3_key(sqlite3 *db, const void *pKey, int nKey),db 是指定数据库,pKey 是密钥,nKey 是密钥长度。例:sqlite3_key( db, "abc", 3)

  • sqlite3_rekey 是变更密钥或给没有加密的数据库添加密钥或清空密钥,变更密钥或清空密钥前必须先正确执行 sqlite3_key。在正确执行 sqlite3_rekey 之后在 sqlite3_close 关闭数据库之前可以正常操作数据库,不需要再执行 sqlite3_key

  • int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey),参数同上。

  • 清空密钥为 sqlite3_rekey(db, NULL, 0)

解密说明

  • 在调用 sqlite3_open() 函数打开数据库后,要调用 sqlite3_key() 函数为数据库设置密码。

  • 如果数据库之前有密码,则调用 sqlite3_key() 函数设置正确密码才能正常工作。

  • 如果一个数据库之前没有密码,且已经有数据,则不能再为其设置密码。

  • 如果要修改密码,则需要在第一步操作后,调用 sqlite3_rekey() 函数设置新的密码。

  • 设置了密码的 SQLite 数据库,无法使用第三方工具直接打开,必须要输入密码。


Windows下如何利用BAT加解密Sqlite

这里需要下载sqlcipher工具,网上可以找一个叫sqlcipher-3.0.1-windows的就可以了。

基本原理说明

现在有三个数据库【数据库名称随意】 origin.db(这个是源数据库,也就是你想要加密的数据库。) encrypted.db (这个加密后的数据库) plaintext.db(这个是解密后的数据库) 加密:把源数据库里面的所有数据,复制到一个已经加密的数据库encrypted.db中,encrypted.db这个数据库是运行命令的时候生成的,并且是加密的。 解密:把加过密的数据库encrypted.db里面所有的数据拷贝到plaintext.db数据库中,plaintext.db是运行命令的时候生成的,并且没有密码。

具体操作

  1. 解压下载好的工具。 bin目录里面的结构:

  2. 打开命令行【win+R ->cmd】 使用命令打开工具所在的文件夹,我的文件夹是:E:\360安全浏览器下载\AAA\sqlcipher- windows\bin

    输入命令1:sqlcipher-shell64.exe origin.db 【完成之后按Enter键】 输入命令2:ATTACH DATABASE ‘encrypted.db’ AS encrypted KEY ‘thisiskey’; 【Enter】 输入命令3:SELECT sqlcipher_export(‘encrypted’); 【Enter】 输入命令4:DETACH DATABASE encrypted; 【Enter】

    上述命令完成之后,就完成了一个加密的过程。接下来解释一下上面的命令: 命令1,使用工具sqlcipher-shell64.exe ,空格之后,后面紧跟着的是你要操作的源数据库。 命令2,新建一个数据库文件encrypted.db,并打开连接。这里encrypted.db是一个加密的db文件 encrypted是该数据库的别名,thiskey是新建的这个数据库文件的密码 命令3,调用了工具的一个接口,该接口的作用就是把上面操作的源数据库origin.db里面的所有数据拷贝到encryted.db当中。 命令4,断开连接

    解密:把加密的数据库encryted.db里面所有的数据拷贝到未加密的数据库plaintext.db中。 使用window命令行打开到bin目录文件夹 输入命令1,sqlcipher-shell64.exe encryted.db 【Enter】 输入命令2,PRAGMA key = ‘thisiskey’; 输入命令3,ATTACH DATABASE ‘plaintext.db’ AS plaintext KEY ‘’; 输入命令4,SELECT sqlcipher_export(‘plaintext’); 输入命令5,DETACH DATABASE plaintext;

    命令解释: 命令1,要操作加密的数据库 命令2,输入加密数据库的密码 命令3,新建了一个数据库plaintext.db, 密码是空【两个单引号中间是空】 命令4,拷贝加密数据库中所有的数据到plaintext.db中 命令5,断开连接 上述命令完成之后就完成了一个解密的过程。 使用sqlite打开plaintext.db发现里面数据和源数据库中的文件一致


DB创建自动化

using Core.Utils;
using GameSystem.Database;
using GameSystem.Proxy;
using GameSystem.VO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;

public class CreateDB : EditorWindow
{
    private static EditorWindow window;
    private string dbName =
#if UNITY_ANDROID
    "assets_android.db";
#elif UNITY_IOS
    "assets_ios.db";
#endif
    private string dbKey = Key; //"76A8EF9C-2052-4D3A-8A30-DCAACC5BEFF6"
    private bool need_encrypt = true;

    [MenuItem("本地化工具/Window/CreateDB")]
    public static void CreateLocalDB()
    {
        window = GetWindow<CreateDB>("创建本地数据库");
        window.Show();
    }

    void OnGUI()
    {
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("请输入DB的名字:", GUILayout.Width(120f));
        dbName = GUILayout.TextField(dbName);
        if (GUILayout.Button("浏览"))
        {
            string fileFullPath = UnityEditor.EditorUtility.OpenFilePanel("Open File Dialog", UnityEngine.Application.streamingAssetsPath, "db");
            if (!string.IsNullOrEmpty(fileFullPath))
            {
                string[] pathArray = fileFullPath.Split('/');
                dbName = pathArray[pathArray.Length - 1];
            }
        }
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("请设置DB的密码:", GUILayout.Width(120f));
        dbKey = GUILayout.TextField(dbKey);
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("是否需要加密数据库:", GUILayout.Width(120f));
        need_encrypt = EditorGUILayout.Toggle(need_encrypt);
        EditorGUILayout.EndHorizontal();

        if (GUILayout.Button("创建"))
        {
            window.Close();
            CreateDBProcess();
        }
    }

    private void CreateDBProcess()
    {
        var db = new SqliteDatabase(dbName, dbName, Application.streamingAssetsPath);

        db.Open(); //此时路径下自动会产生一个db文件
        CreateTable<LocalAssetsMasterVO>(db, typeof(LocalAssetsMasterVO).Name);
        //db.Close();

        int count = 1;
        foreach (var item in LoadProxy.LocalAbFileNamesList)
        {
            EditorUtility.DisplayProgressBar("数据库生成", string.Format("生成中:{0}/{1}", count, LoadProxy.LocalAbFileNamesList.Count), (float)count++ / LoadProxy.LocalAbFileNamesList.Count);
            var query = new StringBuilder("replace into " + typeof(LocalAssetsMasterVO).Name + " (");
            query.Append(CreateFieldNames(typeof(LocalAssetsMasterVO)));
            query.Append(") values ");
            var valuesStr = CreateFieldValues(new LocalAssetsMasterVO() { code = item, version = "default_version" });
            db.ExecuteNonQuery(query.ToString() + valuesStr);
        }
        EditorUtility.ClearProgressBar();

        if (need_encrypt)
        {
            string batRootPath = Path.Combine(Application.dataPath, @"Editor\Tools\CreateDB\bat");
            string batName = "encrypt_db.bat";
            string batPath = Path.Combine(batRootPath, batName);

            //可传递参数给批处理
            System.Diagnostics.Process p = new System.Diagnostics.Process();
            //第二个参数为传入的参数,string类型以空格分隔各个参数
            //把需要加密的数据库名字去除后缀后的名字传递进去
            System.Diagnostics.ProcessStartInfo pi = new System.Diagnostics.ProcessStartInfo(
                batPath, 
                string.Format("{0} {1}", Path.GetFileNameWithoutExtension(dbName), dbKey)
                );
            pi.UseShellExecute = false;
            pi.RedirectStandardOutput = true;
            p.StartInfo = pi;
            p.Start();
            p.WaitForExit();
        }

        AssetDatabase.Refresh();
        EditorUtility.DisplayDialog("", "创建完毕", "OK");
    }

-------------------------------------------------------------------------------------
#region SupportProprites
    private static string streamingPath = Application.streamingAssetsPath;
    private static GameDatabase m_database;
    private Dictionary<Type, MasterCache> m_masterMaps;
    private static MasterProxy m_masterProxy;
    static readonly byte[] xorkey = { 0x1E, 0x9F, 0x83, 0xEA, 0xB0, 0xE2, 0xDB, 0x95 };
    static readonly byte[] src = {
      0x29,0xA9,0xC2,0xD2,0xF5,0xA4,0xE2,0xD6,0x33,0xAD,0xB3,0xDF,0x82,0xCF,0xEF,0xD1,
      0x2D,0xDE,0xAE,0xD2,0xF1,0xD1,0xEB,0xB8,0x5A,0xDC,0xC2,0xAB,0xF3,0xA1,0xEE,0xD7,
      0x5B,0xD9,0xC5,0xDC
    };
    public static string Key { get { return Encoding.ASCII.GetString(Utils.XorBytes(src, xorkey)); } }

    static private readonly string DefaultApiDomainName = "game-ntp-dev";

    private const string SharedCacheFileName = "assets_android";

    public static string ApiDomainName
    {
        get
        {
            if (PlayerPrefs.HasKey("ApiDomainName"))
            {
                return PlayerPrefs.GetString("ApiDomainName");
            }
            PlayerPrefs.SetString("ApiDomainName", DefaultApiDomainName);
            return DefaultApiDomainName;
        }
    }
#endregion

#region SupportMethod
    private static string CreateFieldNames(Type type)
    {
        int index = 0;
        var fields = type.GetFields();
        var ret = new StringBuilder();
        foreach (var field in fields)
        {
            if (index != 0)
            {
                ret.Append(", ");
            }
            ret.Append(field.Name);
            index++;
        }
        return ret.ToString();
    }

    private static string CreateFieldValues<T>(T record)
    {
        int index = 0;
        var values = new StringBuilder("(");
        var fields = typeof(T).GetFields();
        foreach (var field in fields)
        {
            if (index != 0)
            {
                values.Append(", ");
            }
            if (field.FieldType == typeof(string))
            {
                var str = (string)field.GetValue(record);
                values.Append(String.Format("'{0}'", WWW.EscapeURL(str)));
            }
            else if (
               field.FieldType == typeof(float)
            || field.FieldType == typeof(double)
            || field.FieldType == typeof(int)
            || field.FieldType == typeof(long)
            )
            {
                values.Append(field.GetValue(record));
            }
            else if (field.FieldType == typeof(bool))
            {
                var value = Convert.ToInt32(field.GetValue(record));
                values.Append(value);
            }
            else
            {
                int value = (int)field.GetValue(record);
                values.Append(value);
            }
            index++;
        }
        values.Append(")");
        return values.ToString();
    }

    private static void CreateTable<T>(SqliteDatabase db, string tableName)
    {
        var type = typeof(T);
        var query = new StringBuilder("create table if not exists " + tableName + " (");
        int index = 0;
        Debug.Assert(type.GetFields().Length > 0);
        foreach (var field in type.GetFields())
        {
            if (index != 0)
            {
                query.Append(", ");
            }
            string t = "";
            if (field.FieldType == typeof(string))
            {
                t = "TEXT";
            }
            else if (field.FieldType == typeof(float))
            {
                t = "REAL";
            }
            else
            {
                t = "INTEGER";
            }
            query.Append(field.Name + " " + t);
            index++;
        }
        query.Append(")");
        db.ExecuteNonQuery(query.ToString());
    }
#endregion
}

BAT脚本

cd /D %~dp0
set db_name=%1
set db_key=%2
(
echo ATTACH DATABASE '%db_name%' AS encrypted KEY '%db_key%';
echo SELECT sqlcipher_export^('encrypted'^);
)>encrypt_query.sql
sqlcipher-shell64.exe D:\Prince_Of_Tennis\ver_current_chs\dlive-client\Dlive\Assets\StreamingAssets\%db_name%.db < encrypt_query.sql
move D:\Prince_Of_Tennis\ver_current_chs\dlive-client\Dlive\Assets\Editor\Tools\CreateDB\bat\%db_name% D:\Prince_Of_Tennis\ver_current_chs\dlive-client\Dlive\Assets\StreamingAssets\

SQL语句文件,利用输入重定向跟在加密命令之后

ATTACH DATABASE 'test_333_444' AS encrypted KEY '789';
SELECT sqlcipher_export('encrypted');

参考