怎么也没想到会跟sqlite3折腾上三天之久。先是考虑node binding的sqlite3模块,很容易通过npm就能找到,很成熟的东西,编译通过,使用很happy.
接着要考虑对数据的加密,自然就找到了wxsqlite3和sqlcipher。众所周知,sqlite3是开源的,而且官方模块其实是有加密功能的,甚至连加密接口sqlite3_rekey()和数据库使用时解密sqlite3_key()接口都是有的,只不过是收费的,据说官方加密功能售价为2000美元。这应该就是所谓的price gouging,搞不起,搞不起。
先说说wxsqlite3,看文档功能挺强大的,而且据说QT里的sqlite3加密用的是它。它的源码里有个sqlite3secure子模块是用来实现加密功能的,它实现了四种便捷的加密级别:
可以通过函数来指定哪种加密方式:
wxsqlite3_config(db, "default:cipher", CODEC_TYPE_SQLCIPHER);
不指定的话,默认是第三种,即:ChaCha20 - Poly1305 HMAC。据说这种是新的标准,相比于AES,ChaCha20的优势在于大量基于XOR操作,就算是普通CPU也会执行得很快,而AES依赖于特定CPU指令,否则性能上会有问题。
我的理解是,第四种,即:AES 256 Bit CBC - SHA1/SHA256/SHA512 HMAC,就是sqlcipher一样的功能,也是sqlcipher采用一样的算法。
考虑到所使用的DB Browser for SQLite工具是基于SQLCipher Version 4.1.0 community (based on SQLite 3.27.2),所以就不用默认选择,而选择CODEC_TYPE_SQLCIPHER了。
添加加密模块是需要基于sqlite3源码去做修改的,sqlite3_rekey()过程其实是一个把所有数据导出再导入,每个page都单独加密的过程,加密之后文件自然会比原数据文件大那么一点点,实测我110MB的数据,加密之后也就变成了114MB左右而已。wxsqlite3提供了一个rekeyvacuum.sh脚本用来处理sqlite3.c源码,看起来一切都很顺利,wxsqlite3这货内置各种算法的源码,所以编译也没有外部依赖,感觉挺爽。
接着就各种痛苦出现了,首先是居然无法对我的数据文件进行sqlite3_rekey()操作,报错说什么『The database disk image is malformed 』,查了半天,好像也有人遇到,说是文件很大造成的,但我的数据文件只有100多M,而且DB Browser可以正常打开的。但如果通过编译了wxsqlite3功能的sqlite3,重新进行新数据库的创建和插入数据,又是可以正常加密和解密了,这就诡异了。同时又出现了另一个问题,CODEC_TYPE_SQLCIPHER方式加密的数据,无法在DB Browser里解密出来,反之也不行,可见它至少并不兼容sqlcipher的算法。
查了好久,实在是无解,又开始转向sqlcipher,但过程也是相当的折腾。sqlcipher使用上会有些优势,不需要进行加密和解密接口的node封装,可以直接使用PRAGMA命令:
PRAGMA key = 'passphrase'; -- start with the existing database passphrase
PRAGMA rekey = 'new-passphrase'; -- rekey will reencrypt with the new passphrase
sqlcipher是在configure时,帮你生成好修改之后的sqlite3.c文件:
./configure --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC"
但这货是基于tclsh的脚本,于是还得安装个tclsh。windows上安装麻烦,又折腾到到Linux上安装,configure完之后,再把sqlite3.c拷贝到windows里进行vs2015工程的编译。
接着是,这货完全依赖于外部的各种摘要算法,可以定义SQLCIPHER_CRYPTO_CC,或者是SQLCIPHER_CRYPTO_LIBTOMCRYPT,或者是SQLCIPHER_CRYPTO_OPENSSL. 我自然选择openssl,这时就麻烦了,因为是针对于windows的vs2015平台,所以我还得找个vs2015对应的openssl工程编译出来链接库。好不容易编译通过了,又发现动态链接库没有,哪个链接库没有,通过下面这个命令可以在windows上查看到:
dumpbin /dependents C:\xxx\sqlite3.node
显示动态链接了libcryptoMD.dll,但我希望是静态链接,于是又查找怎么在编译时指定为静态链接编译,原来是要把/MD改成/MT。又接着各种函数找不到,加上Ws2_32.lib和crypt32.lib之后,终于消停了。
接着麻烦又来了,号称是所谓的5-15% overhead,但实际测试发现,随便一个查询,至少比原来要慢三倍。一个频繁查询在sqlite3里耗时50ms,现在变成了160ms+。网上很多人也遇到同样问题,说是可以通过新建索引来减少对磁盘数据的读取,或者通过调大page的size,page的 size默认为4096,每个page为块单独加密和解密,如果读取磁盘越少,意味着需要解密次数越少,执行速度也就越快。再就是所谓的关闭一些安全检查可以提高速度,如:
PRAGMA cipher_memory_security = OFF
然而实验证明并无卵用,但无论如何,也无法接受成倍的性能消耗,于是继续折腾。还好我的需求是数据只读,不需要写入,于是可以通过在软件加载时把所有数据解密后拷贝一份到内存里,这样就能保证安全的同时不损失速度,只是启动会慢一点点,而且内存消耗会多个上百MB,正好发现SQLCipher提供了类似操作,即sqlcipher_export。不过,由于不是所有数据都对耗时敏感,所以我可以做得更好,只将数据库中使用频繁的表拷贝到内存中的临时库里,不频繁使用的就不拷贝,这样启动也不慢了,内存消耗也不大了,堪称完美。于是自己对源码进行了改写,使用起来就是:
await exec("ATTACH DATABASE ':memory:' AS tmp KEY ''")
await exec("SELECT sqlcipher_export('tmp', 'main', 'table_xxx,table_yyy')")
再接着又一问题,发现打包之后,软件比原来大了近一百M啊,原来加密之后的数据,基本成了一堆随机的二进制了,压缩算法基本就丧失了意义。这个看来无解,只能忍了。