SQLCipher#001#SQLCipher的下载、编译、使用和测试程序 -- Linux版

我们知道SQLite不对数据加密,如需加密数据,通常采用SQLCipher方案。

SQLCipher在SQLite基础上,基于AES-256算法对数据库文件整体加密,包括数据页和元数据。通过SQLite的hooks功能注入加密逻辑,实现数据写入磁盘前加密、读取时自动解密,开发者无需大幅修改现有SQL逻辑即可增强数据安全性。

本文描述SQLCipher下载、编译、安装、命令行、编码开发等简单操作和基础知识。

1/4) 下载源代码

官方网站: https://www.zetetic.net/sqlcipher

代码网站: https://github.com/sqlcipher/sqlcipher

下载网址: https://github.com/sqlcipher/sqlcipher/releases

演示版本: https://github.com/sqlcipher/sqlcipher/archive/refs/tags/v4.10.0.tar.gz

使用上述链接下载V4.10.0版本后,使用如下命令解压:

1
tar -zxvf sqlcipher-4.10.0.tar.gz 

代码包解压后如下图所示:

2/4) 编译和安装

第一步,使用如下命令安装依赖库libssl-dev:

1
sudo apt install libssl-dev

安装完毕后,我们可以在如下目录看到libcrypto和libssl的动态库和静态库:

第二步,进入SQLCipher解压后的代码目录,使用如下命令配置编译选项:

1
2
3
./configure --with-tempstore=yes --disable-tcl --prefix="/home/mancode/apps/sqlcipher/4.10.0" \
CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_EXTRA_INIT=sqlcipher_extra_init -DSQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown" \
LDFLAGS="-lcrypto"

该命令执行完毕之后,如下图所示:

第三步,编译

使用如下命令,进行编译:

1
make

该命令执行完毕之后,如下图所示:

第四步,安装

使用如下命令,进行安装:

1
make install

该命令执行完毕之后,如下图所示:

执行安装命令后,我们看看目标目录结构:

将 /home/mancode/apps/sqlcipher/4.10.0/bin 加入到系统PATH中,方便调用sqlite3命令。

3/4) 使用sqlite3工具演示SQLCipher的使用方法

第一步,启动命令,创建一个加密数据库

使用如下命令,test.db就是我们欲创建的加密数据库:

1
sqlite3 test.db

启动成功如下图:

注意:到此test.db还不是加密数据库,在下一步设置加密密码之后才转为加密数据库。

第二步,设置密钥,加密数据库

使用命令:

1
PRAGMA key = '123456789';

该命令执行如下图所示:

第三步,创建表和插入数据

使用命令:

1
2
3
create table msg(id interger PRIMARY KEY, name text NOT NULL, age interger, school text);

insert into msg(id, name, age, school) values(20250101, 'zhangsan', 18, 'tsinghua');

该命令执行如下图所示:

第四步,验证:在不输入密码的情况下读取数据

按照预期,在不输入密码的情况下打开数据库,读取数据肯定要报错,我们使用如下方法验证:

从上图可见,如我们的预期一样,在没有输入数据库密码时,获取相关元数据时,报Error: file is not a database的错误。

然后我们再按上述方法输入数据库密码,看下数据库的表现:

从上图可见,如我们的预期一样,在输入数据库密码后,可以正常读取元数据以及用户数据。

4/4) 使用SQLCipher库编码开发

如下测试代码,完成打开数据库、设置密钥、创建数据表、插入数据、检索数据等基本操作。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sqlite3.h>
#include <string>

#define TEST_DB  "test.db"
#define TEST_KEY "123456789"

