+--------------------------------------+----------------+--------------------+
| page data | iv | hmac |
+--------------------------------------+----------------+--------------------+
iv是一段随机数,可以保证每一页的iv值都不一样。和page data一起作用,用于生成hmac值。
sizeof(page data) = page sz - reserved sz
hmac是一段校验和,可以用来检查page data是否损坏。
sizeof(iv + hmac) = reserved sz
加密实现是通过派生后的key+盐值,对原始page加密,生成加密数据。然后通过iv,对加密数据生成校验和hmac值。
SQLCipher的实现方式是在读、写page的时候实现的加、解密。通过pragma key和attach流程中可以分别设置key,在这一过程中只是注册了加解密的实现算法、保存了key。在后面的第一次操作中才开始派生key。
pragma key的入口函数是sqlite3Pragma->sqlite3_key_v2,attach的入口函数是attachFunc。
在这两个函数内,实际上都是执行sqlite3CodecAttach操作
int sqlite3CodecAttach(
sqlite3* db,
int nDb, // 数据库数组db->aDb的索引,前两个分别是main和temp,后面的是attach的
const void *zKey, // 密文或者密钥的地址
int nKey // 密钥的长度,如果是密文,这里是-1
);
在这个函数里。主要执行了以下动作:
sqlcipher_activate
注册了全局的sqlcipher_provider *default_provider。这个里面主要存了一些加解密算法的实现函数的指针。可以通过宏选择加解密算法,这些算法都是利用第三方的开源加密库。比如SQLCIPHER_CRYPTO_CC,可以选择common crypto算法,实现放在crypto_cc.c里。
sqlcipher_codec_ctx_init
这个函数主要填充了codec_ctx对象,这个对象对应于一个数据库。这里面的一些关于加密的参数都先设置成了默认值,密文(或密钥)填充进两个cipher_ctx——read_ctx和write_ctx里。
typedef struct {
int derive_key; // 是否需要派生key。设置原始key后置位,派生key后取消置位
int pass_sz; // 原始key的size。因为有可能是blob,所以需要size
unsigned char *key; // 派生后的key。可能是用户设置的HEX key转换成二进制得到,或者由用户设置的密文和盐值经过kdf_iter次派生得到
unsigned char *hmac_key; // 由key和hmac_kdf_salt经过fast_kdf_iter次派生得到
unsigned char *pass; // 配置的原始key,可能是密文、x'HEX key'、x'HEX key + HEX kdf_salt'
char *keyspec; // x'HEX key + HEX kdf_salt'
} cipher_ctx;
typedef struct {
int store_pass; // 是否要保存原始key。如果设成false,应该在派生key后清空原始key
int kdf_iter; // key的派生次数
int fast_kdf_iter; // hmac_key的派生次数
int kdf_salt_sz;
int key_sz;
int iv_sz;
int block_sz;
int page_sz;
int keyspec_sz;
int reserve_sz;
int hmac_sz;
int plaintext_header_sz;
int hmac_algorithm;
int kdf_algorithm;
unsigned int skip_read_hmac;
unsigned int need_kdf_salt; // 是否需要取kdf_salt。默认置位,取到kdf_salt后取消置位
unsigned int flags; // 默认值DEFAULT_CIPHER_FLAGS,标志是否要hmac校验、大小端
unsigned char *kdf_salt; // 生成key的盐值。优先使用用户配置的,其次从文件头取,取不到用随机数
unsigned char *hmac_kdf_salt; // kdf_salt的每字节 XOR hmac_salt_mask
unsigned char *buffer; // 预申请的一段内存,用于加、解密的缓存区,大小为page_sz
Btree *pBt;
cipher_ctx *read_ctx; // 读page用的密钥配置
cipher_ctx *write_ctx; // 写page用的密钥配置
sqlcipher_provider *provider; // 默认的provider,算法可以通过宏配置
void *provider_ctx;
} codec_ctx;
sqlite3PagerSetCodec
这个函数把加解密的函数注册进了pager里面。这些函数后面在读写page的时候会用到。
db->aDb[nDb]->pBt->pBt->pPager->xCodec; // sqlite3Codec
db->aDb[nDb]->pBt->pBt->pPager->xCodecFree; // sqlite3FreeCodecArg
db->aDb[nDb]->pBt->pBt->pPager->pCodec; // codec_ctx
最重要的是sqlite3Codec这个函数,这个函数实现了key的派生,对页面的加密、解密。代码中没有直接调用过这个函数,而是把这个函数注册进pager,又把pager->xCodec封装成两个宏。CODEC1用于解密,CODEC2用于加密。
# define CODEC1(P,D,N,X,E) \
if( P->xCodec && P->xCodec(P->pCodec,D,N,X)==0 ){ E; }
# define CODEC2(P,D,N,X,E,O) \
if( P->xCodec==0 ){ O=(char*)D; }else \
if( (O=(char*)(P->xCodec(P->pCodec,D,N,X)))==0 ){ E; }
P代表pager,D代表输入数据pData(如果是解密操作,也是输出数据),N代表pageno,X代表操作码CODEC_READ_OP或CODEC_WRITE_OP或CODEC_JOURNAL_OP,E代表执行出错时的动作,O代表加密后的输出数据。
sqlite3Codec执行以下动作:
Rekey是对一个加密数据库更改密钥的操作。
在set key的流程中,有一个生成reat_ctx和write_ctx的过程sqlcipher_codec_ctx_init。这两个对象绝大多数情况都是相同的,只有在rekey过程中不同。
在rekey过程中,正是利用了这两个对象实现了更改密文的操作。可以分为三步:
这三步完成后,rekey操作成功。老的数据都已经转换成新key加密的了,再写入新数据也是用新key加密的。
Export可以实现将一个加密数据库导出到一个明文数据库、将一个明文数据库导出到一个加密数据库,将一个加密数据库导出到一个不同key的加密数据库中。export过程的内部实现方法是sqlcipher_exportFunc。该函数内实际上是利用原生的sqlite语句实现了导出。
下面的语句执行后可以得到 CREATE table/index 或者 INSERT INTO SELECT 语句,然后再执行查询出的语句即可实现复制。
-- CREATE table/index/unique index ...
SELECT sql FROM source.sqlite_schema WHERE type='table' AND name!='sqlite_sequence' AND rootpage>0;
SELECT sql FROM source.sqlite_schema WHERE sql LIKE 'CREATE INDEX %%';
SELECT sql FROM source.sqlite_schema WHERE sql LIKE 'CREATE UNIQUE INDEX %%';
-- INSERT INTO target.xxx SELECT * FROM main.xxx
SELECT 'INSERT INTO target.' || quote(name) || '
SELECT * FROM source.' || quote(name) || ';'
FROM source.sqlite_schema
WHERE type = 'table' AND name!='sqlite_sequence' AND rootpage>0;
-- INSERT INTO target.sqlite_sequence SELECT * FROM main.sqlite_sequence
SELECT 'INSERT INTO target.' || quote(name) || '
SELECT * FROM source.' || quote(name) || ';'
FROM target.sqlite_schema
WHERE name=='sqlite_sequence';
下面的语句可以直接执行。
INSERT INTO target.sqlite_schema
SELECT type, name, tbl_name, rootpage, sql
FROM source.sqlite_schema
WHERE type='view' OR type='trigger' OR (type='table' AND rootpage=0)";
这两部分sql执行完,实际上是实现了export的功能。