SQLite以其单文件、零配置的特性被广泛应用于桌面软件、移动客户端和嵌入式设备。但默认情况下,SQLite数据库文件是完全明文存储的,把.db文件拖进十六进制编辑器,建表语句、用户名、手机号一目了然。如果项目涉及用户隐私数据或商业敏感信息,就必须对数据库文件本身做加密。SQLite官方给出的答案是SEE(SQLite Encryption Extension),即SQLite加密扩展。这篇文章完整讲解SEE的工作原理、编译集成方法和实际使用中的注意事项。

一、SEE是什么,它和SQLCipher有什么区别
SEE是SQLite作者D. Richard Hipp所在的Hipp, Wyrick & Company公司提供的商业加密扩展,需要付费购买(一次性授权,价格数千美元级别),购买后获得定制源码或预编译的加密版DLL。SEE的核心机制是替换SQLite底层读写页面的VFS(虚拟文件系统)层,在数据页写入磁盘前加密、读出磁盘后解密,对上层SQL逻辑完全透明。开发者不需要修改任何业务SQL,加密解密由扩展自动完成。
SEE支持多种加密算法,包括AES-256-CCM(默认)、AES-128-CBC、ChaCha20-Poly1305等,通过编译宏选择。它的一大优势是官方维护,与SQLite主线版本同步更新,稳定性有保障。而社区里更流行的SQLCipher是Zetetic公司开发的开源方案(社区版采用BSD许可),同样采用页级加密思路,默认使用AES-256-CBC,配合PBKDF2做密钥派生。两者在使用接口上非常相似,都是通过PRAGMA key设置密钥,但SEE是官方商业产品,SQLCipher是开源实现。如果你的项目预算充足、需要官方技术支持,选SEE;如果追求零成本和开源生态,SQLCipher是不错的替代品。
需要注意的一点是,SEE加密后的数据库文件与普通SQLite文件不兼容,普通sqlite3命令行工具打不开加密库,必须使用编译了SEE的版本打开。这一点在调试和运维环节容易被忽视,建议同时准备一个带加密能力的命令行工具。
二、SEE的编译与集成方式
购买SEE后官方会提供两种交付形式:一是完整替换版源码,二是扩展库形式。第一种方式最常用,直接用SEE源码替换标准SQLite的sqlite3.c amalgamation文件,重新编译即可。编译时通过宏定义选择算法,例如在Windows的MSVC环境下:
cl /DSQLITE_HAS_CODEC=1 /DSQLITE_TEMPORARY_FILES=0 sqlite3.c /link /dll /out:sqlite3.dll cl shell.c sqlite3.c /DSQLITE_HAS_CODEC=1 /Fe:sqlite3.exe
上面的命令会同时生成带加密能力的DLL和命令行工具。SQLITE_HAS_CODEC是启用加密的开关宏,SEE的定制源码里包含了该宏对应的实现。如果选择AES-128算法,再加一个/DSQLITE_CODEC_TYPE=AES128即可,默认不定义该宏时使用AES-256-CCM。Linux下用gcc编译同理:
gcc -DSQLITE_HAS_CODEC=1 -DSQLITE_CODEC_TYPE=CHACHA20 \
shell.c sqlite3.c -o sqlite3 -lpthread -ldl第二种方式是扩展库集成。SEE可以作为sqlite3ext.h体系下的运行时扩展使用,程序启动时调用sqlite3_load_extension加载,好处是不用改动现有的SQLite编译链,缺点是主程序必须保留对扩展加载的支持(SQLITE_OMIT_LOAD_EXTENSION不能被定义)。对C/C++项目来说,推荐直接用替换源码方式,链接更简单,也不存在扩展文件被删导致功能失效的风险。
对于.NET、Java、Python等语言,需要使用绑定库提供的加密构建版本。以System.Data.SQLite为例,官方提供了带SEE的付费版本(SQLiteCrypt类支持),Python的pysqlite3-binary则可以对接SQLCipher。集成前务必确认绑定库底层链接的是编译了SEE的sqlite3,否则PRAGMA key会被静默忽略,程序看似正常但文件其实是明文的,这是最危险的坑。
三、密钥设置与代码实战
SEE的使用核心就两步:打开数据库后立即设置密钥,然后正常执行SQL。设置密钥有两种PRAGMA写法。PRAGMA key='密码字符串'接受UTF-8字符串,PRAGMA hexkey='十六进制串'接受原始密钥字节。推荐使用hexkey,因为字符串密钥存在空字符截断、编码不一致等问题,而十六进制密钥可以精确控制密钥字节,适合程序生成的随机密钥。
下面是一个完整的C语言示例,演示打开加密库、首次创建加密库和验证密钥的流程:
#include <stdio.h>
#include "sqlite3.h"
int main(void) {
sqlite3 *db;
int rc = sqlite3_open("app.db", &db);
if (rc != SQLITE_OK) return 1;
/* 打开后立即设置密钥,必须在任何其他操作之前执行 */
rc = sqlite3_exec(db, "PRAGMA hexkey=\"6b6579736f6d657468696e67\";", 0, 0, 0);
if (rc != SQLITE_OK) {
fprintf(stderr, "set key failed\n");
return 1;
}
/* 首次打开新库时设置salt和rekey加密算法,可选 */
sqlite3_exec(db, "PRAGMA rekey=\"newpass\";", 0, 0, 0);
/* 验证密钥是否正确:读取一个系统表,失败说明密钥错误或文件损坏 */
rc = sqlite3_exec(db, "SELECT count(*) FROM sqlite_master;", 0, 0, 0);
if (rc != SQLITE_OK) {
fprintf(stderr, "wrong password or not an encrypted db\n");
}
/* 之后就是正常的业务SQL */
sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS user(id INTEGER PRIMARY KEY, name TEXT);", 0, 0, 0);
sqlite3_close(db);
return 0;
}有几个关键点必须强调。第一,PRAGMA key或PRAGMA hexkey必须是打开数据库后的第一条语句,之前不能有任何读写操作。第二,判断密钥是否正确,不能只看设置PRAGMA本身的返回值,必须实际执行一次查询,因为SEE采用延迟解密,密钥错误只有在真正读到数据页时才会报SQLITE_NOTADB错误。第三,修改密钥用PRAGMA rekey,它会重写整个数据库文件,大库操作耗时明显,建议在空闲时段执行。
命令行工具同样支持加密,用法是打开数据库后先输key:
$ ./sqlite3 app.db sqlite> PRAGMA key='mypassword'; sqlite> .tables user
导出明文备份也有一个技巧:先attach一个普通数据库,再把数据复制过去:
ATTACH DATABASE 'plain.db' AS plain KEY ''; SELECT sql FROM sqlite_master; -- 逐表执行建表语句后 INSERT INTO plain.user SELECT * FROM main.user; DETACH DATABASE plain;
其中KEY ''空密钥表示plain.db不加密,这一招常用于数据迁移和故障排查。
四、密钥管理与常见问题
加密的安全性最终取决于密钥管理。密钥绝不能硬编码在代码里,反编译工具一跑就暴露。常见的做法有三种:一是Windows下用DPAPI(CryptProtectData)绑定当前用户或机器加密存储密钥;二是macOS和iOS用Keychain;三是Android用Keystore系统。思路都是把密钥交给操作系统级的安全设施保管,程序运行时取出、用完即丢。另外要注意,密钥一旦丢失,加密数据库无法恢复,SEE没有后门,这一点务必在产品层面设计好密钥备份机制。
常见报错方面,file is not a database通常有两个原因:密钥错误,或者数据库文件本来就不是SEE加密的(比如是明文库或SQLCipher加密的库)。no such function或PRAGMA被忽略,基本可以断定编译时没有启用加密宏,检查SQLITE_HAS_CODEC是否定义。还有一类隐蔽问题是journal和WAL文件:SEE对主库、journal、WAL文件统一加密,但混用普通SQLite和SEE版本操作同一个库会留下明文残留页,切换加密方案前最好完整VACUUM一次并删除旧的-journal文件。
性能上,加密会带来约5%到10%的吞吐损耗,页级加密对随机读写影响很小,SEE的CCM模式还支持硬件AES加速,实测在支持AES-NI的CPU上损耗可以忽略。总体来说,只要集成步骤正确、密钥管理得当,SEE是目前与SQLite结合最紧密、维护最可靠的整库加密方案,值得对数据安全有要求的项目采用。
SQLite SEE数据库加密SQLite Encryption Extension修改时间:2026-09-07 04:56:39