SQLCipher是一个利用256bit AES加密SQLite文件的开源库。
SQLCipher使用内部的SQLite Codec API来插入回调函数到Pager管理中,在写入数据库页到磁盘或者从磁盘读取中读取数据库页的过程中加密。SQLCipher加密有两个特点。
PRAGMA kdf_iter
更改。主数据库文件内的所有数据都会被加密,日志文件中的数据也会被加密。如果您禁用了数据库临时存储(配置时--enable-tempstore=yes
,构建时定义了SQLITE_TEMP_STORE=2
),以下几点需要考虑:
SQLCipher是SQLite的扩展,但是由于多种原因它不能做成SQLite的插件。SQLCipher修改了SQLite,独立于SQLite的源码,单独维护。在SQLite的特定版本后SQLCipher基线。SQLCipher对SQLite的核心代码尽可能的少做改动,避免SQLite合入侵入式修改时产生问题。
SQLCipher之所以这样做,基于以下几点原因:
SQLITE_HAS_CODEC
,标准的SQLite没有这个。SQLITE_HAS_CODEC
,SQLite也不会将codec作为插件。尽管SQLite提供了一个加载扩展的函数,它也不能扩展其他系统的内部函数(它主要是用来允许自定义用户函数的)。PRAGMA cipher_*
)。利用SQLite提供的加载扩展函数也做不到这一点。SQLCipher是基于SQLite的,大多数API和SQLite3的C/C++接口相同。然而,SQLCipher也以PRAGMA、SQL语句、C函数的方式增加了一些安全相关的扩展。
创建一个新的加密数据库的过程叫做"keying"。在第一次操作数据库时才生成密钥(懒汉式)。这意味着在第一次操作数据库之前需要设置好密码或者密钥。一旦操作了数据库(例如SELECT, CREATE TABLE, UPDATE),需要读取或者写入页时,密钥应该已经生成完毕,随时可用。
示例一:密码加密
key可以是密码,然后利用PBKDF2密钥生成算法将密码转换成密钥。
sqlite> PRAGMA key = 'passphrase';
示例二:密钥加密(没有密钥加密的过程)
可以使用一个blob作为原始密钥。这种方法要求应用提供一个包含64字符的16进制序列,该序列会被转化成32B(256bit)的密钥。
sqlite> PRAGMA key = "x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'";
示例三:带盐值的密钥加密(没有密钥加密的过程)
可以使用一个blob作为原始密钥,同时可以自己指定盐值。通常盐值由SQLCipher自动生成,存储在数据库文件的前16字节中。这种方式要求应用提供一个包含96字符的16进制序列。前64个字符(32B)作为原始的密钥,剩下的32字符(16B)作为盐值。
PRAGMA key = "x'98483C6EB40B6C31A448C22A66DED3B5E5E8D5119CAC8327B655C8B5C483648101010101010101010101010101010101'";
测试key
打开一个已经存在的数据库时,如果key不对的话,PRAGMA key
语句不会立即报错。想要测试数据库是否能用这个key打开,可以做一些操作做实验。
最简单的方式就是查询sqlite_master表,这个操作会查询数据库的第一页数据并且会解析schema。
sqlite> PRAGMA key = 'passphrase';
sqlite> SELECT count(*) FROM sqlite_master; -- if this throws an error, the key was incorrect. If it succeeds and returns a numeric value, the key is correct;
用C代码实现
sqlite3_key(database, "test123", 7);
if (sqlite3_exec(database, "SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK) {
// key is correct.
} else {
// key is incorrect
}
要点
PRAGMA key
应该是数据库的第一个操作。可以通过编码的方式设置密钥或者密码。实际上PRAGMA key
正是通过调用sqlite3_key()
实现的。sqlite3_key_v2()
和sqlite3_key()
的区别在于,前者可以指定数据库名字,后者指定主数据库。
int sqlite3_key(
sqlite3 *db, /* Database to be keyed */
const void *pKey, int nKey /* The key, and the length of the key in bytes */
);
int sqlite3_key_v2(
sqlite3 *db, /* Database to be keyed */
const char *zDbName, /* Name of the database */
const void *pKey, int nKey /* The key */
);
想要更改一个加密数据库的密码或者密钥,需要先用旧的key解密。只要数据库可写可读,PRAGMA rekey
可以以新的方式加密数据库。
示例
sqlite> PRAGMA key = 'old passphrase';
sqlite> PRAGMA rekey = 'new passphrase';
要点
PRAGMA rekey
一定要在PRAGMA key
之后使用,数据库可读时就可调用。PRAGMA rekey
不能用来加密非加密数据库。它只能用于更改密码/密钥。使用sqlcipher_export()
来加密明文数据库。可以通过编码的方式重新设置密钥或者密码。实际上PRAGMA rekey
正是通过调用sqlite3_rekey()
实现的。sqlite3_rekey_v2()
和sqlite3_rekey()
的区别在于,前者可以指定数据库名字,后者指定主数据库。
int sqlite3_rekey(
sqlite3 *db, /* Database to be rekeyed */
const void *pKey, int nKey /* The new key, and the length of the key in bytes */
);
int sqlite3_rekey_v2(
sqlite3 *db, /* Database to be rekeyed */
const char *zDbName, /* Name of the database */
const void *pKey, int nKey /* The new key */
);
可以设置不加密数据库文件头的块的大小,以便对数据库做一些校验。这个大小一定要大于0,一定是加密的数据块的倍数,并且小于第一个数据库页的可用空间。可以这样设置:
PRAGMA cipher_plaintext_header_size = 32;
这个功能主要用于iOS将要把WAL模式数据库文件存入shared container场景。这种场景下iOS需要判断数据库文件是否是WAL模式,如果是,iOS会给应用分配特殊权限,允许应用在后台时对数据库持有文件锁。如果数据库文件被整体加密,iOS也就无法判断文件类型,iOS在后台应用持有文件锁的时候会杀掉这个进程。
为了这个场景,iOS开发者应该告知SQLCipher保留一部分数据库文件头不加密。推荐的偏移量是32B——这不算太大,schema和数据等敏感信息不会暴露;也不算太小,足够iOS读取文件头的段信息,"SQLite Format 3\0"和数据库标识日志模式的读写版本号(0-19B),这足够iOS辨别文件,可以保留后台应用不被杀死。
注意:SQLCipher通常在数据库文件头的前16个字节保留数据库盐值,如果使能了这个命令,就不再有位置保留数据库盐值。因此,应用应该在外部保留、传入盐值。
PRAGMA cipher_salt
命令可以用来设置、输出盐值。例如,第一次创建数据库时可以键入该命令来打印SQLCipher自己生成的盐值,然后保留这个盐值。然后下次再打开数据库时,可以键入该命令输入盐值,16进制的16字节数据。
PRAGMA key = 'test';
PRAGMA cipher_plaintext_header_size = 32;
PRAGMA cipher_salt = "x'01010101010101010101010101010101'";
盐值的另一个应用就是和密钥一起加密使用。应用可以提供96字符、16进制的BLOB,前64字符用作密钥,后32字符用作盐值。
PRAGMA key = "x'98483C6EB40B6C31A448C22A66DED3B5E5E8D5119CAC8327B655C8B5C483648101010101010101010101010101010101'";
注意,PRAGMA cipher_plaintext_header_size
一旦使用,每一次打开加密数据库都需要重新执行该命令。同样,每一次也都需要提供盐值。
取出、设置数据库盐值。32字符的16进制数字,会被转化成16B数据。
PRAGMA cipher_salt;
PRAGMA cipher_salt = "x'01010101010101010101010101010101'";
注意:正常场景下,不需要使用该命令,SQLCipher自己生成数据库盐值并把它存入数据库文件的前16字节中。这个命令一定是跟PRAGMA cipher_plaintext_header_size
一起用的。
SQLCipher使用PBKDF2加强密钥,抵御暴力匹配、字典攻击。默认256000次迭代(512000次SHA512操作)。该命令可以重设置迭代数。
sqlite> PRAGMA key = 'blue valentines';
sqlite> PRAGMA kdf_iter = '10000';
要点
PRAGMA key
命令之后、在其他操作之前。输出、设置KDF算法。默认是PBKDF2_HMAC_SHA512,也支持PBKDF2HMAC_SHA256、PBKDF2_HMAC_SHA1 。
PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA256;
SQLCipher2.0增加了校验每页HMAC的特性。默认情况下,SQLCipher2.0的数据库检查HMAC,这一特性导致SQLCipher2.0无法打开SQLCipher1.1.X的数据库。因此,为了保证SQLCipher1.1.X的数据库能相后兼容,可以输入该命令禁用HMAC检查。
示例
sqlite> PRAGMA key = 'blue valentines';
sqlite> PRAGMA cipher_use_hmac = OFF;
要点
PRAGMA key
命令之后、在其他操作之前。为了性能考虑可以禁用内存擦写功能。默认开启。
PRAGMA cipher_memory_security = OFF;
可以设置外部熵值到crypto provider,需要是一个16进制序列的blob。
PRAGMA cipher_add_random = "x'deadbaad'";
SQLCipher不同版本有不同的默认设置,因此现存的数据库经常需要从老版本升级到新版本。该命令就是为了方便升级。
> ./sqlcipher 2xdatabase.db
> PRAGMA key = 'YourKeyGoesHere';
> PRAGMA cipher_migrate;
如果该命令执行成功,会返回0,升级后的数据仍是打开状态,文件名不变。
该命令能让SQLCipher从1.x、2.x或者3.x直接升级到第四代SQLCipher。如果数据库使用的不是默认设置,需要使用sqlcipher_export
手动升级。
这一命令代价昂贵,因为它试着使用每一个版本默认设置打开数据库,才能最终决定合适的设置。因此应用不该在每一次打开数据库时都执行该命令。推荐应用采用如下步骤打开、升级数据库:
PRAGMA key
,然后尝试一次简单的查询。PRAGMA cipher_migrate
,尝试升级数据库。这一流程在数据库已经是版本最新时可以保证性能最好。如果key不正确,性能会有略微下降,因为尝试升级时可能多次派生key。但是这一流程在大多数场景下表现都不错。
如果经常发生key不对的情况,那么第2步的性能就太差了。应用可以通过选项或者其他方式来跟踪SQLCipher版本。
注意:当打开数据库连接运行PRAGMA cipher_migrate
时,应该置位SQLITE_OPEN_CREATE
标志位,因为升级过程会attach新的数据库。
在ATTACH语句可以加上特殊的key参数,这样可以使用指定的key来附加数据库。这在数据库间复制、转移数据时非常有用。
示例一:附加加密数据库到明文数据库
$ ./sqlcipher plaintext.db
sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'testkey';
示例二:使用16进制key,附加加密数据库
$ ./sqlcipher plaintext.db
sqlite> ATTACH DATABASE 'test2.db' AS db2 KEY "x'10483C6EB40B6C31A448C22A66DED3B5E5E8D5119CAC8327B655C8B5C4836481'";
示例三:附加明文数据库到加密数据库
$ ./sqlcipher encrypted.db
sqlite> ATTACH DATABASE 'plaintext.db' AS plaintext KEY ''; -- empty key will disable encryption
要点:
sqlcipher_export
可以很方便的从主数据库中复制数据到附加数据库中,包括schema、触发器、虚表和数据表。它主要的功能是可以方便的从加密数据库转移到非加密数据库,或者从非加密数据库转移到加密数据库,或者从两个有着不同配置的加密数据库间转移数据。更多的信息可以看how to encrypt a plaintext database using SQLCipher。
使用sqlcipher_export
非常简单,只需执行一个SELECT语句,传入想导出到的目标数据库的名字就可以。
额外有一个可选参数,可以设置导出的源数据库。如果不使用该参数,默认源数据库是主数据库。该参数也可以用来复制附加数据库到主数据库。
示例一:加密明文数据库
$ ./sqlcipher plaintext.db
sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'testkey';
sqlite> SELECT sqlcipher_export('encrypted');
sqlite> DETACH DATABASE encrypted;
示例二:导出加密数据库到明文数据库
$ ./sqlcipher encrypted.db
sqlite> PRAGMA key = 'testkey';
sqlite> ATTACH DATABASE 'plaintext.db' AS plaintext KEY ''; -- empty key will disable encryption
sqlite> SELECT sqlcipher_export('plaintext');
sqlite> DETACH DATABASE plaintext;
示例三:从3.X转换到4.X
$ ./sqlcipher 1.1.x.db
sqlite> PRAGMA key = 'testkey';
sqlite> PRAGMA cipher_page_size = 1024;
sqlite> PRAGMA kdf_iter = 64000;
sqlite> PRAGMA cipher_hmac_algorithm = HMAC_SHA1;
sqlite> PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;
sqlite> ATTACH DATABASE 'sqlcipher-4.db' AS sqlcipher4 KEY 'testkey';
sqlite> SELECT sqlcipher_export('sqlcipher4');
sqlite> DETACH DATABASE sqlcipher4;
示例四:更改加密设置
$ ./sqlcipher encrypted.db
sqlite> PRAGMA key = 'testkey';
sqlite> ATTACH DATABASE 'newdb.db' AS newdb KEY 'newkey';
sqlite> PRAGMA newdb.cipher_page_size = 4096;
sqlite> PRAGMA newdb.cipher = 'aes-256-cfb';
sqlite> PRAGMA newdb.kdf_iter = 10000;
sqlite> SELECT sqlcipher_export('newdb');
sqlite> DETACH DATABASE newdb;
示例五:复制附加的明文数据库到新的空加密数据库
$ ./sqlcipher encrypted.db
sqlite> PRAGMA key = 'testkey';
sqlite> ATTACH DATABASE 'plain.db' AS plain KEY '';
sqlite> SELECT sqlcipher_export('main', 'plain');
sqlite> DETACH DATABASE plain;
要点:
sqlcipher_export
命令不会修改目标数据库的user_version,不限制用户自己操作。调用该命令,参数可以设置为1、2、3、4,这样强制SQLCipher使用目标主版本号对应的默认设置。比如下面的命令会让SQLCipher把当前的数据库当成SQLCipher3.X数据库来处理。
PRAGMA cipher_compatibility = 3;
SQLCipher2数据库增加该特性,可以更改数据库的页大小。默认的页大小是4KB,可以更改使用更大的页大小来提升数据库性能。最近的测试数据表明,在数据库页调整变大后,在操作大量的数据库页面时,数据库性能可以明显提升(5-30%)(例如:不带index的查询、一个事务内大量插入、大量的删除)。
想要调整页大小,需要在每次设置key后立刻执行该命令。页大小必须是512和65535间的2的指数。
示例
sqlite> PRAGMA KEY = 'testkey';
sqlite> PRAGMA cipher_page_size = 8192;
要点
PRAGMA key
命令之后、在其他操作之前。这一命令可以分析多个查询和它们各自的执行时间,毫秒级。可以有四个参数:stdout、stderr、要记录到的文件名、off。示例:
$ ./sqlcipher example.db
sqlite> PRAGMA cipher_profile='sqlcipher.log';
sqlite> CREATE TABLE t1(a,b);
sqlite> INSERT INTO t1(a,b) VALUES('one for the money', 'two for the show');
sqlite> PRAGMA cipher_profile=off;
PRAGMA cipher_settings;
对每页做HMAC校验,输出非法页、错误的报告。如果某页被标记为非法,那么大概率在SQLCipher将其写入到磁盘后该页被篡改过。该命令想要生效,需要提前使能过HMAC,而且输入的密码或者密钥必须正确。每一个错误输出一行,如果没有返回任何信息,可以认为数据库是完整的、一致的。
sqlite> pragma key = 'testkey';
sqlite> pragma cipher_integrity_check;
HMAC verification failed for page 4
HMAC verification failed for page 9
page 240 has an invalid size of 1 bytes
默认值是HMAC_SHA512,还可以设置成HMAC_SHA256、HMAC_SHA1 。
PRAGMA cipher_hmac_algorithm = HMAC_SHA256;
数据库一定是加密的,才能有输出信息。
数据库一定是加密的,输出信息才有意义。
$ ./sqlcipher example.db
sqlite> PRAGMA cipher_version;
3.3.1
如果想更改密钥派生的默认迭代次数,又不想每次打开数据库都执行PRAGMA cipher_kdf_iter
,可以执行该命令,该命令会针对所有的数据库生效。
示例
./sqlcipher sqlcipher2.0.db
sqlite> PRAGMA cipher_default_kdf_iter = 4000;
sqlite> PRAGMA key = 's3cr37';
要点
PRAGMA cipher_default_settings;
调用该命令,参数可以设置为1、2、3、4,这样对当前进程强制SQLCipher使用目标主版本号对应的默认设置(对该命令执行后才打开的数据库有效)。比如下面的命令会让SQLCipher之后打开的数据库当成SQLCipher2.X数据库来处理。
PRAGMA cipher_default_compatibility = 2;
输出、设置下次ATTACH数据库时使用的KDF算法。默认是PBKDF2_HMAC_SHA512,也支持PBKDF2HMAC_SHA256、PBKDF2_HMAC_SHA1 。
PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA256;
输出、设置下次ATTACH数据库时使用的HMAC算法。默认值是HMAC_SHA512,还可以设置成HMAC_SHA256、HMAC_SHA1 。
PRAGMA cipher_default_hmac_algorithm = HMAC_SHA256;
可以设置不加密数据库文件头的块的大小,以便对数据库做一些校验。这个大小一定要大于0,一定是加密的数据块的倍数,并且小于第一个数据库页的可用空间。和PRAGMA cipher_plaintext_header_size
功能相同,作用域是之后的所有SQLCipher操作。
PRAGMA cipher_default_plaintext_header_size = 32;
想要ATTACH一个已经更改了页大小的数据库,一定要先执行该命令。该值是512和65536间的2的指数。
示例
$ ./sqlcipher foo.db
sqlite> PRAGMA key = 'foo';
sqlite> PRAGMA cipher_page_size = 8192;
sqlite> CREATE TABLE t1(a,b);
sqlite> INSERT INTO t1(a,b) values('one for the money', 'two for the show');
sqlite> .q
$ ./sqlcipher bar.db
sqlite> PRAGMA cipher_default_page_size = 8192;
sqlite> PRAGMA key = 'bar';
sqlite> ATTACH DATABASE 'foo.db' as foo KEY 'foo';
sqlite> SELECT count(*) FROM foo.t1;
有时不可能每次都在打开数据库时执行PRAGMA cipher_use_hmac
。例如,想要ATTACH一个1.1.x的数据库到主数据库。这种情况下可以执行该命令更改全局配置。
示例
./sqlcipher sqlcipher2.0.db
sqlite> PRAGMA key = 's3cr37'; -- opens using default setting, with HMAC on
sqlite> PRAGMA cipher_default_use_hmac = OFF;
sqlite> ATTACH DATABASE '1.1.x.db' AS remote key 's3cr37'; -- next open operation, the default for HMAC is off, and this database will be keyed with password 's3cr37'
要点