17、数据库加密基础:SQLite加密扩展(SEE)、自定义加密方案、AES加密实战
说到数据库加密,我脑子里立刻浮现出几年前的一个项目。当时给一家金融公司做嵌入式设备,数据存到SQLite里,客户就问了一句:“如果设备被人拆了,存储芯片直接读,数据安全吗?”
嗯,这个问题问到了点子上。SQLite本身是不加密的,数据库文件就是一个普通文件。谁拿到文件,谁就能用SQLite打开看。所以,加密不是可选项,而是刚需。
17.1 为什么需要数据库加密?
你想想看,嵌入式设备经常暴露在物理攻击下。U盘丢了、设备被拆了、存储卡被拔了……这些场景下,数据库文件直接裸奔。没有加密,就等于把密码本送给别人。
我个人习惯把数据库加密分为两个层面:
- 传输加密:数据在网络中传输时加密(比如TLS)
- 存储加密:数据落盘时加密(我们这章的重点)
存储加密又分两种:一种是SQLite官方提供的加密扩展(SEE),另一种是自己动手实现的自定义加密方案。咱们一个一个说。
核心观点:数据库加密的核心目标是——即使存储介质被物理获取,也无法直接读取数据内容。
17.2 SQLite加密扩展(SEE)
SQLite官方其实提供了一套加密接口,叫SQLite Encryption Extension(SEE)。但说实话,这东西是收费的,而且源代码不公开。我在项目中很少直接用SEE,原因很简单:
- 授权费用不低
- 无法定制算法
- 社区支持有限
不过,SEE的设计思路值得学习。它本质上是在SQLite的pager层(也就是数据页管理那层)加了一个加密钩子。每次写入数据页之前先加密,每次读取数据页之后先解密。
SEE支持的加密算法包括:
| 算法 | 密钥长度 | 说明 |
|---|---|---|
| AES-128 | 128位 | 速度较快,安全性足够 |
| AES-256 | 256位 | 更安全,但计算开销大 |
| RC4 | 可变 | 不推荐,已过时 |
个人建议:如果你预算充足,且不想折腾加密实现,SEE是个省心的选择。但如果你像我一样喜欢掌控底层,那就继续往下看。
17.3 自定义加密方案
大多数嵌入式项目,我选择自己实现加密层。为什么?因为灵活。我可以选择算法、控制密钥管理、甚至针对硬件做优化。
自定义加密方案的核心思路是:在SQLite的VFS(虚拟文件系统)层做手脚。SQLite允许你注册自己的VFS实现,接管所有文件读写操作。我们在读写函数里插入加密/解密逻辑,就能实现透明加密。
基本架构是这样的:
这样做的好处是:应用程序完全无感知。你只需要在打开数据库时传入一个密钥,后续所有读写操作自动加解密。
注意:自定义VFS方案需要你对SQLite的VFS接口非常熟悉。我曾经在一个项目里因为没处理好页对齐问题,导致数据库损坏。嗯,那次教训挺深刻的。
17.4 AES加密实战
好了,理论说完了,咱们直接上代码。我选AES-256-CBC模式,这是目前嵌入式领域最常用的组合。为什么是CBC?因为同样的明文每次加密结果不同,安全性更高。
先看AES加密的核心函数:
#include <stdio.h>
#include <string.h>
#include <openssl/aes.h>
// AES-256-CBC 加密
// 输入:plaintext - 明文数据
// plaintext_len - 明文长度
// key - 32字节密钥
// iv - 16字节初始向量
// 输出:ciphertext - 密文数据
// 返回:加密后的长度(16字节对齐)
int aes_encrypt(const unsigned char *plaintext, int plaintext_len,
const unsigned char *key, const unsigned char *iv,
unsigned char *ciphertext) {
AES_KEY aes_key;
unsigned char iv_copy[16];
int len = 0;
int i;
// 复制IV,因为AES_cbc_encrypt会修改IV
memcpy(iv_copy, iv, 16);
// 设置加密密钥
if (AES_set_encrypt_key(key, 256, &aes_key) != 0) {
return -1;
}
// 计算填充后的长度(PKCS7填充)
int padded_len = ((plaintext_len / 16) + 1) * 16;
unsigned char *padded = (unsigned char *)malloc(padded_len);
memcpy(padded, plaintext, plaintext_len);
// PKCS7填充:填充字节的值等于填充长度
int pad_len = 16 - (plaintext_len % 16);
for (i = plaintext_len; i < padded_len; i++) {
padded[i] = pad_len;
}
// 执行CBC加密
AES_cbc_encrypt(padded, ciphertext, padded_len,
&aes_key, iv_copy, AES_ENCRYPT);
free(padded);
return padded_len;
}
解密函数是对称的:
// AES-256-CBC 解密
int aes_decrypt(const unsigned char *ciphertext, int ciphertext_len,
const unsigned char *key, const unsigned char *iv,
unsigned char *plaintext) {
AES_KEY aes_key;
unsigned char iv_copy[16];
memcpy(iv_copy, iv, 16);
// 设置解密密钥
if (AES_set_decrypt_key(key, 256, &aes_key) != 0) {
return -1;
}
// 执行CBC解密
AES_cbc_encrypt(ciphertext, plaintext, ciphertext_len,
&aes_key, iv_copy, AES_DECRYPT);
// 移除PKCS7填充
int pad_len = plaintext[ciphertext_len - 1];
if (pad_len < 1 || pad_len > 16) {
return -1; // 填充无效
}
return ciphertext_len - pad_len;
}
避坑指南:我曾经犯过一个低级错误——每次加密都使用固定的IV。结果两个相同的数据库页加密后密文一模一样,攻击者一眼就能看出数据模式。记住:每个页的IV必须不同,通常用页号作为IV的一部分。
17.5 集成到SQLite VFS
有了AES加解密函数,接下来就是把它挂到SQLite的VFS上。核心是重写xRead和xWrite两个函数:
// 自定义VFS的xWrite实现
static int crypto_xWrite(sqlite3_file *pFile,
const void *pBuf, int iAmt, sqlite3_int64 iOfst) {
CryptoFile *p = (CryptoFile *)pFile;
unsigned char encrypted[4096]; // 假设页大小最大4KB
int encrypted_len;
// 生成基于偏移量的IV
unsigned char iv[16];
memset(iv, 0, 16);
memcpy(iv, &iOfst, sizeof(sqlite3_int64));
// 加密数据
encrypted_len = aes_encrypt((const unsigned char *)pBuf, iAmt,
p->key, iv, encrypted);
if (encrypted_len < 0) {
return SQLITE_IOERR;
}
// 写入加密后的数据
return p->real.xWrite(&p->real, encrypted, encrypted_len, iOfst);
}
// 自定义VFS的xRead实现(类似,做解密操作)
static int crypto_xRead(sqlite3_file *pFile,
void *pBuf, int iAmt, sqlite3_int64 iOfst) {
CryptoFile *p = (CryptoFile *)pFile;
unsigned char encrypted[4096];
int rc;
// 先读取加密数据
rc = p->real.xRead(&p->real, encrypted, iAmt, iOfst);
if (rc != SQLITE_OK) {
return rc;
}
// 生成IV
unsigned char iv[16];
memset(iv, 0, 16);
memcpy(iv, &iOfst, sizeof(sqlite3_int64));
// 解密数据
int decrypted_len = aes_decrypt(encrypted, iAmt,
p->key, iv, (unsigned char *)pBuf);
if (decrypted_len < 0) {
return SQLITE_IOERR;
}
return SQLITE_OK;
}
关键点:加密后的数据长度可能会变(因为填充),所以写入和读取的长度要一致。我通常固定每个页加密后的大小,比如4KB页加密后还是4KB(用固定填充)。这样VFS层不用处理变长问题,简单很多。
17.6 密钥管理
加密算法再强,密钥泄露就全完了。我在项目中常用的密钥管理方式:
- 硬件绑定:从芯片的唯一ID(如STM32的UID)派生密钥
- 用户输入:应用启动时要求输入密码,通过PBKDF2派生密钥
- 安全元件:如果有SE芯片,密钥存储在SE内部,只提供加解密服务
我个人最推荐硬件绑定+用户密码组合。这样即使设备丢了,没有密码也打不开数据库;即使密码泄露,没有硬件也派生不出正确密钥。
警告:千万不要把密钥硬编码在代码里!我曾经见过一个产品,密钥直接写在全局变量里,反编译一下就拿到了。那叫一个惨。
17.7 性能考量
加密肯定有性能开销。AES-256在Cortex-M4上加密一个4KB页大约需要0.5ms。如果你的应用频繁读写小数据,这个开销不可忽视。
我一般建议:
- 只加密敏感字段,而不是整个数据库(如果业务允许)
- 使用AES-128代替AES-256,安全性足够,速度快30%
- 开启SQLite的WAL模式,减少加密次数
好了,数据库加密这块就聊到这儿。记住:加密不是银弹,但它是安全的第一道防线。动手试试吧,从AES加密函数开始,慢慢搭建你自己的加密VFS。
最后一句:代码写完了,记得做完整性测试。加密后的数据库文件,用十六进制编辑器打开,应该看不到任何明文特征。如果看到"SQLite format 3"字样,说明你的加密层没生效。