SQLCipher核心思想

加密原理

+--------------------------------------+----------------+--------------------+
|            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值。

Set key / Attach

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执行以下动作:

    • sqlcipher_codec_key_derive
      派生key。检查derive_key标志位,如果置位,执行sqlcipher_cipher_ctx_key_derive,派生key,然后清除derive_key标志位。先read_ctx后write_ctx,如果两个ctx里的原始key一样,派生一个就行,另一个copy过去。然后检查store_pass标志位,如果未置位,需要清空两个原始key。
    • 根据不同的操作码分别做处理。如果是CODEC_READ_OP,利用read_ctx执行解密操作,page数据pData解密后,缓存到buffer里,然后再copy buffer数据到pData中,返回pData;如果是CODEC_WRITE_OP,利用write_ctx执行加密操作,pData数据加密后,缓存到buffer里,返回buffer;如果是CODEC_JOURNAL_OP,也是执行加密操作,不过是利用read_ctx使用原始key加密日志数据,这是为了防止rekey事务失败,需要rollback的场景,此时需要从journal文件中读取数据。具体加解密、hmac校验的动作在sqlcipher_page_cipher里实现。

Rekey

Rekey是对一个加密数据库更改密钥的操作。

在set key的流程中,有一个生成reat_ctx和write_ctx的过程sqlcipher_codec_ctx_init。这两个对象绝大多数情况都是相同的,只有在rekey过程中不同。

在rekey过程中,正是利用了这两个对象实现了更改密文的操作。可以分为三步:

  1. 设置新key到write_ctx;
  2. 逐页读出后重新写入。读的过程中使用read_ctx解密,写的过程中使用write_ctx加密;
  3. 复制write_ctx到read_ctx中。

这三步完成后,rekey操作成功。老的数据都已经转换成新key加密的了,再写入新数据也是用新key加密的。

Export

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的功能。

你可能感兴趣的:(数据库)