15、数据库加密与安全:SQLite加密扩展(SEE)、自定义加密方案、密码管理、防SQL注入

说到数据库安全,我脑子里立刻蹦出几年前的一个深夜。那会儿我负责一个嵌入式设备的数据存储模块,设备被人拿走了,SD卡一读,数据库文件直接打开,用户数据一览无余。嗯,从那以后,我对数据库加密这件事就再也不敢马虎了。

这一章,咱们就聊聊SQLite的加密与安全。说白了,就是怎么让你的数据文件即使被人拿走了,也读不出任何有价值的东西。

15.1 SQLite加密扩展(SEE)

SQLite官方提供了一个加密方案,叫SQLite Encryption Extension,简称SEE。它是在SQLite内核层面做的加密,数据写入磁盘前自动加密,读取时自动解密。对上层应用来说,完全无感。

SEE支持AES-128、AES-256等算法。你只需要在打开数据库时提供一个密码,剩下的交给它。

核心要点:SEE是商业授权,需要付费。如果你用的是开源项目,可以考虑社区替代方案,比如SQLCipher。

我个人习惯在项目早期就确定是否要用SEE。因为一旦数据量大了再改加密方案,迁移成本很高。

15.2 自定义加密方案

如果你不想用商业方案,或者平台受限,可以自己实现一套加密。思路其实不复杂:

  • 字段级加密:只加密敏感字段,比如身份证号、密码。写入前加密,读取后解密。
  • 页面级加密:在写入SQLite的B-Tree页面之前,对整个页面进行加解密。
  • 文件级加密:用操作系统提供的加密文件系统,比如Linux的eCryptfs。

我在项目中遇到过一种情况:设备性能很弱,AES-256跑起来太慢。后来我们只对几个关键字段做了XOR+移位混淆,虽然安全性比不上AES,但应付一般的数据泄露场景足够了。

注意:自定义加密方案不要自己发明算法。用成熟的库,比如OpenSSL、mbedTLS。我曾经见过有人自己写了个“超级复杂”的加密,结果三天就被破解了。

代码示例:字段级AES加密

// 使用mbedTLS进行AES-256-CBC加密
#include <mbedtls/aes.h>

int encrypt_field(const unsigned char *plain, int plain_len,
                  const unsigned char *key, const unsigned char *iv,
                  unsigned char *cipher) {
    mbedtls_aes_context aes;
    mbedtls_aes_init(&aes);
    mbedtls_aes_setkey_enc(&aes, key, 256);
    
    // 注意:这里需要处理填充,PKCS7是常用方案
    int ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT,
                                    plain_len, iv, plain, cipher);
    mbedtls_aes_free(&aes);
    return ret;
}

你想想看,字段级加密的优点是灵活,缺点是查询时没法用LIKE、ORDER BY这些操作。所以设计表结构时就要想清楚:哪些字段需要加密,哪些字段需要保持明文以便查询。

15.3 密码管理

加密的钥匙就是密码。密码怎么存、怎么传、怎么轮换,都是大问题。

方案 优点 缺点
硬编码在代码里 简单 反编译就暴露
从外部文件读取 可配置 文件本身需要保护
使用硬件安全模块(HSM) 安全性高 成本高,嵌入式不一定支持
基于设备指纹派生 绑定设备 设备更换后数据无法解密

我建议的做法是:用设备唯一ID(比如MCU的UID)加上一个固定的盐值,通过PBKDF2派生出一个256位的密钥。这样即使代码被反编译,攻击者也拿不到完整的密钥。

小技巧:密码轮换时,不要直接改数据库的加密密钥。可以设计一个密钥版本号,旧数据用旧密钥解密后,用新密钥重新加密写入。这个过程可以后台慢慢做。

15.4 防SQL注入

SQL注入这事儿,说起来是老生常谈,但我在代码审查时还是经常看到。说白了,就是用户输入被当成了SQL代码执行。

SQLite本身没有存储过程,注入的风险主要来自拼接SQL字符串。比如:

// 危险的写法
char sql[256];
sprintf(sql, "SELECT * FROM users WHERE name='%s'", user_input);
// 如果user_input是 "admin' OR '1'='1",你就完了

正确的做法是使用参数化查询。SQLite的C API支持绑定参数:

sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "SELECT * FROM users WHERE name=?", -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, user_input, -1, SQLITE_TRANSIENT);
// 然后执行
sqlite3_step(stmt);

为什么参数化能防注入?因为SQLite会把参数值当作数据,而不是SQL代码。你想想看,即使用户输入了 ' OR '1'='1,它也只是被当作一个字符串去匹配name字段,不会改变SQL语义。

我曾经见过一个项目,开发者觉得“我们用户输入都是数字,不用参数化也没事”。结果有人传了个负数,导致SQL逻辑完全反转。从那以后,我所有SQL都用参数化,不管输入是什么类型。

15.5 本章知识体系

下面这张图,把数据库加密与安全的几个核心模块串起来了。你可以看到,从加密方案到密码管理,再到防注入,每一层都不可或缺。

数据库加密与安全知识体系 目标:保护数据机密性与完整性 SEE加密扩展 商业方案,内核级加密 自定义加密 字段/页面/文件级 密码管理 派生/存储/轮换 防SQL注入 参数化查询 AES-128/256 SQLCipher替代 商业授权 mbedTLS/OpenSSL XOR混淆 性能权衡 PBKDF2派生 设备指纹 密钥轮换 sqlite3_bind_* 避免字符串拼接 输入验证 安全是系统工程,每一层都不能偷懒

数据库安全这件事,没有银弹。加密方案选型要看你的威胁模型:防谁?防到什么程度?成本多少?我个人的经验是:先做对防SQL注入,再考虑加密。因为注入攻击是主动的,加密是被动的。你想想看,数据被加密了但被人删光了,那加密还有什么意义?

一句话总结:加密保护静态数据,参数化保护动态查询,密码管理保护钥匙。三者缺一不可。


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