原文链接:
http://lancelot.blog.51cto.com/393579/940805
http://lancelot.blog.51cto.com/393579/940808
http://lancelot.blog.51cto.com/393579/940812
http://lancelot.blog.51cto.com/393579/940813
http://lancelot.blog.51cto.com/393579/940814
http://lancelot.blog.51cto.com/393579/940816
http://lancelot.blog.51cto.com/393579/940818
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 |
首先要在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' D:\Research\MySQLite\Debug/../src/sqlite3.c:80968: undefined reference to `sqlite3CodecGetKey' D:\Research\MySQLite\Debug/../src/sqlite3.c:80970: undefined reference to `sqlite3CodecAttach' src\sqlite3.o: In function `sqlite3Pragma': D:\Research\MySQLite\Debug/../src/sqlite3.c:94023: undefined reference to `sqlite3_key' D:\Research\MySQLite\Debug/../src/sqlite3.c:94026: undefined reference to `sqlite3_rekey' D:\Research\MySQLite\Debug/../src/sqlite3.c:94038: undefined reference to `sqlite3_key' D:\Research\MySQLite\Debug/../src/sqlite3.c:94040: undefined reference to `sqlite3_rekey' D:\Research\MySQLite\Debug/../src/sqlite3.c:94048: undefined reference to `sqlite3_activate_see' src\sqlite3.o: In function `sqlite3RunVacuum': D:\Research\MySQLite\Debug/../src/sqlite3.c:101744: undefined reference to `sqlite3CodecGetKey' |
先不用管上面的编译错误,创建crypt.c和crypt.h,用来实现加密解密函数和相应接口的定义。
crypt.c里实现了加密解密函数,代码如下:
#include "crypt.h" #include "memory.h"
/*********** 关键加密函数 ***********/ int My_Encrypt_Func(unsigned char * pData, unsigned int data_len, unsigned char * key, unsigned int len_of_key)
{ unsigned int i; unsigned char bit, val;
for (i = 0; i < data_len; i++) { val = ~(*pData); *pData = val; pData++; } return 0; }
/*********** 关键解密函数 ***********/ int My_DeEncrypt_Func(unsigned char * pData, unsigned int data_len, unsigned char * key, unsigned int len_of_key)
{ unsigned int i; unsigned char bit, val;
for (i = 0; i < data_len; i++) { val = ~(*pData); *pData = val; pData++; } return 0; } |
这里加密解密函数就是简单的采用了求反的操作,目的是用来演示加密和解密。以后实际运用中把这两个函数内的算法修改为自己的加密解密算法即可。
注意:这里的加密解密函数实际上没有用到Key值,因此加密后使用任意Key值均可以解开数据库,但是加密后,用第三方工具是不能直接打开的。
crypt.h用来声明加密解密函数的定义,以便sqlite3.c包含加密解密接口,代码如下:
/** * 加密函数 */ int My_Encrypt_Func(unsigned char * pData, unsigned int data_len, unsigned char * key, unsigned int len_of_key);
/** * 解密函数 */ int My_DeEncrypt_Func(unsigned char * pData, unsigned int data_len, unsigned char * key, unsigned int len_of_key); |
注意:网上的代码中,参数key定义为“const char *”类型,与sqlite3.c代码一起编译时会有错误,这里按照sqlite3.c中的类型修改为“unsigned char *”类型。
#ifdef
SQLITE_HAS_CODEC
#include "crypt.h"
/***
加密结构
***/
#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,
int nDB,
void** Key,
int* nKey) {
return;
}
/*被
sqlite 和 sqlite3_key_interop 调用, 附加密钥到数据库.*/
int sqlite3CodecAttach(sqlite3 *db,
int nDb,
const void *pKey,
int nKeyLen);
/**
这个函数好像是
sqlite 3.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 LPCryptBlock CreateCryptBlock(unsigned char* hKey, Pager *pager,
LPCryptBlock pExisting);
//加密/解密函数, 被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,
void *(*xCodec)(
void*,
void*, Pgno,
int),
void *pCodecArg);
//加密/解密函数, 被pager调用
void * sqlite3Codec(void *pArg, unsigned char *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 LPCryptBlock CreateCryptBlock(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)
{
return sqlite3_key_interop(db, pKey, nKey);
}
int sqlite3_rekey(sqlite3 *db,
const void *pKey,
int nKey)
{
return sqlite3_rekey_interop(db, pKey, nKey);
}
/*被
sqlite 和 sqlite3_key_interop 调用, 附加密钥到数据库.*/
int sqlite3CodecAttach(sqlite3 *db,
int nDb,
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 an existing 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)
return
SQLITE_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
|
DeriveKey 函数就是做这个扩展的。有人把接收到的密钥求md5,这也是一个办法,因为md5运算结果固定16字节,不论你有多少字符,最后就是16字节。这是md5算法的特点。但是我不想用md5,因为还得为它添加包含一些 md5 的.c或.cpp文件。我不想这么做。我自己写了一个算法来扩展密钥,很简单的算法。当然,你也可以使用你的扩展方法,也而可以使用 md5 算法。只要修改 DeriveKey 函数就可以了。
在 DeriveKey 函数里,只管申请空间构造所需要的密钥,不需要释放,因为在另一个函数里有释放过程,而那个函数会在数据库关闭时被调用。参考上面 DeriveKey 函数来申请内存。
上面的代码是从网上下载下来的,它使用的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添加了加密解密功能后,使用方法如下:
1、 在调用sqlite3_open()函数打开数据库后,要调用sqlite3_key()函数为数据库设置密码;
2、 如果数据库之前有密码,则调用sqlite3_key()函数设置正确密码才能正常工作;
3、 如果一个数据库之前没有密码,且已经有数据,则不能再为其设置密码;
4、 如果要修改密码,则需要在第一步操作后,调用sqlite3_rekey()函数设置新的密码;
5、 设置了密码的SQLite数据库,无法使用第三方工具打开;
具体使用的示例代码如下:
#include <stdio.h> #include <stdlib.h> #include "sqlite3.h" #define SQLITE3_STATIC
extern int sqlite3_key(sqlite3 *db, const void *pKey, int nKey);
static int _callback_exec(void * notused,int argc, char ** argv, char ** aszColName) { int i; for ( i=0; i<argc; i++ ) { printf( "%s = %s\r\n", aszColName[i], argv[i] == 0 ? "NUL" : argv[i] ); }
return 0; }
int main(int argc, char * argv[]) { const char * sSQL; char * pErrMsg = 0; int ret = 0; sqlite3 * db = 0;
//创建数据库 ret = sqlite3_open("d:\\encrypt.db", &db);
//添加密码 ret = sqlite3_key( db, "dcg", 3 );
//在内存数据库中创建表 sSQL = "create table class(name varchar(20), student);"; sqlite3_exec( db, sSQL, _callback_exec, 0, &pErrMsg );
//插入数据 sSQL = "insert into class values('mem_52911', 'zhaoyun');"; sqlite3_exec( db, sSQL, _callback_exec, 0, &pErrMsg );
//取得数据并显示 sSQL = "select * from class;"; sqlite3_exec( db, sSQL, _callback_exec, 0, &pErrMsg );
//关闭数据库 sqlite3_close(db); db = 0;
return 0; } |
现象与原因
采用上面的方法对数据库进行加密,存在页面尺寸错乱的问题。在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设置回去;