sqlite3* open_database(const char *db_path, const char *key) 
{
    sqlite3 *db = NULL;
    int rc = sqlite3_open(db_path, &db);

    if (rc != SQLITE_OK) 
    {
        fprintf(stderr, "无法打开数据库 %s: %s\n", db_path, sqlite3_errmsg(db));
        sqlite3_close(db);
        return NULL;
    }

    rc = sqlite3_key(db, key, strlen(key));
    if (rc != SQLITE_OK) 
    {
        fprintf(stderr, "设置密钥失败: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return NULL;
    }

    return db;
}

void close_database(sqlite3 *db) 
{
    if (db) 
    {
        sqlite3_close(db);
    }
}

int execute_sql(sqlite3 *db, const char *sql) 
{
    char *err_msg = NULL;
    int rc = sqlite3_exec(db, sql, NULL, NULL, &err_msg);
    
    if (rc != SQLITE_OK) 
    {
        fprintf(stderr, "SQL执行失败: %s\nSQL语句: %s\n", err_msg, sql);
        sqlite3_free(err_msg);
    }
    
    return rc;
}

int test_basic_operations() 
{
    sqlite3 * db = open_database(TEST_DB, TEST_KEY);
    if (!db) 
    {
        fprintf(stderr, "无法打开数据库\n");
        return 0;
    }

    // 创建表
    const char *create_table_sql = "CREATE TABLE IF NOT EXISTS msg ("
                                   "id interger PRIMARY KEY, name text NOT NULL, age interger, school text)";

    if (execute_sql(db, create_table_sql) != SQLITE_OK) 
    {
        fprintf(stderr, "创建表失败\n");
        close_database(db);
        return 0;
    }

    // 插入数据
    const char *insert_sql = "INSERT INTO msg(id, name, age, school) VALUES(20250102, 'lishi', 19, 'pku')";
    if (execute_sql(db, insert_sql) != SQLITE_OK) 
    {
        fprintf(stderr, "插入数据失败\n");
        close_database(db);
        return 0;
    }

    // 查询数据
    sqlite3_stmt *stmt;
    const char *query_sql = "SELECT id, name, age, school FROM msg";
    if (sqlite3_prepare_v2(db, query_sql, -1, &stmt, NULL) != SQLITE_OK) 
    {
        fprintf(stderr, "查询准备失败: %s\n", sqlite3_errmsg(db));
        close_database(db);
        return 0;
    }

    while (sqlite3_step(stmt) == SQLITE_ROW) 
    {
        int id = sqlite3_column_int(stmt, 0);
        const char *name = (const char *)sqlite3_column_text(stmt, 1);
        int age = sqlite3_column_int(stmt, 2);
        const char *school = (const char *)sqlite3_column_text(stmt, 3);

        printf("查询结果: ID=%d, Name=%s, Age=%d, School=%s\n", id, name, age, school);
    }

    sqlite3_finalize(stmt);

    // 关闭数据库
    close_database(db);

    return 1;
}

int main() 
{
    test_basic_operations();

    return 0;
}

然后使用如下简单的Makefile进行编译:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SQLCIPHER_DIR:=/home/mancode/apps/sqlcipher/4.10.0
SQLCIPHER_INC:=-I${SQLCIPHER_DIR}/include
SQLCIPHER_LIB:=${SQLCIPHER_DIR}/lib/libsqlite3.a

OPENSSL_DIR:=/lib/x86_64-linux-gnu
OPENSSL_INC:=-I/usr/incude
OPENSSL_LIB:=${OPENSSL_DIR}/libcrypto.a ${OPENSSL_DIR}/libssl.a

all:atest

atest:atest.cpp
    g++ -DSQLITE_HAS_CODEC -o atest atest.cpp ${SQLCIPHER_INC} ${OPENSSL_INC} ${SQLCIPHER_LIB} ${OPENSSL_LIB}

clean:
    rm -rf atest

注意:编译参数务必加上 -DSQLITE_HAS_CODEC,否则会报如下错误:

1
error: ‘sqlite3_key’ was not declared in this scope

编译成功之后,我们使用第三章创建的test.db进行测试,结果如下图所示:

至此,我们演示了SQLCipher的下载、编译、安装、命令行、编码开发等基本操作,整体来看开发人员很容易从SQLite迁移到SQLCipher。

使用该SDK的测试代码和工程文件,可参考:https://github.com/sanxiansite/sqlcipher-example/tree/main/example/linux