这几天优化数据库读写,移植了xxtea加密到最新的数据库sqlite 3.12.2里,一些好文章放在这里。移植后,数据库读写性能异常优秀!
这几天又发现,数据库还是发生了无法写入情况,数据库崩溃掉了。所以,估计想兼容以前数据库是很困难的了。。
- 十分推荐他的博客:SQLite学习笔记(十)&&加密 Sqlite学习笔记(一)&&编译安装
- sqlite3.6.18加密模块change(讨论)
- sqlite3 加解密
- SQLITE3 加密总结
- sqlite3+使用总结
SQLite学习笔记(十)&&加密 – 天士梦 – 博客园
SQLite学习笔记(十)&&加密
随着移动互联网的发展,手机使用越来越广泛,sqlite作为手机端存储的一种解决方案,使用也非常普遍。但是sqlite本身安全特性却比较弱,比如不支持用户权限,只要能获取到数据库文件就能进行访问;另外也没有加密功能,任何人获取到文件后,就可以查到明文数据。这使得大家对于sqlite又爱又不敢用,毕竟用户的隐私是非常重要的。Sqlite分为开源版本和收费版,收费的版本是支持加密的,只不过需要付2000刀的技术支持费。当然,由于sqlite本身是开源的,业内有很多产品对sqlite增加了加密功能,比如wxsqlite,sqlcipher等。因此想免费使用加密版本的sqlite也不是不可以,但前提是使用过程遇到问题,一定要能hold住,因为免费的东西不一定是稳定的。废话少说,本文主要讨论两个问题, 1.自己如何实现sqlite的加密功能,2.如何使用加密的sqlite。
加密算法选择
要实现加密,首先需要选择一种主流、高效、安全的加密算法,对于手机端和嵌入式设备,还要加上一条简单的原则。加密算法主要分为对称加密算法和非对称加密算法,对于文本加密一般采用对称加密,而对于秘钥的管理则采用非对称加密。目前主流的对称加密算法有DES,AES,RC系列,TEA系列, Blowfish等,非对称加密算法有RSA、Diffie-Hellman等。本人对几种常见的对称加密算法进行了测试,综合起来AES和XXTEA算法性能最好,XXTEA的优势在于实现非常简单,代码不到100行,在端设备上也是一种优势。另外,腾讯一直在使用TEA系列算法作为它的通讯加密和本地存储加密,用实践证明了TEA系列算法的可靠性和安全性。下图是AES算法和XXTEA算法的一些测试数据,仅供参考。XXTEA-256和XXTEA-64,分别表示块大小是256个INT和64个INT。通过测试读写200M数据的时间来评估算法的优劣,每个数据通过测试5次取平均值。
不加密 | AES | XXTEA-256 | XXTEA-64 | |
写操作 | 7879.3ms | 10283.6ms | 10577.3ms | 10649.4ms |
读操作 | 315.69ms | 3681.4ms | 3527ms | 3598ms |
上层应用加密
选择好加密算法,那么要实现加密功能了。有童鞋会问,为啥一定要在sqlite内部实现加密,上层应用加密不是一样OK吗?这个确实可以。很多开发语言都自带了加密库函数,直接调用即可。写入数据时进行加密,读出数据时再进行解密。但这种方式主要有以下几个缺点:
- 对于加密的数据列,无法使用索引,虽然可以用等值查询,但对于范围查询则无能为力。而且所有类型需要设计为BLOB类型,来存储密文。
- 虽然可以对数据列进行加密,但表的元数据无法加密。
所以,总地来说,应用层是一种解决方案,但对开发者不太友好。
sqlite自身加密
从原理上来看,加入加密模块功能也相对简单,在写入数据块前,调用加密模块进行数据块加密,然后写入文件;而在读取数据块时,先调用加密模块解密,然后加载到缓存。加密模块(红色标注)在整个sqlite实现框架的位置如下图。
实现层面,我们要做的工作主要包括三部分,实现加密算法,然后将加密接口与sqlite关联起来,最后在合适的位置(读写文件),调用加/解密接口完成加密解密工作。由于sqlite自身已经预留了加解密接口,因此第3部分的工作已经帮我们做了,我们只需要实现第1和第2部分工作。核心接口如下:
1.sqlite3CodecAttach
含义:调用sqlite3PagerSetCodec 将加密接口与sqlite的pager模块关联
2.xxxCodec
含义:自己定义的加密接口,输入参数是(page_no,读写模式),将指定page加、解密。
3.sqlite3_key
含义:设置数据库密钥,sqlite3_open后调用该接口。对于非加密库,若调用该接口,会导致后续访问数据报错。
Error: file is encrypted or is not a database
4.sqlite3_rekey
含义:修改数据库密钥,这个接口会遍历数据库中的所有页,然后用新密钥对页进行加密,写入文件。若将新密钥设置为NULL,则可以将加密库变为普通库。
5.sqlite3_activate_see
sqlite3CodecGetKey
这两个接口没有实际作用,可以实现一个空函数,保证能编译通过就行。
加密sqlite使用
如果顺利,我们现在已经有了一个包含加密功能的sqlite了,那么如何使用?下面主要列了一些常见的场景。
1.如何判断一个db文件是否加密
$sqlite3 ./data/test.db
sqlite> .tables Error: file is encrypted or is not a database
sqlite> pragma key=’123456′ ;
sqlite> .tables
Error: file is encrypted or is not a database
对于加密的db文件,如果没有执行pragma key=xxx命令直接执行,则会报错。假设密码是 123456,报错后,重新执行pragma key命令,然后执行.tables,这时候依旧报错。这个主要是因为密钥在连接创建时初始化,所以对于加密数据库,需要执行的第一条命令是pragma key=xxx
2.导出加密文件数据
$sqlite3 ./data/test.db // 打开加密数据库文件test.db
sqlite> pragma key=’123456′; // 设置秘钥
sqlite> .output aaa.sql //将输出重定向到文件aaa.sql
sqlite> .dump //导出数据库
sqlite> .exit 这样aaa.sql中包含了test.db中数据库的明文内容。
3.解密加密数据库
$sqlite3 ./data/test.db
sqlite> pragma key=’123456′
sqlite> pragma rekey=”; //设置密钥为空,则将密文数据库解密。
//解密后可以直接打开数据库
$sqlite3 ./data/test.db
sqlite> .tables
- orders t1 user
sqlite> .exit
参考文档
作者: |
|
2009-10-14 21:33:45 | ||||
标题: |
|
加入我的收藏 | ||||
楼主: | 很多人使用xxtea的sqlite加密 3.6.18版, sqlite3PagerSetCodec变化非常大 static void sqlite3PagerSetCodec( Pager *pPager, void *(*xCodec)(void*,void*,Pgno,int), void (*xCodecSizeChng)(void*,int,int), //新加的 void (*xCodecFree)(void*), //新加的 void *pCodec //对应原来的 pCodecArg另外,xxtea中(如下), pAux 和 xFreeAux 已经完全消失了 db->aDb[0].pAux = pBlock; db->aDb[0].xFreeAux = DestroyCryptBlock;**** 问题: xFreeAux 能否用 xCodecFree 代替, 代替后,xxtea怎么变动 **** 关于asqlite 风铃兄的sqlite控件看样子是改的disqlite的,对dataset数据行buffer存取 asqlite官版在操作text及widestring上,read做了很多次重复widestring转换,效率低,unicode更是有转换bug 3291763 |
作者: |
|
2009-10-14 22:35:47 | ||||
1楼: | 今天刚好改 风铃兄 的sqlite,随便说说,不对的请大家指正.
sqlite3PagerSetCodec变化非常大 xCodecSizeChng 更改page大小时候调用的函数,暂时没用上直接NULL DestroyCryptBlock 函数要改一下参数类型,不然编译不过。 用20M的Demo测试,未发现明显问题。 |
作者: |
|
2009-10-15 0:42:25 | ||||
2楼: | 即然你都提了,我也发我最新修改的xxteacrypt.c代码
#ifdef SQLITE_SOURCESTD #ifndef SQLITE_OMIT_DISKIO #include “md5c.c” #define CRYPT_OFFSET 8 typedef struct _CryptBlock int xxtea( int * v, int n , int * k ) { /* Decoding Part */ return 1 ; void sqlite3_activate_see(const char *info) void sqlite3CodecSizeChange(void *pArg, int pageSize, int reservedSize) /* /* //创建或更新一个页的加密算法索引.此函数会申请缓冲区. if (!pExisting) //创建新加密块 ZeroMemory(pBlock->Data, pBlock->PageSize + CRYPT_OFFSET); return pBlock; //销毁一个加密块及相关的缓冲区,密钥. //如果写密钥存在并且不等于读密钥,也销毁. if(pBlock->Data){ //释放加密块. // 释放与一个页相关的加密块 //加密/解密函数, 被pager调用 if (!pBlock) return data; // 确保pager的页长度和加密块的页长度相等.如果改变,就需要调整. switch(nMode) len = 0 – (pBlock->PageSize / 4); break; CopyMemory(pBlock->Data + CRYPT_OFFSET, data, pBlock->PageSize); len = pBlock->PageSize / 4; CopyMemory(pBlock->Data + CRYPT_OFFSET, data, pBlock->PageSize); len = pBlock->PageSize / 4; return data; // 从用户提供的缓冲区中得到一个加密密钥 if ((pKey == NULL) || (nKeyLen <1 )) digest = sqlite3_malloc(16); MD5Init(&md5ctx); MD5Update(&md5ctx, (unsigned char*)pKey, nKeyLen); hKey = digest; //被sqlite 和 sqlite3_key_interop 调用, 附加密钥到数据库. //如果没有指定密匙,可能标识用了主数据库的加密或没加密. if (!pBlock) return SQLITE_OK; //主数据库没有加密 memcpy(pBlock->ReadKey, &hKey, 16); //创建一个新的加密块,并将解码器指向新的附加数据库. //不保存用户的原始密码.返回NULL. //密钥并不保留到临时空间,仅保存于主数据库. //改变已有数据库的加密密钥 if (!pBlock && !hKey) return SQLITE_OK; //重新加密一个数据库,改变pager的写密钥, 读密钥依旧保留. // 开始一个事务 if (!rc) for(n = 1; rc == SQLITE_OK && n <= nPage; n ++) // 如果成功,提交事务。 // 如果失败,回滚。 // 如果成功,销毁先前的读密钥。并使读密钥等于当前的写密钥。 // 如果读密钥和写密钥皆为空,就不需要再对页进行编解码。 SQLITE_API int sqlite3_key(sqlite3 *db, const void *pKey, int nKey) SQLITE_API int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey) #endif // SQLITE_HAS_CODEC #endif // SQLITE_OMIT_DISKIO |
作者: |
|
2009-10-15 9:11:29 | ||||
3楼: | 非常感谢,改的比我完善多了. 其实按风兄的思路,改UniDAC的liteprovier支持 sqlite3密码也很简单,这样UniDAC也能支持读取带密码的sqlite3数据库了. ———————————————- – |
作者: |
|
2009-10-15 10:37:14 | ||||
4楼: | 谢谢风铃兄修改的xxtea, 这下可以兼容旧的版本了 ———————————————-3291763 |
作者: |
|
2009-10-15 12:03:04 | ||||
5楼: | 另外如果用的BDS2010的话,用BDS2010自带的cw32.lib测试20M Demo
内存占用约多7%, 但速度约快7% |
作者: |
|
2009-11-16 13:28:31 | ||||
6楼: | 最好,接过讨论一下风兄的AES加密版的sqlite,如何让bcc32编译通过 ———————————————-3291763 |
作者: |
|
2010-2-25 23:05:40 | ||||
7楼: | 我一直想改unidac支持加密的sqlite,但没成功,有谁可以贴出代码?感谢! ———————————————- – |
作者: |
|
2010-3-2 14:01:24 | ||||
8楼: | 有谁可以告诉我如何修改unidac让它支持wxsqlite3.dll的加密功能?我修改了litecalluni.pas和sqliteclassesuni.pas,但都没有成功。 ———————————————- – |
作者: |
|
2010-3-28 19:05:19 | ||||
9楼: | ntp2000 :你可发一个带密码的SQlite3.dll和一个支持SQLite加密功能的UniDac组件吗?谢谢~~ ———————————————- 没有最好,只有更好! |
作者: |
|
2012-7-3 11:52:47 | ||||
10楼: | 我把这些方法移植到3.7.13的源码中发现这个不能加密,原来能看到的数据加密后任然可见.后面发现xxtea这个算法根本就没被调用,即使赋值给sqlite3中pager->xCodec后也没见做任何操作,请问这是为什么不能加密。是不是这组代码不完善啊?还有就是如果我需要在linux下编译添加接口后的sqlite3如何编译呢?求大侠尽快回复!! ———————————————- – |
作者: |
|
2012-7-3 13:42:47 | ||||
11楼: | 有人帮我解释一下这是为什么啊?加密部分函数mySqlite3PagerSetCodec这个函数什么都没做啊。 ———————————————- – |
作者: |
|
2012-7-3 16:50:55 | ||||
12楼: | 加密/解密函数, 被pager调用sqlite3Codec这个函数是在哪里被调用的呢 ———————————————- – |
作者: |
|
2012-7-10 12:57:39 | ||||
13楼: | 楼主,可不可以指导一下如何实现这个sqlite数据库的加密功能,真心急用呀。 ———————————————- 交朋友 |
作者: |
|
2012-7-10 13:21:33 | ||||
14楼: | 另外楼主,你的sqlite3Codec()这个函数中的CopyMemory(pBlock->Data + CRYPT_OFFSET, data, pBlock->PageSize);这个函数是从哪来的呢? ———————————————- 交朋友 |
SQLite 3.7.13的加密解密(一)—— 前言
SQLite数据库支持加密和解密,但是免费版没有这个功能,不过网上已经有相关的资料,不过这些资料都不是基于SQLite 3.7.13版本的,这里根据网上找到的最全的资料进行整理,实现了SQLite 3.7.13版数据库的加密解密。本系列文章对此进行了详细说明。
开发环境:
操作系统 | Win 7 |
IDE | Eclipse Juno (4.2) CDT |
编译器 | MinGW GCC 4.6.2 |
SQLite | 3.7.13 |
SQLite 3.7.13的加密解密(二)—— 开放宏定义
首先要在sqlite3.c中最前面,添加代码(网上有说在sqlite3.h中添加也可,实际测试在sqlite3.h中打开该宏是无效的):
#ifndef SQLITE_HAS_CODEC
#define SQLITE_HAS_CODEC #endif |
这个宏是用来确定是否支持加密的。添加上述代码后编译,会出现如下错误:
D:\Research\MySQLite\Debug/../src/sqlite3.c:80963: undefined reference to sqlite3CodecAttach'
sqlite3CodecGetKey’
D:\Research\MySQLite\Debug/../src/sqlite3.c:80970: undefined reference to src\sqlite3.o: In function sqlite3Pragma’: D:\Research\MySQLite\Debug/../src/sqlite3.c:94023: undefined reference to D:\Research\MySQLite\Debug/../src/sqlite3.c:94026: undefined reference to sqlite3_rekey’ D:\Research\MySQLite\Debug/../src/sqlite3.c:94038: undefined reference to D:\Research\MySQLite\Debug/../src/sqlite3.c:94040: undefined reference to sqlite3_rekey’ D:\Research\MySQLite\Debug/../src/sqlite3.c:94048: undefined reference to src\sqlite3.o: In function sqlite3RunVacuum’: D:\Research\MySQLite\Debug/../src/sqlite3.c:101744: undefined reference to `sqlite3CodecGetKey’ |
SQLite 3.7.13的加密解密(三)—— 创建加密解密函数
先不用管上面的编译错误,创建crypt.c和crypt.h,用来实现加密解密函数和相应接口的定义。
crypt.c里实现了加密解密函数,代码如下:
#include “crypt.h”
#include “memory.h” int My_Encrypt_Func(unsigned char * pData, unsigned int data_len, { } int My_DeEncrypt_Func(unsigned char * pData, unsigned int data_len, { } |
这里加密解密函数就是简单的采用了求反的操作,目的是用来演示加密和解密。以后实际运用中把这两个函数内的算法修改为自己的加密解密算法即可。
注意:这里的加密解密函数实际上没有用到Key值,因此加密后使用任意Key值均可以解开数据库,但是加密后,用第三方工具是不能直接打开的。
crypt.h用来声明加密解密函数的定义,以便sqlite3.c包含加密解密接口,代码如下:
int My_Encrypt_Func(unsigned char * pData, unsigned int data_len, int My_DeEncrypt_Func(unsigned char * pData, unsigned int data_len, |
注意:网上的代码中,参数key定义为“const char *”类型,与sqlite3.c代码一起编译时会有错误,这里按照sqlite3.c中的类型修改为“unsigned char *”类型。
SQLite 3.7.13的加密解密(四)—— 挂接加密解密函数
把crypt.c中实现的加密解密函数挂接到sqlite3.c中,并且实现前面编译提示的未实现的函数。在sqlite3.c的最后一行的后面,添加如下代码:
#ifdef SQLITE_HAS_CODEC
#include “crypt.h” #define CRYPT_OFFSET 8 typedef struct _CryptBlock { } CryptBlock, *LPCryptBlock; #ifndef #define #endif #ifndef #define #endif void sqlite3CodecGetKey(sqlite3* db, int nDB, void** Key, int* nKey) { } int sqlite3CodecAttach(sqlite3 *db, int nDb, const void *pKey, int nKeyLen); void sqlite3_activate_see(const char* right) { } int sqlite3_key(sqlite3 *db, const void *pKey, int nKey); int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey); // 从用户提供的缓冲区中得到一个加密密钥 // 用户提供的密钥可能位数上满足不了要求,使用这个函数来完成密钥扩展 static unsigned char * DeriveKey(const void *pKey, int nKeyLen); //创建或更新一个页的加密算法索引.此函数会申请缓冲区. static LPCryptBlock CreateCryptBlock(unsigned char* hKey, Pager *pager, //加密/解密函数, 被pager调用 void * sqlite3Codec(void *pArg, unsigned char *data, Pgno nPageNum, int nMode); //设置密码函数 int __stdcall sqlite3_key_interop(sqlite3 *db, const void *pKey, int nKeySize); // 修改密码函数 int __stdcall sqlite3_rekey_interop(sqlite3 *db, const void *pKey, int nKeySize); //销毁一个加密块及相关的缓冲区,密钥. static void DestroyCryptBlock(LPCryptBlock pBlock); static void * sqlite3pager_get_codecarg(Pager *pPager); void sqlite3pager_set_codec(Pager *pPager, //加密/解密函数, 被pager调用 void * sqlite3Codec(void *pArg, unsigned char *data, Pgno nPageNum, int nMode) { } //销毁一个加密块及相关的缓冲区,密钥. static void DestroyCryptBlock(LPCryptBlock pBlock) { //如果写密钥存在并且不等于读密钥,也销毁. } static void * sqlite3pager_get_codecarg(Pager *pPager) { } // 从用户提供的缓冲区中得到一个加密密钥 static unsigned char * DeriveKey(const void *pKey, int nKeyLen) { } //创建或更新一个页的加密算法索引.此函数会申请缓冲区. static LPCryptBlock CreateCryptBlock(unsigned char* hKey, Pager *pager, { } void sqlite3pager_set_codec(Pager *pPager, void *(*xCodec)(void*, void*, Pgno, int), void*pCodecArg) { } int sqlite3_key(sqlite3 *db, const void *pKey, int nKey) { } int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey) { } int sqlite3CodecAttach(sqlite3 *db, int nDb, const void *pKey, int nKeyLen) { } // Changes the encryption key for an existing database. int __stdcall sqlite3_rekey_interop(sqlite3 *db, const void *pKey, int nKeySize) { } int __stdcall sqlite3_key_interop(sqlite3 *db, const void *pKey, int nKeySize) { } // 释放与一个页相关的加密块 void sqlite3pager_free_codecarg(void *pArg) { } #endif //#ifdef SQLITE_HAS_CODEC |
特别说明:
DeriveKey 函数,这个函数是对密钥的扩展。比如,你要求密钥是128位,即是16字节,但是如果用户只输入 1个字节呢?2个字节呢?或输入50个字节呢?你得对密钥进行扩展,使之符合16字节的要求。
DeriveKey 函数就是做这个扩展的。有人把接收到的密钥求md5,这也是一个办法,因为md5运算结果固定16字节,不论你有多少字符,最后就是16字节。这是md5 算法的特点。但是我不想用md5,因为还得为它添加包含一些 md5 的.c或.cpp文件。我不想这么做。我自己写了一个算法来扩展密钥,很简单的算法。当然,你也可以使用你的扩展方法,也而可以使用 md5 算法。只要修改 DeriveKey 函数就可以了。
在 DeriveKey 函数里,只管申请空间构造所需要的密钥,不需要释放,因为在另一个函数里有释放过程,而那个函数会在数据库关闭时被调用。参考上面 DeriveKey 函数来申请内存。
SQLite 3.7.13的加密解密(五)—— 修正编译错误和警告
上面的代码是从网上下载下来的,它使用的SQLite版本比较旧,因此在SQLite 3.7.13下编译不通过,下面需要对编译错误和警告逐一修正。
编译信息 |
原因与修改方法 |
‘Pager’ has no member named ‘pCodecArg’ | 在3.7.13版本中,Pager的成员变量pCodecArg名称修改为pCodec,因此用到pCodecArg变量的地方修改为使用pCodec。 |
too few arguments to function ‘sqlite3PagerPagecount’ | 原来sqlite3PagerPagecount()函数用返回值得到页数量,3.7.13改为用指针参数得到页数量。
修改前代码: Pgno nPage = sqlite3PagerPagecount(p); 修改如下: int nPage; sqlite3PagerPagecount(p, &nPage); |
too few arguments to function ‘sqlite3BtreeRollback’ | 3.7.13版中sqlite3BtreeRollback()函数增加了个参数,是表示之前SQL语句执行结果的,在网上查了一下,这里直接传常量SQLITE_OK。 |
implicit declaration of function ‘sqliteFree’ | sqliteFree()函数在3.7.13版本中已经没有了,修改为使用sqlite3_free()函数。 |
implicit declaration of function ‘sqliteMalloc’ | 原因同上,修改为使用sqlite3_malloc()函数。 |
|
|
implicit declaration of function ‘DATA_TO_PGHDR’ | 在3.7.13版本中,宏DATA_TO_PGHDR已经被去掉,这里暂时把该if语句下的代码全部注释。 |
warning: passing argument 2 of ‘sqlite3pager_set_codec’ from incompatible pointer type | sqlite3pager_set_coedc()函数的第二个参数类型为:void *(*xCodec)(void*, void*, Pgno, int)
而调用的地方传递的参数类型为:void *sqlite3Codec(void *pArg, unsigned char *data,Pgno nPageNum, int nMode) 很明显,第二个参数类型不匹配,修改sqlite3Codec()函数的第二个参数类型为void *,注意相应的函数声明的地方也要修改。 |
warning: passing argument 3 of ‘sqlite3PagerAcquire’ from incompatible pointer type | 这里第三个参数类型为void *,实际要求的数据类型为DbPage *,将对应变量类型定义为DbPage *即可。 |
warning: variable ‘bRc’ set but not used | 这个警告不影响使用,不用改。 |
warning: ‘sqlite3PagerSetCodec’ defined but not used | 同上 |
SQLite 3.7.13的加密解密(六)—— 使用方法
采用上一节的方法为SQLite添加了加密解密功能后,使用方法如下:
1、 在调用sqlite3_open()函数打开数据库后,要调用sqlite3_key()函数为数据库设置密码;
2、 如果数据库之前有密码,则调用sqlite3_key()函数设置正确密码才能正常工作;
3、 如果一个数据库之前没有密码,且已经有数据,则不能再为其设置密码;
4、 如果要修改密码,则需要在第一步操作后,调用sqlite3_rekey()函数设置新的密码;
5、 设置了密码的SQLite数据库,无法使用第三方工具打开;
具体使用的示例代码如下:
#include <stdio.h>
#include <stdlib.h> #include “sqlite3.h” #define extern int sqlite3_key(sqlite3 *db, const void *pKey, int nKey); static int _callback_exec(void * notused,int argc, char ** argv, char ** aszColName) { } int main(int argc, char * argv[]) { } |
SQLite 3.7.13的加密解密(七)—— 遗留问题
现象与原因
采用上面的方法对数据库进行加密,存在页面尺寸错乱的问题。在SQLite的DB文件中,第16、17两个字节的值表示数据库中每个页的大小,SQLite规定页大小必须是512的倍数,如果加密算法恰好导致这两个字节的值为512的倍数,且与数据库的实际页面大小不一样,就会导致不能进行数据库操作。
其原因是在sqlite3_open()函数中,会读取DB文件头,从16、17字节得到页大小,但是sqlite3_open()函数中没有调用解密函数,因此得到的就是错误的值。一般来说,采用的加密算法不会导致16、17这两个字节恰好是512的倍数,在SQLite内部有保护,如果这个数据不是512的倍数,或者超过一定数值,则自动取默认值1024。
但是也不排除加密算法没有选择好,导致这两个字节的值出现问题,譬如我曾经采用个简单的加密算法,就是将每个字节循环左移一位,结果16、17这两个字节的值就是2048,正好是512的倍数,最终导致程序崩溃。
解决方案
这种问题,实际上是因为sqlite代码中没有充分考虑这种pagesize在加密后读取的问题,解决方法有两个:
方案一:(彻底解决方案)修改sqlite源码,使opendatase读取的pagesize无效,在设置好数据库密钥以后,第一次读取数据时重新计算pagesize;
方案二:(针对加密的修补方案)修改sqlite3_key的加密实现,在设置密钥时,解密读取数据库的头信息,读取解密后的pagesize,再把这个正确的pagesize设置回去;
- <pre name=“code” class=“plain”></pre>
- <pre></pre>
- <pre></pre>
前序
Sqlite3 的确很好用。小巧、速度快。但是因为非微软的产品,帮助文档总觉得不够。这些天再次研究它,又有一些收获,这里把我对 sqlite3 的研究列出来,以备忘记。
这里要注明,我是一个跨平台专注者,并不喜欢只用 windows 平台。我以前的工作就是为 unix 平台写代码。下面我所写的东西,虽然没有验证,但是我已尽量不使用任何windows 的东西,只使用标准 C 或标准C++。但是,我没有尝试过在别的系统、别的编译器下编译,因此下面的叙述如果不正确,则留待以后修改。
下面我的代码仍然用 VC 编写,因为我觉得VC是一个很不错的IDE,可以加快代码编写速度(例如配合 Vassist )。下面我所说的编译环境,是VC2003。如果读者觉得自己习惯于 unix 下用 vi 编写代码速度较快,可以不用管我的说明,只需要符合自己习惯即可,因为我用的是标准 C 或 C++ 。不会给任何人带来不便。
一、版本
从 www.sqlite.org<http://www.sqlite.org/> 网站可下载到最新的 sqlite 代码和编译版本。我写此文章时,最新代码是 3.3.17 版本。
很久没有去下载 sqlite 新代码,因此也不知道 sqlite 变化这么大。以前很多文件,现在全部合并成一个 sqlite3.c 文件。如果单独用此文件,是挺好的,省去拷贝一堆文件还担心有没有遗漏。但是也带来一个问题:此文件太大,快接近7万行代码,VC开它整个机器都慢下来了。如果不需要改它代码,也就不需要打开sqlite3.c文件,机器不会慢。但是,下面我要写通过修改 sqlite 代码完成加密功能,那时候就比较痛苦了。如果个人水平较高,建议用些简单的编辑器来编辑,例如 UltraEdit 或 Notepad 。速度会快很多。(源码网整理,www.codepub.com)
二、基本编译
这个不想多说了,在 VC 里新建 dos 控制台空白工程,把 sqlite3.c 和sqlite3.h 添加到工程,再新建一个 main.cpp 文件。在里面写:
- extern “C”
- {
- #include”./sqlite3.h”
- };
- int main( int , char** )
- {
- return 0;
- }
为什么要 extern “C”?如果问这个问题,我不想说太多,这是C++的基础。要在C++ 里使用一段 C 的代码,必须要用 extern “C”括起来。C++跟 C虽然语法上有重叠,但是它们是两个不同的东西,内存里的布局是完全不同的,在C++编译器里不用extern “C”括起C代码,会导致编译器不知道该如何为 C 代码描述内存布局。
可能在 sqlite3.c 里人家已经把整段代码都 extern “C”括起来了,但是你遇到一个 .c 文件就自觉的再括一次,也没什么不好。
基本工程就这样建立起来了。编译,可以通过。但是有一堆的 warning。可以不管它。
三、SQLITE操作入门
sqlite提供的是一些C函数接口,你可以用这些函数操作数据库。通过使用这些接口,传递一些标准sql 语句(以 char * 类型)给 sqlite 函数,sqlite 就会为你操作数据库。
sqlite 跟MS的access一样是文件型数据库,就是说,一个数据库就是一个文件,此数据库里可以建立很多的表,可以建立索引、触发器等等,但是,它实际上得到的就是一个文件。备份这个文件就备份了整个数据库。
sqlite 不需要任何数据库引擎,这意味着如果你需要 sqlite 来保存一些用户数据,甚至都不需要安装数据库
(如果你做个小软件还要求人家必须装了sqlserver才能运行,那也太黑心了)。
下面开始介绍数据库基本操作。
1、 基本流程
(1) 关键数据结构
sqlite 里最常用到的是 sqlite3 * 类型。从数据库打开开始,sqlite就要为这个类型准备好内存,直到数据库关闭,整个过程都需要用到这个类型。当数据库打开时开始,这个类型的变量就代表了你要操作的数据库。下面再详细介绍。
(2) 打开数据库
- int sqlite3_open( 文件名, sqlite3 ** );
用这个函数开始数据库操作。需要传入两个参数,一是数据库文件名,比如:c:\\DongChunGuang_Database.db。文件名不需要一定存在,如果此文件不存在,sqlite 会自动建立它。如果它存在,就尝试把它当数据库文件来打开。sqlite3 ** 参数即前面提到的关键数据结构。这个结构底层细节如何,你不要关它。函数返回值表示操作是否正确,如果是 SQLITE_OK 则表示操作正常。相关的返回值sqlite定义了一些宏。具体这些宏的含义可以参考 sqlite3.h 文件。里面有详细定义(顺便说一下,sqlite3 的代码注释率自称是非常高的,实际上也的确很高。只要你会看英文,sqlite 可以让你学到不少东西)。
下面介绍关闭数据库后,再给一段参考代码。
(3) 关闭数据库
- int sqlite3_close(sqlite3 *);
前面如果用 sqlite3_open 开启了一个数据库,结尾时不要忘了用这个函数关闭数据库。
下面给段简单的代码:
- extern “C”
- {
- #include”./sqlite3.h”
- };
- int main( int , char** )
- {
- sqlite3 * db = NULL; //声明sqlite关键结构指针
- int result
- //打开数据库
- //需要传入 db 这个指针的指针,因为 sqlite3_open 函数要为这个指针分配内存,还要让db指针指向这个内存区
- result = sqlite3_open( “c:\\Dcg_database.db”, &db );
- if( result != SQLITE_OK )
- {
- //数据库打开失败
- return -1;
- }
- //数据库操作代码
- //…
- //数据库打开成功
- //关闭数据库
- sqlite3_close( db );
- return 0;
- }
这就是一次数据库操作过程。
2、 SQL语句操作
本节介绍如何用sqlite 执行标准 sql 语法。
(1) 执行sql语句
- int sqlite3_exec(sqlite3*, const char *sql, sqlite3_callback, void *, char **errmsg );
这就是执行一条 sql 语句的函数。
第1个参数不再说了,是前面open函数得到的指针。说了是关键数据结构。
第2个参数const char*sql 是一条 sql 语句,以\0结尾。
第3个参数sqlite3_callback是回调,当这条语句执行之后,sqlite3会去调用你提供的这个函数。(什么是回调函数,自己找别的资料学习)
第4个参数void* 是你所提供的指针,你可以传递任何一个指针参数到这里,这个参数最终会传到回调函数里面,如果不需要传递指针给回调函数,可以填NULL。等下我们再看回调函数的写法,以及这个参数的使用。
第5个参数char **errmsg 是错误信息。注意是指针的指针。sqlite3里面有很多固定的错误信息。执行 sqlite3_exec 之后,执行失败时可以查阅这个指针(直接 printf(“%s\3n”,errmsg))得到一串字符串信息,这串信息告诉你错在什么地方。sqlite3_exec函数通过修改你传入的指针的指针,把你提供的指针指向错误提示信息,这样sqlite3_exec函数外面就可以通过这个 char*得到具体错误提示。
说明:通常,sqlite3_callback 和它后面的 void * 这两个位置都可以填 NULL。填NULL表示你不需要回调。比如你做 insert 操作,做 delete 操作,就没有必要使用回调。而当你做select 时,就要使用回调,因为 sqlite3 把数据查出来,得通过回调告诉你查出了什么数据。
(2) exec 的回调
- typedef int(*sqlite3_callback)(void*,int,char**, char**);
你的回调函数必须定义成上面这个函数的类型。下面给个简单的例子:
- //sqlite3的回调函数
- // sqlite 每查到一条记录,就调用一次这个回调
- int LoadMyInfo( void * para, intn_column, char ** column_value, char ** column_name )
- {
- //para是你在 sqlite3_exec 里传入的void * 参数
- //通过para参数,你可以传入一些特殊的指针(比如类指针、结构指针),然后在这里面强制转换成对应的类型(这里面是void*类型,必须强制转换成你的类型才可用)。然后操作这些数据
- //n_column是这一条记录有多少个字段 (即这条记录有多少列)
- // char ** column_value 是个关键值,查出来的数据都保存在这里,它实际上是个1维数组(不要以为是2维数组),每一个元素都是一个 char * 值,是一个字段内容(用字符串来表示,以\0结尾)
- //char ** column_name 跟column_value是对应的,表示这个字段的字段名称
- //这里,我不使用 para 参数。忽略它的存在.
- int i;
- printf( “记录包含 %d 个字段\n”, n_column );
- for( i = 0 ; i < n_column; i ++ )
- {
- printf( “字段名:%s ?> 字段值:%s\n”, column_name[i], column_value[i] );
- }
- printf( “——————\n“ );
- return 0;
- }
- int main( int , char ** )
- {
- sqlite3 * db;
- int result;
- char * errmsg =NULL;
- result =sqlite3_open( “c:\\Dcg_database.db”, &db );
- if( result !=SQLITE_OK )
- {
- //数据库打开失败
- return -1;
- }
- //数据库操作代码
- //创建一个测试表,表名叫MyTable_1,有2个字段: ID 和 name。其中ID是一个自动增加的类型,以后insert时可以不去指定这个字段,它会自己从0开始增加
- result = sqlite3_exec( db, “create table MyTable_1( ID integerprimary key autoincrement, name nvarchar(32) )”, NULL, NULL, errmsg );
- if(result != SQLITE_OK )
- {
- printf( “创建表失败,错误码:%d,错误原因:%s\n”, result, errmsg );
- }
- //插入一些记录
- result = sqlite3_exec( db, “insert into MyTable_1( name ) values ( ‘走路’ )”, 0, 0, errmsg );
- if(result != SQLITE_OK )
- {
- printf( “插入记录失败,错误码:%d,错误原因:%s\n”, result, errmsg );
- }
- result = sqlite3_exec( db, “insert into MyTable_1( name ) values ( ‘骑单车’ )”, 0, 0, errmsg );
- if(result != SQLITE_OK )
- {
- printf( “插入记录失败,错误码:%d,错误原因:%s\n”, result, errmsg );
- }
- result = sqlite3_exec( db, “insert into MyTable_1( name ) values ( ‘坐汽车’ )”, 0, 0, errmsg );
- if(result != SQLITE_OK )
- {
- printf( “插入记录失败,错误码:%d,错误原因:%s\n”, result, errmsg );
- }
- //开始查询数据库
- result = sqlite3_exec( db, “select * from MyTable_1”, LoadMyInfo, NULL, errmsg );
- //关闭数据库
- sqlite3_close( db );
- return 0;
- }
通过上面的例子,应该可以知道如何打开一个数据库,如何做数据库基本操作。
有这些知识,基本上可以应付很多数据库操作了。
(3) 不使用回调查询数据库
上面介绍的 sqlite3_exec 是使用回调来执行 select 操作。还有一个方法可以直接查询而不需要回调。但是,我个人感觉还是回调好,因为代码可以更加整齐,只不过用回调很麻烦,你得声明一个函数,如果这个函数是类成员函数,你还不得不把它声明成 static 的(要问为什么?这又是C++基础了。C++成员函数实际上隐藏了一个参数:this,C++调用类的成员函数的时候,隐含把类指针当成函数的第一个参数传递进去。结果,这造成跟前面说的 sqlite 回调函数的参数不相符。只有当把成员函数声明成 static 时,它才没有多余的隐含的this参数)。
虽然回调显得代码整齐,但有时候你还是想要非回调的 select 查询。这可以通过 sqlite3_get_table 函数做到。
- int sqlite3_get_table(sqlite3*, const char *sql, char ***resultp,int *nrow, int *ncolumn, char **errmsg );
第1个参数不再多说,看前面的例子。
第2个参数是 sql 语句,跟sqlite3_exec 里的 sql 是一样的。是一个很普通的以\0结尾的char *字符串。
第3个参数是查询结果,它依然一维数组(不要以为是二维数组,更不要以为是三维数组)。它内存布局是:第一行是字段名称,后面是紧接着是每个字段的值。下面用例子来说事。
第4个参数是查询出多少条记录(即查出多少行)。
第5个参数是多少个字段(多少列)。
第6个参数是错误信息,跟前面一样,这里不多说了。
下面给个简单例子:
- int main( int , char ** )
- {
- sqlite3 * db;
- int result;
- char * errmsg = NULL;
- char **dbResult; //是 char ** 类型,两个*号
- int nRow, nColumn;
- int i , j;
- int index;
- result = sqlite3_open( “c:\\Dcg_database.db”, &db );
- if( result != SQLITE_OK )
- { //数据库打开失败
- return -1;
- }
- //数据库操作代码
- //假设前面已经创建了MyTable_1 表
- //开始查询,传入的dbResult 已经是 char **,这里又加了一个 & 取地址符,传递进去的就成了 char ***
- result = sqlite3_get_table( db, “select *from MyTable_1”,&dbResult, &nRow, &nColumn, &errmsg );
- if( SQLITE_OK == result )
- { //查询成功
- index = nColumn; //前面说过 dbResult 前面第一行数据是字段名称,从 nColumn 索引开始才是真正的数据
- printf( “查到%d条记录\n”, nRow );
- for( i = 0; i < nRow ; i++ )
- {
- printf( “第 %d 条记录\n”, i+1 );
- for( j = 0 ; j < nColumn; j++ )
- {
- printf( “字段名:%s ?> 字段值:%s\n”, dbResult[j], dbResult [index] );
- ++index; // dbResult 的字段值是连续的,从第0索引到第 nColumn – 1索引都是字段名称,从第 nColumn 索引开始,后面都是字段值,它把一个二维的表(传统的行列表示法)用一个扁平的形式来表示
- }
- printf( “——-\n” );
- }
- }
- //到这里,不论数据库查询是否成功,都释放 char** 查询结果,使用 sqlite 提供的功能来释放
- sqlite3_free_table( dbResult );
- //关闭数据库
- sqlite3_close( db );
- return 0;
- }
到这个例子为止,sqlite3 的常用用法都介绍完了。
用以上的方法,再配上 sql 语句,完全可以应付绝大多数数据库需求。
但有一种情况,用上面方法是无法实现的:需要insert、select 二进制。当需要处理二进制数据时,上面的方法就没办法做到。下面这一节说明如何插入二进制数据
3、 操作二进制
sqlite 操作二进制数据需要用一个辅助的数据类型:sqlite3_stmt * 。
这个数据类型记录了一个“sql语句”。为什么我把 “sql语句”用双引号引起来?因为你可以把 sqlite3_stmt * 所表示的内容看成是 sql语句,但是实际上它不是我们所熟知的sql语句。它是一个已经把sql语句解析了的、用sqlite自己标记记录的内部数据结构。
正因为这个结构已经被解析了,所以你可以往这个语句里插入二进制数据。当然,把二进制数据插到 sqlite3_stmt 结构里可不能直接 memcpy ,也不能像 std::string 那样用 + 号。必须用 sqlite 提供的函数来插入。
(1) 写入二进制
下面说写二进制的步骤。
要插入二进制,前提是这个表的字段的类型是 blob 类型。我假设有这么一张表:
create table Tbl_2( IDinteger, file_content blob )
首先声明
- sqlite3_stmt * stat;
然后,把一个 sql 语句解析到 stat 结构里去:
// sqlite3_prepare 接口把一条SQL语句编译成字节码留给后面的执行函数. 使用该接口访问数据库是当前比较好的的一种方法.
- sqlite3_prepare( db,“insert into Tbl_2( ID, file_content) values( 10, ? )”, -1, &stat, 0 );
上面的函数完成 sql 语句的解析。第一个参数跟前面一样,是个 sqlite3 * 类型变量,第二个参数是一个 sql 语句。
这个 sql 语句特别之处在于 values 里面有个? 号。在sqlite3_prepare函数里,?号表示一个未定的值,它的值等下才插入。第三个参数我写的是-1,这个参数含义是前面 sql 语句的长度。如果小于0,sqlite会自动计算它的长度(把sql语句当成以\0结尾的字符串)。第四个参数是 sqlite3_stmt 的指针的指针。解析以后的sql语句就放在这个结构里。
第五个参数我也不知道是干什么的。为0就可以了。
如果这个函数执行成功(返回值是 SQLITE_OK 且 stat 不为NULL ),那么下面就可以开始插入二进制数据。
- sqlite3_bind_blob(stat, 1, pdata, (int)(length_of_data_in_bytes), NULL ); // pdata为数据缓冲区,length_of_data_in_bytes为数据大小,以字节为单位
这个函数一共有5个参数。
第1个参数:是前面prepare得到的 sqlite3_stmt * 类型变量。
第2个参数:?号的索引。前面prepare的sql语句里有一个?号,假如有多个?号怎么插入?方法就是改变 bind_blob 函数第2个参数。这个参数我写1,表示这里插入的值要替换stat 的第一个?号(这里的索引从1开始计数,而非从0开始)。如果你有多个?号,就写多个 bind_blob 语句,并改变它们的第2个参数就替换到不同的?号。如果有?号没有替换,sqlite为它取值null。
第3个参数:二进制数据起始指针。
第4个参数:二进制数据的长度,以字节为单位。
第5个参数:是个析够回调函数,告诉sqlite当把数据处理完后调用此函数来析够你的数据。这个参数我还没有使用过,因此理解也不深刻。但是一般都填NULL,需要释放的内存自己用代码来释放。
bind完了之后,二进制数据就进入了你的“sql语句”里了。你现在可以把它保存到数据库里:
虚拟机执行字节码,执行过程是一个步进(stepwise)的过程,每一步(step)由sqlite3_step()启动,并由VDBE(sqlite虚拟机)执行一段字节 码。由sqlite3_prepare编译字节代码,并由sqlite3_step()启动虚拟机执行。在遍历结果集的过程中,它返回SQLITE_ROW,当到达结果末尾时,返回SQLITE_DONE
- int result =sqlite3_step( stat );
通过这个语句,stat 表示的sql语句就被写到了数据库里。
最后,要把 sqlite3_stmt结构给释放:sqlite3_finalize( stat ); //把刚才分配的内容析构掉
(2) 读出二进制
下面说读二进制的步骤。
跟前面一样,
先声明 sqlite3_stmt *类型变量:
- sqlite3_stmt * stat;
然后,把一个 sql 语句解析到 stat 结构里去:
- sqlite3_prepare( db,“select * from Tbl_2”, -1,&stat, 0 );
当 prepare 成功之后(返回值是 SQLITE_OK ),开始查询数据。
- int result =sqlite3_step( stat );
这一句的返回值是SQLITE_ROW 时表示成功(不是 SQLITE_OK )。
你可以循环执行 sqlite3_step 函数,一次 step 查询出一条记录。直到返回值不为 SQLITE_ROW 时表示查询结束。
然后开始获取第一个字段:ID 的值。ID是个整数,用下面这个语句获取它的值:
int id =sqlite3_column_int( stat, 0 ); //第2个参数表示获取第几个字段内容,从0开始计算,因为我的表的ID字段是第一个字段,因此这里我填0
下面开始获取 file_content 的值,因为 file_content 是二进制,因此我需要得到它的指针,还有它的长度:
- const void * pFileContent =sqlite3_column_blob( stat, 1 );
- int len = sqlite3_column_bytes( stat, 1 );
这样就得到了二进制的值。
把 pFileContent 的内容保存出来之后,
不要忘了释放sqlite3_stmt 结构:
- sqlite3_finalize( stat ); //把刚才分配的内容析构掉
(3) 重复使用 sqlite3_stmt 结构
如果你需要重复使用sqlite3_prepare 解析好的 sqlite3_stmt 结构,需要用函数:sqlite3_reset。
- result = sqlite3_reset(stat);
这样, stat 结构又成为sqlite3_prepare 完成时的状态,你可以重新为它bind 内容。
(4) 事务处理
sqlite 是支持事务处理的。如果你知道你要同步删除很多数据,不仿把它们做成一个统一的事务。通常一次 sqlite3_exec 就是一次事务,如果你要删除1万条数据,sqlite就做了1万次:开始新事务->删除一条数据->提交事务->开始新事务->… 的过程。这个操作是很慢的。因为时间都花在了开始事务、提交事务上。你可以把这些同类操作做成一个事务,这样如果操作错误,还能够回滚事务。
事务的操作没有特别的接口函数,它就是一个普通的 sql 语句而已:
分别如下:
- int result;
- result =sqlite3_exec( db, “begin transaction”, 0, 0, &zErrorMsg ); //开始一个事务
- result =sqlite3_exec( db, “commit transaction”, 0, 0, &zErrorMsg ); //提交事务
- result = sqlite3_exec(db, “rollback transaction”, 0, 0, &zErrorMsg ); //回滚事务
(3) 补充
基本上,使用sqlite3_open, sqlite3_close, sqlite3_exec这三个函数,可以完成大大部分的工作。但还不完善。上面的例子中,都是直接以sql语句的形式来操作数据库,这样很容易被注入。所以有必要使用sql参数。
sqlite3_prepare
sqlite3_bind_*
sqlite3_step
sqlite3_column_*
struct sqlite3_stmt
sqlite3_finalize
sqlite3_prepare用来编译sql语句。sql语句被执行之前,必须先编译成字节码。S
qlite3_stmt是一个结构体,表示sql语句编译后的字节码。
sqlite3_step用来执行编译后的sql语句。
sqlite3_bind_*用于将sql参数绑定到sql语句。
sqlite3_column_*用于从查询的结果中获取数据。
sqlite3_finalize用来释放sqlite3_stmt对象。
代码最能说明函数的功能,
下面就用一个例子来演示吧~~
- //———————————————-
- //sqlite3_prepare, sqlite3_bind_*,
- //sqlite3_step, sqlite3_column_*,
- //sqlite3_column_type
- //sqlite3_stmt, sqlite3_finalize,sqlite3_reset
- //查询
- //———————————————-
- sqlite3 *conn = NULL;
- sqlite3_stmt *stmt = NULL;
- const char *err_msg = NULL;
- // 列数据类型
- char col_types[][10] = { “”,“Integer”, “Float”, “Text”, “Blob”, “NULL”};
- sqlite3_open(“test.db”,&conn);
- sqlite3_prepare(conn, “SELECT * FROM[test_for_cpp] WHERE [id]>?”, -1, &stmt, &err_msg);
- sqlite3_bind_int(stmt, 1, 5);
- while (SQLITE_ROW == sqlite3_step(stmt))
- {
- int col_count = sqlite3_column_count(stmt); // 结果集中列的数量
- const char *col_0_name = sqlite3_column_name(stmt, 0); // 获取列名
- int id = sqlite3_column_int(stmt, 0);
- int id_type = sqlite3_column_type(stmt, 0); // 获取列数据类型
- const char *col_2_name = sqlite3_column_name(stmt, 2);
- int age = sqlite3_column_int(stmt, 2);
- int age_type = sqlite3_column_type(stmt, 2);
- const char *col_1_name = sqlite3_column_name(stmt, 1);
- char name[80];
- strncpy(name, (const char *)sqlite3_column_text(stmt, 1), 80);
- int name_type = sqlite3_column_type(stmt, 1);
- // 打印结果
- printf(“col_count: %d, %s = %d(%s), %s = %s(%s), %s = %d(%s)\n”,
- col_count, col_0_name, id, col_types[id_type], col_2_name, name,
- col_types[name_type], col_1_name, age, col_types[age_type]);
- }
- sqlite3_finalize(stmt); // 释放sqlite3_stmt
- sqlite3_close(conn);
这段代码查询id号大于5的所有记录,并显示到控制台,最后效果为
Sqlite c/c++ api学习 -stanfordxu – stanfordxu的博客其他函数
在上面的例子中,还使用了其他的一些函数,如:
sqlite3_column_count用于获取结果集中列的数量;
sqlite3_column_name用于获取列的名称;
sqlite3_column_type用于获取列的数据类型;
sqlite3_errcode用于获取最近一次操作出错的错误代码;
sqlite3_errmsg用于获取最近一次操作出错的错误说明。 sqlite的api中还有很多的函数,有了上面的基础,相信你通过查询官方的文档,能迅速掌握本文未介绍的api。
字符串编码
在官网上查看Sqlite的api的时候,发现有很同函数的名称都非常相似,只是最后添加了”_16”,如:sqlite3_open和 sqlite3_open16, sqlite3_errmsg和sqlite3_errmsg16,等等。其实添加了”16”后缀的函数,主要用于支持utf-16编码的字符串。如 sqlite3_open16可以接收utf-16编码的数据库路径。
在sourceforge上,有一个开源的项目sqlitex,它封装了这些api,使对sqlite数据库的操作更加方便。sqlitex的源代码非常的简单,感兴趣的同学可以下载下来自己研究。
- /////////////////////////////////////////////////// 另外一个代码 ///////////////////////////////////////////////
- #include <stdio.h>
- #include <stdlib.h>
- #include “sqlite3.h”
- #include <string.h>
- int main(int argc, char **argv)
- {
- int rc, i, ncols;
- sqlite3 *db;
- sqlite3_stmt *stmt;
- char *sql;
- const char *tail;
- //打开数据
- rc = sqlite3_open(“foods.db”, &db);
- if(rc) {
- fprintf(stderr, “Can’t open database: %s\n”,
- sqlite3_errmsg(db));
- sqlite3_close(db);
- exit(1);
- }
- sql = “select * from episodes”;
- //预处理
- rc = sqlite3_prepare(db, sql, (int)strlen(sql), &stmt, &tail);
- if(rc != SQLITE_OK) {
- fprintf(stderr, “SQL error: %s\n”, sqlite3_errmsg(db));
- }
- rc = sqlite3_step(stmt);
- ncols = sqlite3_column_count(stmt);
- while(rc == SQLITE_ROW) {
- for(i=0; i < ncols; i++) {
- fprintf(stderr, “‘%s’ “, sqlite3_column_text(stmt, i));
- }
- fprintf(stderr, “\n”);
- rc = sqlite3_step(stmt);
- }
- //释放statement
- sqlite3_finalize(stmt);
- //关闭数据库
- sqlite3_close(db);
- return 0;
- //=====================================================================
四、 给数据库加密
前面所说的内容网上已经有很多资料,虽然比较零散,但是花点时间也还是可以找到的。现在要说的这个——数据库加密,资料就很难找。也可能是我操作水平不够,找不到对应资料。但不管这样,我还是通过网上能找到的很有限的资料,探索出了给sqlite数据库加密的完整步骤。
这里要提一下,虽然 sqlite 很好用,速度快、体积小巧。但是它保存的文件却是明文的。若不信可以用NotePad 打开数据库文件瞧瞧,里面insert 的内容几乎一览无余。这样赤裸裸的展现自己,可不是我们的初衷。当然,如果你在嵌入式系统、智能手机上使用 sqlite,最好是不加密,因为这些系统运算能力有限,你做为一个新功能提供者,不能把用户有限的运算能力全部花掉。
Sqlite为了速度而诞生。因此Sqlite本身不对数据库加密,要知道,如果你选择标准AES算法加密,那么一定有接近50%的时间消耗在加解密算法上,甚至更多(性能主要取决于你算法编写水平以及你是否能使用cpu提供的底层运算能力,比如MMX或sse系列指令可以大幅度提升运算速度)。
Sqlite免费版本是不提供加密功能的,当然你也可以选择他们的收费版本,那你得支付2000块钱,而且是USD。我这里也不是说支付钱不好,如果只为了数据库加密就去支付2000块,我觉得划不来。因为下面我将要告诉你如何为免费的Sqlite扩展出加密模块——自己动手扩展,这是Sqlite允许,也是它提倡的。
那么,就让我们一起开始为 sqlite3.c 文件扩展出加密模块。
1、 必要的宏
通过阅读 Sqlite 代码(当然没有全部阅读完,6万多行代码,没有一行是我习惯的风格,我可没那么多眼神去看),我搞清楚了两件事:
Sqlite是支持加密扩展的;
需要 #define 一个宏才能使用加密扩展。
这个宏就是 SQLITE_HAS_CODEC。
你在代码最前面(也可以在 sqlite3.h 文件第一行)定义:
- #ifndef SQLITE_HAS_CODEC
- #define SQLITE_HAS_CODEC
- #endif
如果你在代码里定义了此宏,但是还能够正常编译,那么应该是操作没有成功。因为你应该会被编译器提示有一些函数无法链接才对。如果你用的是 VC 2003,你可以在“解决方案”里右键点击你的工程,然后选“属性”,找到“C/C++”,再找到“命令行”,在里面手工添加“/D “SQLITE_HAS_CODEC””。
定义了这个宏,一些被 Sqlite 故意屏蔽掉的代码就被使用了。这些代码就是加解密的接口。
尝试编译,vc会提示你有一些函数无法链接,因为找不到他们的实现。
如果你也用的是VC2003,那么会得到下面的提示:
error LNK2019: 无法解析的外部符号 _sqlite3CodecGetKey ,该符号在函数 _attachFunc 中被引用
error LNK2019: 无法解析的外部符号 _sqlite3CodecAttach ,该符号在函数 _attachFunc 中被引用
error LNK2019: 无法解析的外部符号 _sqlite3_activate_see ,该符号在函数 _sqlite3Pragma 中被引用
error LNK2019: 无法解析的外部符号 _sqlite3_key ,该符号在函数 _sqlite3Pragma 中被引用
fatal error LNK1120: 4 个无法解析的外部命令
这是正常的,因为Sqlite只留了接口而已,并没有给出实现。
下面就让我来实现这些接口。
2、 自己实现加解密接口函数
如果真要我从一份 www.sqlite.org 网上down下来的 sqlite3.c 文件,直接摸索出这些接口的实现,我认为我还没有这个能力。
好在网上还有一些代码已经实现了这个功能。通过参照他们的代码以及不断编译中vc给出的错误提示,最终我把整个接口整理出来。
实现这些预留接口不是那么容易,要重头说一次怎么回事很困难。我把代码都写好了,直接把他们按我下面的说明拷贝到 sqlite3.c 文件对应地方即可。我在下面也提供了sqlite3.c 文件,可以直接参考或取下来使用。
这里要说一点的是,我另外新建了两个文件:crypt.c和crypt.h。
其中crypt.h如此定义:
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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
<pre name="code" class="cpp">#ifndef DCG_SQLITE_CRYPT_FUNC_ #define DCG_SQLITE_CRYPT_FUNC_ /*********** 董淳光写的 SQLITE 加密关键函数库 ***********/ /*********** 关键加密函数 ***********/ int My_Encrypt_Func( unsigned char * pData,unsigned int data_len, const char * key, unsigned int len_of_key ); /*********** 关键解密函数 ***********/ int My_DeEncrypt_Func( unsigned char * pData,unsigned int data_len, const char * key, unsigned int len_of_key ); #endif 其中的 crypt.c 如此定义: #include "./crypt.h" #include "memory.h" /*********** 关键加密函数 ***********/ int My_Encrypt_Func( unsigned char * pData,unsigned int data_len, const char * key, unsigned int len_of_key ) { return 0; } /*********** 关键解密函数 ***********/ int My_DeEncrypt_Func( unsigned char *pData, unsigned int data_len, const char * key, unsigned int len_of_key ) { return 0; }</pre> <pre></pre> <p></p> <pre></pre> <p></p> <p>这个文件很容易看,就两函数,一个加密一个解密。传进来的参数分别是待处理的数据、数据长度、密钥、密钥长度。</p> <p>处理时直接把结果作用于 pData 指针指向的内容。</p> <p>你需要定义自己的加解密过程,就改动这两个函数,其它部分不用动。扩展起来很简单。</p> <p>这里有个特点,data_len 一般总是 1024 字节。正因为如此,你可以在你的算法里使用一些特定长度的加密算法,比如AES要求被加密数据一定是128位(16字节)长。这个1024不是碰巧,而是 Sqlite的页定义是1024字节,在sqlite3.c文件里有定义:</p> <p></p> <pre name="code" class="cpp"># define SQLITE_DEFAULT_PAGE_SIZE 1024</pre> <p></p> <p>你可以改动这个值,不过还是建议没有必要不要去改它。</p> <p>上面写了两个扩展函数,如何把扩展函数跟 Sqlite 挂接起来,这个过程说起来比较麻烦。我直接贴代码。</p> <p>分3个步骤。</p> <p>首先,在 sqlite3.c 文件顶部,添加下面内容:</p> <p></p> <pre name="code" class="cpp">#ifdef SQLITE_HAS_CODEC #include "./crypt.h" /*********** 用于在 sqlite3 最后关闭时释放一些内存 ***********/ void sqlite3pager_free_codecarg(void*pArg); #endif</pre> <p></p> <p>这个函数之所以要在 sqlite3.c 开头声明,是因为下面在 sqlite3.c 里面某些函数里要插入这个函数调用。所以要提前声明。</p> <p>其次,在sqlite3.c文件里搜索“sqlite3PagerClose”函数,要找到它的实现代码(而不是声明代码)。</p> <p>实现代码里一开始是:</p> <p></p> <pre name="code" class="cpp">#ifdef SQLITE_ENABLE_MEMORY_MANAGEMENT /* A malloc()cannot fail in sqlite3ThreadData() as one or more calls to malloc() must have alreadybeen made by this thread **before it gets to this point. This means theThreadData must have been allocated already so that ThreadData.nAlloc can be **set. */ ThreadData *pTsd = sqlite3ThreadData(); assert( pPager ); assert( pTsd && pTsd->nAlloc ); #endif</pre> <p></p> <p>需要在这部分后面紧接着插入:</p> <p></p> <pre name="code" class="cpp">#ifdef SQLITE_HAS_CODEC sqlite3pager_free_codecarg(pPager->pCodecArg); #endif</pre> <p></p> <p>这里要注意,sqlite3PagerClose 函数大概也是 3.3.17版本左右才改名的,以前版本里是叫“sqlite3pager_close”。因此你在老版本sqlite代码里搜索“sqlite3PagerClose”是搜不到的。</p> <p>类似的还有“sqlite3pager_get”、“sqlite3pager_unref”、“sqlite3pager_write”、“sqlite3pager_pagecount”等都是老版本函数,它们在 pager.h 文件里定义。新版本对应函数是在 sqlite3.h 里定义(因为都合并到 sqlite3.c和sqlite3.h两文件了)。所以,如果你在使用老版本的sqlite,先看看 pager.h 文件,这些函数不是消失了,也不是新蹦出来的,而是老版本函数改名得到的。</p> <p>最后,往sqlite3.c 文件下找。找到最后一行:</p> <p>/************** End of main.c************************************************/</p> <p>在这一行后面,接上本文最下面的代码段。</p> <p>这些代码很长,我不再解释,直接接上去就得了。</p> <p>唯一要提的是 DeriveKey 函数。这个函数是对密钥的扩展。比如,你要求密钥是128位,即是16字节,但是如果用户只输入 1个字节呢?2个字节呢?或输入50个字节呢?你得对密钥进行扩展,使之符合16字节的要求。</p> <p>DeriveKey 函数就是做这个扩展的。有人把接收到的密钥求md5,这也是一个办法,因为md5运算结果固定16字节,不论你有多少字符,最后就是16字节。这是md5算法的特点。但是我不想用md5,因为还得为它添加包含一些 md5 的.c或.cpp文件。我不想这么做。我自己写了一个算法来扩展密钥,很简单的算法。当然,你也可以使用你的扩展方法,也而可以使用 md5 算法。只要修改DeriveKey 函数就可以了。</p> <p>在 DeriveKey 函数里,只管申请空间构造所需要的密钥,不需要释放,因为在另一个函数里有释放过程,而那个函数会在数据库关闭时被调用。参考我的 DeriveKey 函数来申请内存。</p> <p>这里我给出我已经修改好的 sqlite3.c 和 sqlite3.h 文件。</p> <p>如果太懒,就直接使用这两个文件,编译肯定能通过,运行也正常。当然,你必须按我前面提的,新建 crypt.h 和 crypt.c 文件,而且函数要按我前面定义的要求来做。</p> <h2><a name="t20"></a>3、 加密使用方法:</h2> <p>现在,你代码已经有了加密功能。</p> <p>你要把加密功能给用上,除了改 sqlite3.c 文件、给你工程添加 SQLITE_HAS_CODEC 宏,还得修改你的数据库调用函数。</p> <p>前面提到过,要开始一个数据库操作,必须先 sqlite3_open 。</p> <p>加解密过程就在 sqlite3_open 后面操作。</p> <p>假设你已经 sqlite3_open 成功了,紧接着写下面的代码:</p> <p></p> <pre name="code" class="cpp"> int i; //添加、使用密码 i = sqlite3_key( db,"dcg", 3 ); //修改密码 i = sqlite3_rekey( db,"dcg", 0 );</pre> <p></p> <p>用 sqlite3_key 函数来提交密码。</p> <p>第1个参数是 sqlite3 *类型变量,代表着用sqlite3_open 打开的数据库(或新建数据库)。</p> <p>第2个参数是密钥。</p> <p>第3个参数是密钥长度。</p> <p>用 sqlite3_rekey 来修改密码。参数含义同 sqlite3_key。</p> <p>实际上,你可以在sqlite3_open函数之后,到 sqlite3_close 函数之前任意位置调用 sqlite3_key 来设置密码。</p> <p>但是如果你没有设置密码,而数据库之前是有密码的,那么你做任何操作都会得到一个返回值:SQLITE_NOTADB,并且得到错误提示:“file is encrypted or is not a database”。</p> <p>只有当你用 sqlite3_key 设置了正确的密码,数据库才会正常工作。</p> <p>如果你要修改密码,前提是你必须先 sqlite3_open 打开数据库成功,然后 sqlite3_key 设置密钥成功,之后才能用 sqlite3_rekey 来修改密码。(源码网整理:<a href="http://www.codepub.com/">www.codepub.com</a>)</p> <p>如果数据库有密码,但你没有用 sqlite3_key 设置密码,那么当你尝试用 sqlite3_rekey 来修改密码时会得到 SQLITE_NOTADB 返回值。</p> <p>如果你需要清空密码,可以使用:</p> <p></p> <pre name="code" class="cpp">//修改密码 i = sqlite3_rekey( db, NULL, 0 );</pre> <p></p> <p>来完成密码清空功能。</p> <h2><a name="t21"></a>4、 sqlite3.c 最后添加代码段</h2> <p></p> <pre name="code" class="cpp">/*** 董淳光定义的加密函数 ***/ #ifdef SQLITE_HAS_CODEC /*** 加密结构 ***/ #define CRYPT_OFFSET 8 typedef struct _CryptBlock { BYTE* ReadKey; // 读数据库和写入事务的密钥 BYTE* WriteKey; // 写入数据库的密钥 int PageSize; // 页的大小 BYTE* Data; } CryptBlock, *LPCryptBlock; #ifndef DB_KEY_LENGTH_BYTE /*密钥长度*/ #define DB_KEY_LENGTH_BYTE 16 /*密钥长度*/ #endif #ifndef DB_KEY_PADDING /*密钥位数不足时补充的字符*/ #define DB_KEY_PADDING 0x33 /*密钥位数不足时补充的字符*/ #endif /*** 下面是编译时提示缺少的函数 ***/ /** 这个函数不需要做任何处理,获取密钥的部分在下面 DeriveKey 函数里实现 **/ void sqlite3CodecGetKey(sqlite3* db, intnDB, void** Key, int* nKey) { return ; } /*被sqlite 和 sqlite3_key_interop 调用, 附加密钥到数据库.*/ int sqlite3CodecAttach(sqlite3 *db, intnDb, const void *pKey, int nKeyLen); /** 这个函数好像是 sqlite3.3.17前不久才加的,以前版本的sqlite里没有看到 这个函数这个函数我还没有搞清楚是做什么的,它里面什么都不做直接返回,对加解密没有影响 **/ void sqlite3_activate_see(const char* right) { return; } int sqlite3_key(sqlite3 *db, const void*pKey, int nKey); int sqlite3_rekey(sqlite3 *db, const void*pKey, int nKey); /***下面是上面的函数的辅助处理函数***/ // 从用户提供的缓冲区中得到一个加密密钥 // 用户提供的密钥可能位数上满足不了要求,使用这个函数来完成密钥扩展 static unsigned char * DeriveKey(const void*pKey, int nKeyLen); //创建或更新一个页的加密算法索引.此函数会申请缓冲区. static LPCryptBlockCreateCryptBlock(unsigned char* hKey, Pager *pager, LPCryptBlock pExisting); //加密/解密函数, 被pager调用 void * sqlite3Codec(void *pArg, unsignedchar *data, Pgno nPageNum, int nMode); //设置密码函数 int __stdcall sqlite3_key_interop(sqlite3*db, const void *pKey, int nKeySize); // 修改密码函数 int __stdcall sqlite3_rekey_interop(sqlite3*db, const void *pKey, int nKeySize); //销毁一个加密块及相关的缓冲区,密钥. static void DestroyCryptBlock(LPCryptBlockpBlock); static void * sqlite3pager_get_codecarg(Pager*pPager); void sqlite3pager_set_codec(Pager*pPager,void *(*xCodec)(void*,void*,Pgno,int),void *pCodecArg ); //加密/解密函数, 被pager调用 void * sqlite3Codec(void *pArg, unsignedchar *data, Pgno nPageNum, int nMode) { LPCryptBlock pBlock = (LPCryptBlock)pArg; unsigned int dwPageSize = 0; if (!pBlock) return data; // 确保pager的页长度和加密块的页长度相等.如果改变,就需要调整. if (nMode != 2) { PgHdr *pageHeader; pageHeader =DATA_TO_PGHDR(data); if(pageHeader->pPager->pageSize != pBlock->PageSize) { CreateCryptBlock(0,pageHeader->pPager, pBlock); } } switch(nMode) { case 0: // Undo a "case 7" journal file encryption case 2: //重载一个页 case 3: //载入一个页 if (!pBlock->ReadKey)break; dwPageSize =pBlock->PageSize; My_DeEncrypt_Func(data,dwPageSize, pBlock->ReadKey, DB_KEY_LENGTH_BYTE ); /*调用我的解密函数*/ break; case 6: //加密一个主数据库文件的页 if (!pBlock->WriteKey)break; memcpy(pBlock->Data +CRYPT_OFFSET, data, pBlock->PageSize); data = pBlock->Data +CRYPT_OFFSET; dwPageSize =pBlock->PageSize; My_Encrypt_Func(data ,dwPageSize, pBlock->WriteKey, DB_KEY_LENGTH_BYTE ); /*调用我的加密函数*/ break; case 7: //加密事务文件的页 /*在正常环境下, 读密钥和写密钥相同. 当数据库是被重新加密的,读密钥和写密钥未必相同.回滚事务必要用数据库文件的原始密钥写入.因此,当一次回滚被写入,总是用数据库的读密钥,这是为了保证与读取原始数据的密钥相同. */ if (!pBlock->ReadKey)break; memcpy(pBlock->Data +CRYPT_OFFSET, data, pBlock->PageSize); data = pBlock->Data +CRYPT_OFFSET; dwPageSize =pBlock->PageSize; My_Encrypt_Func( data,dwPageSize, pBlock->ReadKey, DB_KEY_LENGTH_BYTE ); /*调用我的加密函数*/ break; } return data; } //销毁一个加密块及相关的缓冲区,密钥. static void DestroyCryptBlock(LPCryptBlock pBlock) { //销毁读密钥. if (pBlock->ReadKey){ sqliteFree(pBlock->ReadKey); } //如果写密钥存在并且不等于读密钥,也销毁. if (pBlock->WriteKey && pBlock->WriteKey !=pBlock->ReadKey){ sqliteFree(pBlock->WriteKey); } if(pBlock->Data){ sqliteFree(pBlock->Data); } //释放加密块. sqliteFree(pBlock); } static void *sqlite3pager_get_codecarg(Pager *pPager) { return(pPager->xCodec) ? pPager->pCodecArg: NULL; } // 从用户提供的缓冲区中得到一个加密密钥 static unsigned char * DeriveKey(const void*pKey, int nKeyLen) { unsigned char * hKey = NULL; int j; if( pKey == NULL || nKeyLen == 0 ) { return NULL; } hKey = sqliteMalloc( DB_KEY_LENGTH_BYTE + 1); if( hKey == NULL ) { return NULL; } hKey[ DB_KEY_LENGTH_BYTE ] = 0; if( nKeyLen < DB_KEY_LENGTH_BYTE ) { memcpy( hKey, pKey, nKeyLen ); //先拷贝得到密钥前面的部分 j = DB_KEY_LENGTH_BYTE - nKeyLen; //补充密钥后面的部分 memset( hKey + nKeyLen, DB_KEY_PADDING, j ); } else { //密钥位数已经足够,直接把密钥取过来 memcpy( hKey, pKey,DB_KEY_LENGTH_BYTE ); } return hKey; } //创建或更新一个页的加密算法索引.此函数会申请缓冲区. static LPCryptBlockCreateCryptBlock(unsigned char* hKey, Pager *pager, LPCryptBlock pExisting) { LPCryptBlock pBlock; if (!pExisting) //创建新加密块 { pBlock = sqliteMalloc(sizeof(CryptBlock)); memset(pBlock, 0, sizeof(CryptBlock)); pBlock->ReadKey = hKey; pBlock->WriteKey = hKey; pBlock->PageSize = pager->pageSize; pBlock->Data = (unsigned char*)sqliteMalloc(pBlock->PageSize +CRYPT_OFFSET); } else //更新存在的加密块 { pBlock = pExisting; if ( pBlock->PageSize != pager->pageSize &&!pBlock->Data){ sqliteFree(pBlock->Data); pBlock->PageSize = pager->pageSize; pBlock->Data = (unsigned char*)sqliteMalloc(pBlock->PageSize +CRYPT_OFFSET); } } memset(pBlock->Data, 0,pBlock->PageSize + CRYPT_OFFSET); return pBlock; } /* ** Set the codec for this pager */ void sqlite3pager_set_codec(Pager *pPager,void *(*xCodec)(void*,void*,Pgno,int), void *pCodecArg ) { pPager->xCodec = xCodec; pPager->pCodecArg = pCodecArg; } int sqlite3_key(sqlite3 *db, const void*pKey, int nKey) { returnsqlite3_key_interop(db, pKey, nKey); } int sqlite3_rekey(sqlite3 *db, const void*pKey, int nKey) { returnsqlite3_rekey_interop(db, pKey, nKey); } /*被sqlite 和 sqlite3_key_interop 调用, 附加密钥到数据库.*/ int sqlite3CodecAttach(sqlite3 *db, intnDb, const void *pKey, int nKeyLen) { int rc = SQLITE_ERROR; unsigned char* hKey = 0; //如果没有指定密匙,可能标识用了主数据库的加密或没加密. if (!pKey || !nKeyLen) { if (!nDb) { return SQLITE_OK; //主数据库, 没有指定密钥所以没有加密. } else //附加数据库,使用主数据库的密钥. { //获取主数据库的加密块并复制密钥给附加数据库使用 LPCryptBlock pBlock = (LPCryptBlock)sqlite3pager_get_codecarg(sqlite3BtreePager(db->aDb[0].pBt)); if (!pBlock) return SQLITE_OK; //主数据库没有加密 if (!pBlock->ReadKey) return SQLITE_OK; //没有加密 memcpy(pBlock->ReadKey, &hKey, 16); } } else //用户提供了密码,从中创建密钥. { hKey = DeriveKey(pKey, nKeyLen); } //创建一个新的加密块,并将解码器指向新的附加数据库. if (hKey) { LPCryptBlock pBlock = CreateCryptBlock(hKey,sqlite3BtreePager(db->aDb[nDb].pBt), NULL); sqlite3pager_set_codec(sqlite3BtreePager(db->aDb[nDb].pBt), sqlite3Codec,pBlock); rc = SQLITE_OK; } return rc; } // Changes the encryption key for anexisting database. int __stdcall sqlite3_rekey_interop(sqlite3*db, const void *pKey, int nKeySize) { Btree *pbt = db->aDb[0].pBt; Pager *p = sqlite3BtreePager(pbt); LPCryptBlock pBlock =(LPCryptBlock)sqlite3pager_get_codecarg(p); unsigned char * hKey = DeriveKey(pKey,nKeySize); int rc = SQLITE_ERROR; if (!pBlock && !hKey) returnSQLITE_OK; //重新加密一个数据库,改变pager的写密钥, 读密钥依旧保留. if (!pBlock) //加密一个未加密的数据库 { pBlock = CreateCryptBlock(hKey, p, NULL); pBlock->ReadKey = 0; // 原始数据库未加密 sqlite3pager_set_codec(sqlite3BtreePager(pbt), sqlite3Codec, pBlock); } else // 改变已加密数据库的写密钥 { pBlock->WriteKey = hKey; } // 开始一个事务 rc = sqlite3BtreeBeginTrans(pbt, 1); if (!rc) { // 用新密钥重写所有的页到数据库。 Pgno nPage = sqlite3PagerPagecount(p); Pgno nSkip = PAGER_MJ_PGNO(p); void *pPage; Pgno n; for(n = 1; rc == SQLITE_OK && n <= nPage; n ++) { if (n == nSkip) continue; rc = sqlite3PagerGet(p, n, &pPage); if(!rc) { rc = sqlite3PagerWrite(pPage); sqlite3PagerUnref(pPage); } } } // 如果成功,提交事务。 if (!rc) { rc = sqlite3BtreeCommit(pbt); } // 如果失败,回滚。 if (rc) { sqlite3BtreeRollback(pbt); } // 如果成功,销毁先前的读密钥。并使读密钥等于当前的写密钥。 if (!rc) { if (pBlock->ReadKey) { sqliteFree(pBlock->ReadKey); } pBlock->ReadKey = pBlock->WriteKey; } else// 如果失败,销毁当前的写密钥,并恢复为当前的读密钥。 { if (pBlock->WriteKey) { sqliteFree(pBlock->WriteKey); } pBlock->WriteKey = pBlock->ReadKey; } // 如果读密钥和写密钥皆为空,就不需要再对页进行编解码。 // 销毁加密块并移除页的编解码器 if (!pBlock->ReadKey &&!pBlock->WriteKey) { sqlite3pager_set_codec(p, NULL, NULL); DestroyCryptBlock(pBlock); } return rc; } /***下面是加密函数的主体***/ int __stdcall sqlite3_key_interop(sqlite3*db, const void *pKey, int nKeySize) { return sqlite3CodecAttach(db, 0, pKey, nKeySize); } // 释放与一个页相关的加密块 void sqlite3pager_free_codecarg(void *pArg) { if (pArg) DestroyCryptBlock((LPCryptBlock)pArg); } #endif //#ifdef SQLITE_HAS_CODEC</pre> <p></p> <pre></pre> |
SQLITE3 加密总结 (sqlite 3.6.12版本)
董淳光 42530 (老工号)
dcg1981@163.com
2009年4月9日星期四
I. 序 1
II. 问题初分析 2
III. 研究结果(附代码) 3
IV. 可靠性 17
V. 关键点说明 17
I. 序
时隔上次写《Sqlite3 使用总结》已过去2年。这2年时间我做了好些对自己人生影响很大的事情。
不扯太远了。2年来一直想把 sqlite3 的加密搞清楚一些,但一直没时间去做。这两天终于有空坐下来研究 sqlite3 的加密方法。有点收获。记录下来免得忘记。
我写本文章时,sqlite3 最新版本是 3.6.12 。我就以这个版本的源代码为例进行分析。并且,我喜欢它那整合代码,整合成一个 .c 和一个 .h 文件。虽然在 vc2003 里编辑慢如蜗牛,但是一旦编辑好,以后使用起来不至于每个工程都拖上一堆文件。工程简洁方便。所以下面的叙述全部都是以 sqlite3 v3.6.12 整合的源代码为基础展开。
我认为未来 sqlite3 v3.XX 的整合版本大体上都可以用下面介绍的代码进行加解密。Sqlite3 在版本变化中,有一些宏、函数被改名,读者很容易查出来并自己修正。也有一些函数会被丢弃。读者应该也可以自行分析出来。我下面的代码尽可能保持与 sqlite 版本的兼容性。不使用那些容易被丢弃的结构或函数。这样以后就不会常常有人发邮件咨询我能否制作一个最新版本sqlite加密了。
II. 问题初分析
首先是要理清 sqlite3 加解密思路。这点在2年前就没有做到位。
我本人愚笨,不擅长凭空分析太抽象的事情。对于软件问题最希望的就是边看现象边理解。分析 sqlite3 加密思路也采用具体现象具体分析的方法。
还是 vc2003 编译器(使用其它编译器的读者可以自行对应着设置,我下面的叙述并没有使用过多的vc2003 编译技巧,对应到 vc2005、2008,甚至 unix 下的 cc 等等都是通用的)。首先在工程属性里的“C/C++”选项里找到“命令行”,如图:
自己手工敲上“/D “SQLITE_HAS_CODEC””。 这个意思是在整个工程里预先 define “SQLITE_HAS_CODEC”这个宏。
这个宏是 sqlite3 作者留下的加密接口。缺省没有这个宏, sqlite3 就是不加密的标准版。定义有这个宏,那么就是要开启加密功能。
设定好宏后,开始编译。
结果,问题现象就出现了:
testSqliteEnc error LNK2019: 无法解析的外部符号 _sqlite3_activate_see ,该符号在函数 _sqlite3Pragma 中被引用
testSqliteEnc error LNK2019: 无法解析的外部符号 _sqlite3_key ,该符号在函数 _sqlite3Pragma 中被引用
testSqliteEnc error LNK2019: 无法解析的外部符号 _sqlite3_rekey ,该符号在函数 _sqlite3Pragma 中被引用
testSqliteEnc error LNK2019: 无法解析的外部符号 _sqlite3CodecAttach ,该符号在函数 _attachFunc 中被引用
testSqliteEnc error LNK2019: 无法解析的外部符号 _sqlite3CodecGetKey ,该符号在函数 _attachFunc 中被引用
意思很明显,有这些函数的定义,但作者没有实现这些函数。需要我们自己实现。反之,大概实现了这些函数后就能进行加密了。
III. 研究结果
参阅了网上某文章,照着他的做法的确可以做出加密效果。在这里十分感谢那文章的作者。
但我并不满足于此。我无法确认人家的代码有没有问题。而且看过他代码,觉得封装程度也并不好。
于是希望通过一些研究判断人家代码可靠性。同时更进一步进行封装。
研究过程不再赘述,无非就是简单编写先前编译缺少的那些函数体,再对照网上代码编写那些函数内容,然后反复参考、单步调试sqlite3源代码。
这里直接给出最终结果。下面章节再简述关键点。
我本想做到另外建立配套程序文件来实现,力求跟原始 sqlite.c 文件全兼容,但可惜的是加解密需要操作sqlite内部数据结构以及内部函数,这些函数和结构没有在 sqlite3.h 头文件里定义。所以我不得不把代码写到了 sqlite3.c 文件里。
最终结果虽然是要修改 sqlite.c 文件,但我已经让修改地方尽量少。这样改起来不容易出错。
修改地方总共加有 3 处,这3处都是添加代码,不需要修改,也没有删除。
需要添加的代码下面全部都有。Sqlite3.c 里需要添加代码的位置大概如下图所示:
下面列出源码。
打开 sqlite3.c 文件。在最顶端添加:
#ifdef SQLITE_HAS_CODEC
void DestroyKeyInBtree(void * lpTree );
#define MY_DESTROY_KEY(BTREE) DestroyKeyInBtree (BTREE)
#else
#define MY_DESTROY_KEY(BTREE)
#endif
这段代码用于释放我们自己分配的密钥数据块。密钥数据块都是我们申请的内存,当数据库关闭时,我希望能释放密钥块内存,否则内存泄露。
但是,sqlite3没有提供数据库关闭进行释放密钥块的接口,得自己添加。
根据网上代码分析,我们释放密钥块时需要获得指定的数据库 BTree 结构体。因为每个密钥块是关联到特定的数据库BTree结构体上的。
通过分析代码,不难找到 sqlite3_close 会调用下面这个函数
SQLITE_PRIVATE int sqlite3BtreeClose(Btree *p)
这是 sqlite3 的函数。我确认销毁密钥块的时机就是在这个函数里。
搜索到这代码:
sqlite3BtreeRollback(p);
sqlite3BtreeLeave(p);
就在 Leave 后面添加我们销毁功能,如下:
/* Rollback any active transaction and free the handle structure.
The call to sqlite3BtreeRollback() drops any table-locks held by
this handle.
*/
sqlite3BtreeRollback(p);
sqlite3BtreeLeave(p);
MY_DESTROY_KEY(p ); // 我们添加的删除密钥的功能
/* If there are still other outstanding references to the shared-btree
structure, return now. The remainder of this procedure cleans
up the shared-btree.
*/
assert(p->wantToLock==0 && p->locked==0 );
这个销毁时机不能再往后放了,再往后BTree相关结构体都已被销毁,我们再从它里面获取我们密钥块指针就会崩溃。因此在我上面代码那里销毁时机刚好。
接下来修改就简单了。
直接拖到最末尾,3.6.12 版本的最后两行有效代码应该是:
#endif /* defined(SQLITE_ENABLE_ICU) */
#endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3) */
就在最后那个 endif 后面添加我下面列出来的所有代码。
//////////////////////////////////////////////////////////////////////////
////////////////////下面是我们自己添加的加密函数实现//////////////////////
//////////////////////我认为每个版本的整合sqlite//////////////////////////
////////////////////////只要添加这些函数就可以实现加密功能////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
#ifndef SQLITE_HAS_CODEC
#ifdef WIN32
#pragma message(“We do not use encryption”)
#endif
#else
#include “./Encrypt.h”
#ifdef WIN32
#pragma message(“&&&&&&&& We use encryption ! &&&&&&&&”)
#endif
/*
下面结构体是我们封装的一个中间结构体
目的是为了给用户加解密环境提供更为独立的功能,不依赖一些 sqlite 莫名其妙设计带来的骚扰
结构体中的 lpUserBlock 才是用户自己 DeriveKey 返回的结构体
*/
struct _TmpUserKeyBlock
{
void * lpUserBlock_old;
void * lpUserBlock_new;
};
typedef struct _TMP_ENCRYPT_BLOCK
{
struct _TmpUserKeyBlock lpUserKey;
int iPageSize;
int iEncryptingBufSize;
char pEncryptingBuf[1];
} TMP_ENCRYPT_BLOCK, * PTMP_ENCRYPT_BLOCK;
/*
下面是对用户 DeriveKey、CopyKey、DestroyKey 的封装。
封装目的是sqlite解密和加密操作的目标数据内存是不一样的。如果不加以封装,后面编写加解密函数就
不得不考虑这些细节。封装后用户函数就不需要再考虑这些部分了。
*/
PTMP_ENCRYPT_BLOCK DeriveKey_Tmp(const void *pKey, int nKeyLen, int iPageSize )
{
void * pUserKey;
PTMP_ENCRYPT_BLOCK pTmpBlock;
int iSize = iPageSize + 128;
if( NULL == pKey || 0 == nKeyLen )
{
return NULL;
}
pTmpBlock = (PTMP_ENCRYPT_BLOCK)malloc( sizeof(TMP_ENCRYPT_BLOCK) + iSize );
if( NULL == pTmpBlock )
{
return NULL;
}
pTmpBlock->iEncryptingBufSize = iSize; // +128 是为了多分配一些内存,免得跟sqlite打交道哪里出现溢出
pTmpBlock->iPageSize = iPageSize;
pUserKey = (void*)DeriveKey( pKey, nKeyLen, iPageSize );
if( NULL == pUserKey )
{
free( pTmpBlock );
return NULL;
}
// 刚开始时,新旧密钥应该都一致
pTmpBlock->lpUserKey.lpUserBlock_old = pUserKey;
pTmpBlock->lpUserKey.lpUserBlock_new = pUserKey;
return pTmpBlock;
}
// 复制一份数据库
PTMP_ENCRYPT_BLOCK CopyKey_Tmp( const PTMP_ENCRYPT_BLOCK lpKey )
{
PTMP_ENCRYPT_BLOCK pNew;
void * pUserKey;
int iSize;
if( NULL == lpKey )
return NULL;
iSize = lpKey->iEncryptingBufSize;
pNew = (PTMP_ENCRYPT_BLOCK)malloc( iSize + sizeof(TMP_ENCRYPT_BLOCK) );
if( NULL == pNew )
return NULL;
pUserKey = CopyKey( lpKey->lpUserKey.lpUserBlock_new );
if( NULL == pUserKey )
{
free( pNew );
return NULL;
}
pNew->lpUserKey.lpUserBlock_old = pUserKey;
pNew->lpUserKey.lpUserBlock_new = pUserKey;
pNew->iPageSize = lpKey->iPageSize;
pNew->iEncryptingBufSize = iSize;
return pNew;
}
// 释放密钥
void DestroyKey_Tmp( PTMP_ENCRYPT_BLOCK lpKey )
{
if( NULL == lpKey )
return;
if( NULL != lpKey->lpUserKey.lpUserBlock_new )
DestroyKey( lpKey->lpUserKey.lpUserBlock_new );
free( lpKey );
return;
}
// 文件加密解密的中间函数
// 本函数做简单处理之后会调用 My_sqlite3Codec 函数
// nMode 类型:
// case 0: // Undo a “case 7” journal file encryption
// case 2: // Reload a page
// case 3: // Load a page
// case 6: // Encrypt a page for the main database file
// case 7: // Encrypt a page for the journal file
void * My_sqlite3Codec_tmp(void *pArg, void *data, unsigned int nPageNum, int nMode)
{
PTMP_ENCRYPT_BLOCK lpTmpArg;
void *lpRet;
void * lpUserKey;
if (!pArg)
{
return data;
}
lpTmpArg = (PTMP_ENCRYPT_BLOCK) pArg;
if( nMode <= 5 )
{
// 解密
lpRet = data;
lpUserKey = lpTmpArg->lpUserKey.lpUserBlock_old;
}
else
{
// 加密
lpRet = lpTmpArg->pEncryptingBuf;
memcpy( lpRet, data, lpTmpArg->iPageSize );
if( nMode == 7 )
{
// 操作事务文件,需要用旧密钥
lpUserKey = lpTmpArg->lpUserKey.lpUserBlock_old;
}
else
{
// 否则就是写入数据库页面,用新密钥
lpUserKey = lpTmpArg->lpUserKey.lpUserBlock_new;
}
}
if( NULL == lpUserKey )
return lpRet;
My_sqlite3Codec( lpUserKey, lpRet, lpTmpArg->iPageSize, nPageNum, nMode );
return lpRet;
}
// 用于重设密钥
static void * My_sqlite3pager_get_codecarg(Pager *pPager)
{
return (pPager->xCodec) ? pPager->pCodecArg: NULL;
}
/*
设置数据库页面加解密参数
*/
void My_sqlite3pager_set_codec( Pager *pPager, void *(*xCodec)(void*,void*,Pgno,int), void *pCodecArg )
{
pPager->xCodec = xCodec;
pPager->pCodecArg = pCodecArg;
}
/*
** 返回数据库磁盘文件总页面数
** If the PENDING_BYTE lies on the page directly after the end of the
** file, then consider this page part of the file too. For example, if
** PENDING_BYTE is byte 4096 (the first byte of page 5) and the size of the
** file is 4096 bytes, 5 is returned instead of 4.
*/
int My_sqlite3pager_pagecount(Pager *pPager)
{
sqlite_int64 n;
assert( pPager );
if( pPager->dbSize >= 0 )
{
n = pPager->dbSize;
}
else
{
if( sqlite3OsFileSize(pPager->fd, &n) != SQLITE_OK )
{
pager_error( pPager, SQLITE_IOERR );
return 0;
}
if( n > 0 && n < pPager->pageSize )
{
n = 1;
}
else
{
n /= pPager->pageSize;
}
if( pPager->state!=PAGER_UNLOCK )
{
pPager->dbSize = (Pgno)n;
}
}
if( n==(PENDING_BYTE/pPager->pageSize) )
{
n++;
}
return (int)n;
}
// 设置密钥
void MySetKey( sqlite3 *db, const void *pKey, int nKey )
{
// 设置密钥简单,直接调用密钥设置接口就行了
sqlite3CodecAttach( db, 0, pKey, nKey );
}
// 重新设置密钥
int MyResetKey( sqlite3 *db, const void *pKey, int nKey )
{
Btree *pbt = db->aDb[0].pBt;
Pager *p = sqlite3BtreePager(pbt);
PTMP_ENCRYPT_BLOCK pNewKey = NULL;
PTMP_ENCRYPT_BLOCK pOldKey = (PTMP_ENCRYPT_BLOCK)My_sqlite3pager_get_codecarg(p);
void * lpUserOld = NULL;
int rc = SQLITE_ERROR;
// 总之都是要新分配一个新密钥空间的
pNewKey = DeriveKey_Tmp( pKey, nKey, (int)(p->pageSize) );
if( NULL == pOldKey && NULL == pNewKey )
{
return SQLITE_OK;
}
if( NULL == pOldKey )
{
pNewKey->lpUserKey.lpUserBlock_old = NULL;
My_sqlite3pager_set_codec(sqlite3BtreePager(pbt), My_sqlite3Codec_tmp, pNewKey);
}
else
{
lpUserOld = pOldKey->lpUserKey.lpUserBlock_new;
if( NULL != pNewKey )
pOldKey->lpUserKey.lpUserBlock_new = pNewKey->lpUserKey.lpUserBlock_new;
else
pOldKey->lpUserKey.lpUserBlock_new = NULL;
}
// 开始事务,加密应该要加密的页面
rc = sqlite3BtreeBeginTrans(pbt, 1);
if (!rc)
{
// 用新密钥重新加密并写入页面
Pgno nPage = My_sqlite3pager_pagecount( p );
Pgno nSkip = PAGER_MJ_PGNO(p);
void *pPage;
Pgno n;
for( n = 1; rc == SQLITE_OK && n <= nPage; n ++ )
{
if ( n == nSkip ) continue;
rc = sqlite3PagerGet(p, n, &pPage);
if( !rc )
{
rc = sqlite3PagerWrite(pPage);
sqlite3PagerUnref( pPage );
}
}
}
// 尝试提交事务
if (!rc)
{
rc = sqlite3BtreeCommit(pbt);
}
// 如果失败,就回滚事务
if (rc)
{
sqlite3BtreeRollback(pbt);
}
if( NULL != pOldKey )
{
if( NULL != lpUserOld )
{
pOldKey->lpUserKey.lpUserBlock_new = lpUserOld;
}
else
{
pOldKey->lpUserKey.lpUserBlock_new = pOldKey->lpUserKey.lpUserBlock_old;
}
DestroyKey_Tmp( pOldKey );
}
if( NULL != pNewKey )
{
pNewKey->lpUserKey.lpUserBlock_old = pNewKey->lpUserKey.lpUserBlock_new;
My_sqlite3pager_set_codec(sqlite3BtreePager(pbt), My_sqlite3Codec_tmp, pNewKey);
}
else
{
My_sqlite3pager_set_codec( p, NULL, NULL );
}
return rc;
}
// 这个函数我都不知道用来干什么的
// 好像相当于打日志,观察 zRight 结果的
void sqlite3_activate_see( const char* zRight )
{
return;
}
// 这个函数作用是把传入的用户 Key 信息设置写到特定的数据库相关结构去,保存
// sqlite3 里面包含n多库结构,就是里面那个 Db *aDb; 的数组结构, nDB 参数就是表示是第几个数组结构(nDB从0开始计算)
// 本函数要调用 sqlite3pager_set_codec 函数,把我们的加解密函数指针,以及密钥指针关联起来,关联方法类似:
// sqlite3pager_set_codec(sqlite3BtreePager(pbt), MyCodecFunc, lpKeyBlock);
int sqlite3CodecAttach(sqlite3* lpDB , int nDB, const void * lpUserKeyText, int iKeyLen )
{
int rc = SQLITE_ERROR;
PTMP_ENCRYPT_BLOCK lpBlock = NULL;
// 如果没有指定密钥数据,可能意味着使用主数据库加密方法,或不加密
if( !lpUserKeyText || iKeyLen <= 0 )
{
if( nDB <= 0 )
{
return SQLITE_OK; // 主数据库,又没有密钥,所以不加密
}
else // 否则是附加数据库,要采用主数据库密钥
{
// 先获得主数据库密钥,然后复制一份
// 然后用到附加数据库身上
PTMP_ENCRYPT_BLOCK pBlock = (PTMP_ENCRYPT_BLOCK)My_sqlite3pager_get_codecarg( sqlite3BtreePager(lpDB->aDb[0].pBt) );
if (!pBlock) return SQLITE_OK; // 主数据库也不加密,那么我们就不加密附加数据库了
if ( NULL == pBlock->lpUserKey.lpUserBlock_new ) return SQLITE_OK; // 看来不用加密
lpBlock = CopyKey_Tmp( pBlock );
if ( !lpBlock )
return rc; // 复制密钥失败!(应该是内存不够之类)
}
}
else // 用户提供了密钥串,那么就按要求创建密钥
{
Btree *pbt = lpDB->aDb[nDB].pBt;
Pager *p = sqlite3BtreePager(pbt);
lpBlock = DeriveKey_Tmp( lpUserKeyText, iKeyLen, (int)p->pageSize );
if( lpBlock == NULL )
{
My_sqlite3pager_set_codec( sqlite3BtreePager( lpDB->aDb[nDB].pBt ), NULL, NULL );
return 0;
}
}
// 将核心加解密函数设置到对应数据库
My_sqlite3pager_set_codec( sqlite3BtreePager( lpDB->aDb[nDB].pBt ), My_sqlite3Codec_tmp, lpBlock );
return 0;
}
// 删除密钥的功能
void DestroyKeyInBtree( Btree * lpTree )
{
Pager *p;
PTMP_ENCRYPT_BLOCK pBlock;
if( NULL == lpTree )
{
return;
}
p = sqlite3BtreePager(lpTree);
if( NULL == p )
return;
pBlock = (PTMP_ENCRYPT_BLOCK)My_sqlite3pager_get_codecarg(p);
if( NULL == pBlock )
return;
DestroyKey_Tmp( pBlock );
}
// 这个函数作用是设法从指定的库里重新分解出 sqlite3CodecAttach 函数设置的密钥信息
// sqlite3 里面包含n多库结构,就是里面那个 Db *aDb; 的数组结构, nDB 参数就是表示是第几个数组结构(nDB从0开始计算)
void sqlite3CodecGetKey(sqlite3* lpDB, int nDB, void** lplpUserKeyText, int* lpOutKeyLen )
{
return;
}
// 设置某数据库的密钥
// 用户ap真正调用的设置密码的函数就是这个
// 看参数名字就应该知道这是写参数含义了,更何况人家还做了这么充分的注释
int sqlite3_key( sqlite3 *db,/* Database to be rekeyed */ const void *pKey, int nKey /* The key */ )
{
MySetKey( db, pKey, nKey );
return SQLITE_OK;
}
// 重新设置某个数据库的密钥
// 用户ap真正调用的重设密码的函数就是这个
// 看参数名字就应该知道这是写参数含义了,更何况人家还做了这么充分的注释
int sqlite3_rekey( sqlite3 *db, /* Database to be rekeyed */ const void *pKey, int nKey /* The new key */ )
{
MyResetKey( db, pKey, nKey );
return SQLITE_OK;
}
#endif //SQLITE_HAS_CODEC
这段代码的使用说明:
1、 必须定义一个用户扩展加密文件 《Encrypt.h》。以及 Encrypt.c 。
以后要修改密码算法,密钥获得,就只修改这两个文件,对 sqlite3.c 文件不需要再做任何修改。
2、 Encrypt.h 里必须提供下面这些函数:
// 制作密钥。用户调用 sqlite3_key 和 sqlite3_rekey 时都会调用到本函数。
// 本函数通过用户提供的基本信息,构造出自己的密钥缓冲区(密钥块),函数里需要自己 malloc 一个
// 空间出来。设定好这个密钥块后将其指针返回出来。系统会在 rekey 或数据库关闭时调用 DestroyKey
// 函数来销毁这个密钥块。
// 参数 pKey 和 nKeyLen 是用户传入的基本密钥信息。后面 iPageSize 是 sqlite3 的页面大小,
// 通常 iPageSize 都是 1024。 Sqlite3 每次加解密一块数据大小都是这个长度。因此可以放心地
// 使用分块加解密算法。
LPCRYPTBLOCK DeriveKey(const void *pKey, int nKeyLen, int iPageSize );
// 复制一份密钥块,新复制出来的密钥块也必须是自己 malloc 出来的新空间,设定好后
// 将新结构体指针返回出来
LPCRYPTBLOCK CopyKey( const LPCRYPTBLOCK lpKey );
// 释放密钥。自己根据需要释放申请过的空间。
void DestroyKey( LPCRYPTBLOCK lpKey );
// 加解密函数核心,包括加密和解密
// 参数 pArg 是我们前面 DeriveKey 函数构造出来的密钥块。
// data 是用户数据区域, iDataSize 是此区域大小(通常就是页面大小,即1024字节)
// nPageNum 是当前页面在数据库里的下标,可以忽略。
// 加解密时直接读取,并写回到 data 指向的数据区。
// nMode 参数含义 (看起来,小于5的都是解密,大于5的都是加密。)
// case 0: // Undo a “case 7” journal file encryption
// case 2: // Reload a page
// case 3: // Load a page
// case 6: // Encrypt a page for the main database file
// case 7: // Encrypt a page for the journal file
void My_sqlite3Codec(void *pArg, void *data, int iDataSize, unsigned int nPageNum, int nMode);
3、 在 sqlite3_open 之后立刻使用 sqlite3_key 函数就可以设置数据库密码。
4、 如果需要修改数据库密码,则需要使用 sqlite3_rekey 函数。更改密码之前如果原数据库有密码,就要先用 sqlite3_key 函数设置过原始密码。
5、 如果数据库密码不正确,对数据库的所有操作都会被返回错误码 SQLITE_NOTADB,错误码含义是 “file is encrypted or is not a database” 。
6、 sqlite3_key 函数或 sqlite3_rekey 函数 如果传入的密钥信息为 NULL,或密钥信息长度为0,都表示不使用密钥、取消密钥、并解密原来数据库。
到此就可以完成加解密了。很简单。
这里附带我制作好的 sqlite3 (3.6.12) 。
IV. 可靠性
在没有加密功能(没有定义 “SQLITE_HAS_CODEC” 宏)的情况下,用 Purify 跑,发现 sqlite3 基本正常。
说基本正常是因为每次 sqlite3 尝试创建一个已经存在的表时都会出现 40KB 内存泄露。所以,千万不要创建已经存在的表。免得内存泄露。
除了这个内存泄露,没有其它异常出现。没有其它内存泄露,也没有指针越界访问,也没有指针非法使用。即使采用事务过程,也没有任何异常出现。
添加了我修改的那些代码,然后开启加密功能,结果跟前面一致,除了可能的40KB内存泄露外,没有其它异常,即使使用事务也没有异常。
V. 关键点说明
1、 为了销毁我们数据块,MY_DESTROY_KEY 能不能更早地执行,我还没有深入分析。但是可以肯定的是不能更晚销毁,否则BTree 已被sqlite删除,我们再访问就指针非法访问,程序崩溃。
2、 Sqlite 对于加解密过程的处理有点奇怪。加解密函数它都传入数据块,但是解密时,一定要将解密结果直接写入它提供的数据块空间;加密时则相反,必须自己申请空间,将它提供的原始数据 memcpy 到自己空间,然后加密自己的空间数据,最后将自己空间指针返回给它。即,加密过程不能修改任何原始数据。——当然,这个过程我已经封装到代码里,以后在 Encrypt.h 里封装的函数和密钥块不需要处理这个过程,一律直接将加解密数据写回到 data 数据块空间就行了。
3、 加解密时 nMode 参数为什么只有那几个取值,为什么不会有其它值?看 sqlite3 代码的确不会有其他值出现,连5都没有。不知道sqlite3为什么这么设计。总之,我认为 nMode <=5 是解密,>5 是加密。这个做法在 3.6.12 版本没有问题。
4、 根据网上代码,nMode == 7 时是操作事务文件。如果在事务执行过程中进行 rekey,这时候要用老密码加密事务文件内容。为什么要用老密码?而且还是加密不是解密?不清楚。但是我测试的确用老密码加密是对的。即,nMode == 7 时要用老密码加密数据。
5、 如果在事务开启过程中 rekey,会导致事务提交。即,一旦rekey,那么已开启的事务就被自动提交,不能回滚了。所以,如果要修改数据库密码,最好不要在事务开启时进行。否则先前开启的事务将失去回滚的机会。
(END)