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实现,接管所有文件读写操作。我们在读写函数里插入加密/解密逻辑,就能实现透明加密。

基本架构是这样的:

应用程序 SQLite核心 自定义VFS层(加密/解密) xRead / xWrite 钩子 存储设备 数据写入时加密 数据读取时解密

这样做的好处是:应用程序完全无感知。你只需要在打开数据库时传入一个密钥,后续所有读写操作自动加解密。

注意:自定义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"字样,说明你的加密层没生效。

公众号:蓝海资料掘金营,微信deep3321