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/wosanxian/sqlcipher-example/tree/main/example/linux