Linux塊設備加密之dm-crypt分析
Linux塊設備加密之dm-crypt分析
來自 http://blog.csdn.net/sonicling/article/details/6275898這篇文章算是《Device Mapper代碼分析》的後續篇,因為dm-crypt是基於dm框架的,因此與上一篇一樣,也以2.6.33內核代碼為基礎來講述代碼的分析過程。但是本文側重點不同在於著重分析一下三個方面:
1、Linux密碼管理
2、dm-crypt到與Linux密碼的關聯
3、dm-crypt的異步處理
一、Linux密碼管理
Linux內核中,密碼相關的頭文件在<srcdir>/include/crypto/下,實現文件在<srcdir>/crypto/下。相關的概念大致有加密、塊加密、異步塊加密、哈希、分組加密模式(ECB/CBC/CFB/OFB/CTR)等。接下來一一進行簡單分析。
1.1 加密算法
我們可以從內核代碼中挑一個簡單而普通的加密算法來研究一下,例如<srcdir>/crypto/aes_generic.c描述的AES算法。
所有加密算法都是以內核模塊方式編寫的。所有內核模塊的代碼都是先看關鍵數據結構,再看算法。aes先聲明了一個叫做crypto_alg的結構體,如下:
[cpp] view plain copy
- static struct crypto_alg aes_alg = {
- .cra_name = "aes",
- .cra_driver_name = "aes-generic",
- .cra_priority = 100,
- .cra_flags = CRYPTO_ALG_TYPE_CIPHER,
- .cra_blocksize = AES_BLOCK_SIZE,
- .cra_ctxsize = sizeof(struct crypto_aes_ctx),
- .cra_alignmask = 3,
- .cra_module = THIS_MODULE,
- .cra_list = LIST_HEAD_INIT(aes_alg.cra_list),
- .cra_u = {
- .cipher = {
- .cia_min_keysize = AES_MIN_KEY_SIZE,
- .cia_max_keysize = AES_MAX_KEY_SIZE,
- .cia_setkey = crypto_aes_set_key,
- .cia_encrypt = aes_encrypt,
- .cia_decrypt = aes_decrypt
- }
- }
- };
alg是algorithm的縮寫。所有的加密、哈希等算法註冊用數據結構都叫做xxx_alg,crypto_alg的完整定義在<srcdir>/include/linux/crypto.h中:
[cpp] view plain copy
- struct crypto_alg {
- struct list_head cra_list;
- struct list_head cra_users;
- u32 cra_flags;
- unsigned int cra_blocksize;
- unsigned int cra_ctxsize;
- unsigned int cra_alignmask;
- int cra_priority;
- atomic_t cra_refcnt;
- char cra_name[CRYPTO_MAX_ALG_NAME];
- char cra_driver_name[CRYPTO_MAX_ALG_NAME];
- const struct crypto_type *cra_type;
- union {
- struct ablkcipher_alg ablkcipher;
- struct aead_alg aead;
- struct blkcipher_alg blkcipher;
- struct cipher_alg cipher;
- struct compress_alg compress;
- struct rng_alg rng;
- } cra_u;
- int (*cra_init)(struct crypto_tfm *tfm);
- void (*cra_exit)(struct crypto_tfm *tfm);
- void (*cra_destroy)(struct crypto_alg *alg);
- struct module *cra_module;
- };
alg的關鍵成員有name(算法名)、driver_name(驅動名)、flags(算法類型、同步or異步)、blocksize(分組大小,單位:字節)、ctxsize(上下文大小/字節)、alignmask(ctx的對齊)、min/max-keysize(最小or最大密鑰長度/字節)、init/exit(tfm的初始化和銷毀)、destroy(alg的銷毀)、set_key/encrypt/decrypt(設置密鑰/加密/解密的函數)。有些算法可能還有iv_size之類的,後面再講。
這裏有個ctx(算法上下文)的概念要解釋一下。所謂上下文,就是算法執行過程中所要貫穿始終的數據結構,由每個算法自己定義。set_key/encrypt/decrypt這幾個函數都可以從參數獲得算法上下文的指針。算法上下文所占的內存空間由密碼管理器來分配,註冊alg的時候指定ctx大小和對齊即可。ctx的對齊又是什麽呢?在密碼管理器分配ctx內存的時候,需要進行內存對齊。對於一些硬件加解密或者特殊要求的算法,ctx的首地址可能需要在內存中4字節或者16字節對齊,這個cra_alignmask就是指定這個。aes使用的是3(0x11),就是將首地址低二位清零,即4字節對齊,如果要求N字節對齊(N是2的某個指數),那麽alignmask就可以指定為N-1。
那麽ctx被分配到哪裏了呢?這個跟另外一個數據結構有關,就是接下來要講的struct crypto_tfm。我們先來看crypto_alg對應的set_key、encrypt、decrypt這三個函數的函數原型(註意這裏的表述哦,是crypto_alg對應的三大函數)。
int set_key(struct crypto_tfm *tfm, const u8 *in_key, unsigned int key_len);
void encrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in);
void decrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in);
in和out參數就是加解密之前和之後的傳入傳出數據,長度就是alg中的blocksize。這三個函數都有類型為struct crypto_tfm *的參數。tfm是transform的縮寫。所有加密、哈希算法的set_key、encrypt、decrypt都帶有這個參數。crypto_tfm的定義在<srcdir>/include/linux/crypto.h中:
[cpp] view plain copy
- #define crt_ablkcipher crt_u.ablkcipher
- #define crt_aead crt_u.aead
- #define crt_blkcipher crt_u.blkcipher
- #define crt_cipher crt_u.cipher
- #define crt_hash crt_u.hash
- #define crt_compress crt_u.compress
- #define crt_rng crt_u.rng
- struct crypto_tfm {
- u32 crt_flags;
- union {
- struct ablkcipher_tfm ablkcipher;
- struct aead_tfm aead;
- struct blkcipher_tfm blkcipher;
- struct cipher_tfm cipher;
- struct hash_tfm hash;
- struct compress_tfm compress;
- struct rng_tfm rng;
- } crt_u;
- void (*exit)(struct crypto_tfm *tfm);
- struct crypto_alg *__crt_alg;
- void *__crt_ctx[] CRYPTO_MINALIGN_ATTR;
- };
從中間那個union和上面的一堆#define可以看出,從這個結構又可以分散出一組xxx_tfm。crypto_alg對應cipher_tfm。最後那個參數__crt_ctx[]就是上面說到的算法上下文。也就是說,算法上下文是跟隨tfm一起分配的。從tfm,我們就可以得到ctx。Linux提供了一個函數inline void *crypto_tfm_ctx(struct crypto_tfm *tfm);來進行轉換,該函數也在<srcdir>/include/linux/crypto.h中。
現在可以梳理一下alg、crypto_tfm、xxx_tfm、ctx的關系了。alg是註冊用的;crypto_tfm是每個算法實例對應的結構;xxx_tfm包含在crypto_tfm中,是每類算法對應的結構,ctx在crypto_tfm的最後。當你的算法拿到一個crypto_tfm指針時,可以通過__crt_alg指針訪問到alg結構、可以通過crt_u.xxx訪問到對應算法類別的xxx_tfm結構、可以通過__crt_ctx得到ctx指針。當你的算法拿到一個xxx_tfm結構體指針時,可以利用xxx_tfm內嵌在crypto_tfm中的這層關系,使用container_of操作反向獲得crypto_tfm指針,由此獲得其他的結構指針。當然,你無需直接訪問成員,而是使用<srcdir>/include/crypto/algapi.h中定義的那一堆內聯函數來實現。但是你要看懂並正確使用那些函數之前,還是先弄懂上述關系,否則就會跟我初看這堆代碼一樣——腦細胞都擰成麻花了。
現在,一個普通的分組加密算法就很好理解了。首先聲明一個crypto_alg結構註冊到密碼管理器中,當外界使用到該算法時,密碼管理器會自動創建crypto_tfm並調用那三大函數進行加解密操作,對應函數返回就表示操作已完成,屬於同步操作。接下來要講的塊加密其實也是類似的。
1.2 同步塊加密
我們挑一個功能比較全面的塊加密算法來看看,如<srcdir>/drivers/crypto/geode-aes.c。這是AMD的一個硬件加密引擎的驅動,以算法模塊方式插入到內核中,驅動硬件進行加解密。我們可以快速的找到它註冊用的數據結構:
[cpp] view plain copy
- static struct crypto_alg geode_cbc_alg = {
- .cra_name = "cbc(aes)",
- .cra_driver_name = "cbc-aes-geode",
- .cra_priority = 400,
- .cra_flags = CRYPTO_ALG_TYPE_BLKCIPHER |
- CRYPTO_ALG_NEED_FALLBACK,
- .cra_init = fallback_init_blk,
- .cra_exit = fallback_exit_blk,
- .cra_blocksize = AES_MIN_BLOCK_SIZE,
- .cra_ctxsize = sizeof(struct geode_aes_op),
- .cra_alignmask = 15,
- .cra_type = &crypto_blkcipher_type,
- .cra_module = THIS_MODULE,
- .cra_list = LIST_HEAD_INIT(geode_cbc_alg.cra_list),
- .cra_u = {
- .blkcipher = {
- .min_keysize = AES_MIN_KEY_SIZE,
- .max_keysize = AES_MAX_KEY_SIZE,
- .setkey = geode_setkey_blk,
- .encrypt = geode_cbc_encrypt,
- .decrypt = geode_cbc_decrypt,
- .ivsize = AES_IV_LENGTH,
- }
- }
- };
在alg結構中,塊加密與普通分組加密的區別就在.cra_u的設置。普通分組加密指定的是.cipher,同步塊加密指定的是.blkcipher,異步塊加密指定的是.ablkcipher。
上面那個aes-generic沒有使用,但這裏使用了的一對函數是cra_init和cra_exit,是對tfm的初始化和清理的操作,在這裏可以對tfm上附帶的ctx進行初始化。
在分組密碼中有一個概念是操作模式。geode-aes裏面有兩個alg,一個是ecb模式的alg,另一個是cbc模式的alg。關於ecb和cbc等加密模式的概念可以去查wiki[1]。我對密碼算法這一塊也不是很熟,不過可以稍微介紹一下ecb和cbc,有什麽不對可以指正。分組密碼算法通常是將固定大小的一整塊數據使用對稱密鑰進行直接加密,這就是ecb。在ecb模式下,每塊數據的明文和密文是一一對應的,前後塊數據之間可以分別加解密,沒有關聯關系。而在cbc模式中,前一塊數據被加密得到的密文與後一塊數據進行XOR運算之後再進行加密,因此前後相連的數據塊對應的密文有了關聯,解密的時候反過來操作。在cbc操作中就出現了一個用於xor的vector,也就是前一塊數據的密文。而第一塊數據使用的vector叫做initial vector(就是前文提到的iv),通常由用戶指定某個哈希算法生成。ivsize就是指定initial vector的大小,還有一個這裏沒使用的成員geniv是一個字符串,表示iv生成算法的算法名,這個iv生成算法必須是在Linux密碼算法管理裏註冊的算法。
同步塊加密的三大函數原型為:
int int set_key(struct crypto_tfm *tfm, const u8 *in_key, unsigned int key_len);
int encrypt(struct blkcipher_desc *desc, struct scatterlist *dst, struct scatterlist *src, unsigned int nbytes);
int decrypt(struct blkcipher_desc *desc, struct scatterlist *dst, struct scatterlist *src, unsigned int nbytes);
這些函數的返回值是int,代表一個系統錯誤碼。blkcipher_desc是貫穿始終的數據結構,該結構定義在<srcdir>/include/linux/crypto.h中:
[cpp] view plain copy
- struct blkcipher_desc {
- struct crypto_blkcipher *tfm;
- void *info;
- u32 flags;
- };
tfm與普通分組加密中講到的transform是一類概念,由tfm可以得到ctx。info通常用來存放iv,因為塊加密的散集序列工具(scatterwalk)在初始化時直接將info當作iv來使用。
那麽散集序列(scatterlist)又是什麽呢?
在Linux內核中,跟外設打交道有三種方式:IO、端口和DMA,這個教科書上都講了。其中DMA方式是由DMA控制器來控制內存、外設間的數據傳輸。我們知道,Linux的地址空間有三種:虛擬地址、物理地址和總線地址。DMA要求每次傳輸的一整塊數據分布在連續的總線地址空間上。而DMA是為傳輸大塊數據設計的,但是大塊的連續總線地址空間通常是稀缺的。因此當沒有那麽多連續空間時,只能將大塊數據分散到盡可能少的小塊連續地址上,然後讓DMA控制器一塊接著一塊地把數據全部傳完。因此Linux內核中專門設計了一種叫做散集序列(scatterlist)的數據結構將小塊的連續總線地址串起來,交給DMA驅動自動地一個接著一個地傳輸。
說白了,scatterlist就是一個線性表(scatterlist可以是鏈表,也可以是數組),每個元素包含一個指針指向一塊總線地址連續的內存塊,這是為DMA量身定做的數據結構。
scatterlist的定義是體系結構相關的,因此定義在<srcdir>/arch/某體系結構/include/scatterlist.h裏,但是x86使用的是通用定義,在<srcdir>/arch/asm-generic/include/scatterlist.h中。
[cpp] view plain copy
- struct scatterlist {
- #ifdef CONFIG_DEBUG_SG
- unsigned long sg_magic;
- #endif
- unsigned long page_link;
- unsigned int offset;
- unsigned int length;
- dma_addr_t dma_address;
- unsigned int dma_length;
- };
page_link指定該內存塊在哪一個頁面中,低2位分別用作鏈表/數組選擇標誌和結束標誌;offset表示內存塊在頁面中的偏移;length代表數據塊長度;dma_address是內存塊的總線地址;dma_length是總線地址空間長度,它與length區別在於length用於32位平臺的,dma_length用於64位平臺。
對scatterlist的線性表操作定義在<srcdir>/include/linux/scatterlist.h中。大家可以自己去看,代碼並不長,而且都很簡單。
1.3 異步塊加密
異步塊加密以<srcdir>/drivers/crypto/mv_cesa.c為例,該驅動是Marvell的一個硬件加密引擎驅動。它的alg是這樣定義的:
[cpp] view plain copy
- struct crypto_alg mv_aes_alg_cbc = {
- .cra_name = "cbc(aes)",
- .cra_driver_name = "mv-cbc-aes",
- .cra_priority = 300,
- .cra_flags = CRYPTO_ALG_TYPE_ABLKCIPHER | CRYPTO_ALG_ASYNC,
- .cra_blocksize = AES_BLOCK_SIZE,
- .cra_ctxsize = sizeof(struct mv_ctx),
- .cra_alignmask = 0,
- .cra_type = &crypto_ablkcipher_type,
- .cra_module = THIS_MODULE,
- .cra_init = mv_cra_init,
- .cra_u = {
- .ablkcipher = {
- .ivsize = AES_BLOCK_SIZE,
- .min_keysize = AES_MIN_KEY_SIZE,
- .max_keysize = AES_MAX_KEY_SIZE,
- .setkey = mv_setkey_aes,
- .encrypt = mv_enc_aes_cbc,
- .decrypt = mv_dec_aes_cbc,
- },
- },
- };
它與上面同步塊加密的區別在於cra_flags和cra_type不同,更重要的區別是指定.cra_u.ablkcipher結構,該結構與blkcipher差不多,也有set_key、encrypt、decrypt三大函數。但是函數原型不同:
int set_key(struct crypto_ablkcipher *cipher, const u8 *key, unsigned int len);
int encrypt(struct ablkcipher_request *req);
int decrypt(struct ablkcipher_request *req);
set_key第一個參數struct crypto_ablkcipher *cipher其實就是crypto_tfm。而encrypt/decrypt的參數struct ablkcipher_request *req 代表的是異步請求:
[cpp] view plain copy
- struct ablkcipher_request {
- struct crypto_async_request base;
- unsigned int nbytes;
- void *info;
- struct scatterlist *src;
- struct scatterlist *dst;
- void *__ctx[] CRYPTO_MINALIGN_ATTR;
- };
很明顯,這是專為參數傳遞準備的一個結構。與同步塊加密和普通分組加密不同的是ablkcipher_request後面也有一個ctx,它與crypto_tfm的ctx不同在於:後者是每個實例的ctx,在init(crypto_tfm*)中初始化;而ablkcipher_request的ctx是屬於每個request的,encrypt和decrypt中初始化(也就是系統傳給算法的request中包含的ctx是未初始化的,千萬別當作crypto_tfm的ctx使用)。這兩個ctx的大小是一致的,都由alg的ctx_size決定。
struct crypto_async_request base是用作異步通知的結構;nbytes、src、dst和同步塊加密的encrypt/decrypt對應參數一樣;info在這裏通常也是作為iv指針使用。如果沒有iv,可以挪作他用。
在base成員中,有一個complete函數指針,類型為typedef void (*crypto_completion_t)(struct crypto_async_request *req, int err);,這個函數由異步塊加密算法調用,當某個異步request操作完成時,調用該函數通知request已完成。第一個參數就是這個request指針,第二個參數是系統錯誤碼。
因為是異步操作,因此系統已經為算法提供了一個請求緩存池,可以通過<srcdir>/include/crypto/algapi.h中定義的ablkcipher_enqueue_request/ablkcipher_dequeue_request函數來操作。
二、dm-crypt分析
dm-crypt是dm構架中用於塊設備加密的模塊。dm-crypt通過dm虛擬一個塊設備,並在bio轉發的時候將數據加密後存儲來實現塊設備的加密,而這些對於應用層是透明的。dm-crypt的target_type定義如下:
[cpp] view plain copy
- static struct target_type crypt_target = {
- .name = "crypt",
- .version = {1, 7, 0},
- .module = THIS_MODULE,
- .ctr = crypt_ctr,
- .dtr = crypt_dtr,
- .map = crypt_map,
- .status = crypt_status,
- .postsuspend = crypt_postsuspend,
- .preresume = crypt_preresume,
- .resume = crypt_resume,
- .message = crypt_message,
- .merge = crypt_merge,
- .iterate_devices = crypt_iterate_devices,
- };
這裏重點分析ctr和map函數。ctr決定了設備的創建過程、也決定了與密碼算法的關聯過程;map決定了bio轉發,也決定了對密碼算法調用的步驟。設備創建和bio轉發在前文中已經講過。這裏重點分析與密碼算法的關聯。
2.1 創建密碼算法實例
crypt_ctr函數的代碼很長,我貼在這裏,一般情況下就沒必要展開了。
[cpp] view plain copy
- static int crypt_ctr(struct dm_target *ti, unsigned int argc, char **argv)
- {
- struct crypt_config *cc;
- struct crypto_ablkcipher *tfm;
- char *tmp;
- char *cipher;
- char *chainmode;
- char *ivmode;
- char *ivopts;
- unsigned int key_size;
- unsigned long long tmpll;
- if (argc != 5) {
- ti->error = "Not enough arguments";
- return -EINVAL;
- }
- tmp = argv[0];
- cipher = strsep(&tmp, "-");
- chainmode = strsep(&tmp, "-");
- ivopts = strsep(&tmp, "-");
- ivmode = strsep(&ivopts, ":");
- if (tmp)
- DMWARN("Unexpected additional cipher options");
- key_size = strlen(argv[1]) >> 1;
- cc = kzalloc(sizeof(*cc) + key_size * sizeof(u8), GFP_KERNEL);
- if (cc == NULL) {
- ti->error =
- "Cannot allocate transparent encryption context";
- return -ENOMEM;
- }
- /* Compatibility mode for old dm-crypt cipher strings */
- if (!chainmode || (strcmp(chainmode, "plain") == 0 && !ivmode)) {
- chainmode = "cbc";
- ivmode = "plain";
- }
- if (strcmp(chainmode, "ecb") && !ivmode) {
- ti->error = "This chaining mode requires an IV mechanism";
- goto bad_cipher;
- }
- if (snprintf(cc->cipher, CRYPTO_MAX_ALG_NAME, "%s(%s)",
- chainmode, cipher) >= CRYPTO_MAX_ALG_NAME) {
- ti->error = "Chain mode + cipher name is too long";
- goto bad_cipher;
- }
- tfm = crypto_alloc_ablkcipher(cc->cipher, 0, 0);
- if (IS_ERR(tfm)) {
- ti->error = "Error allocating crypto tfm";
- goto bad_cipher;
- }
- strcpy(cc->cipher, cipher);
- strcpy(cc->chainmode, chainmode);
- cc->tfm = tfm;
- if (crypt_set_key(cc, argv[1]) < 0) {
- ti->error = "Error decoding and setting key";
- goto bad_ivmode;
- }
- /*
- * Choose ivmode. Valid modes: "plain", "essiv:<esshash>", "benbi".
- * See comments at iv code
- */
- if (ivmode == NULL)
- cc->iv_gen_ops = NULL;
- else if (strcmp(ivmode, "plain") == 0)
- cc->iv_gen_ops = &crypt_iv_plain_ops;
- else if (strcmp(ivmode, "plain64") == 0)
- cc->iv_gen_ops = &crypt_iv_plain64_ops;
- else if (strcmp(ivmode, "essiv") == 0)
- cc->iv_gen_ops = &crypt_iv_essiv_ops;
- else if (strcmp(ivmode, "benbi") == 0)
- cc->iv_gen_ops = &crypt_iv_benbi_ops;
- else if (strcmp(ivmode, "null") == 0)
- cc->iv_gen_ops = &crypt_iv_null_ops;
- else {
- ti->error = "Invalid IV mode";
- goto bad_ivmode;
- }
- if (cc->iv_gen_ops && cc->iv_gen_ops->ctr &&
- cc->iv_gen_ops->ctr(cc, ti, ivopts) < 0)
- goto bad_ivmode;
- if (cc->iv_gen_ops && cc->iv_gen_ops->init &&
- cc->iv_gen_ops->init(cc) < 0) {
- ti->error = "Error initialising IV";
- goto bad_slab_pool;
- }
- cc->iv_size = crypto_ablkcipher_ivsize(tfm);
- if (cc->iv_size)
- /* at least a 64 bit sector number should fit in our buffer */
- cc->iv_size = max(cc->iv_size,
- (unsigned int)(sizeof(u64) / sizeof(u8)));
- else {
- if (cc->iv_gen_ops) {
- DMWARN("Selected cipher does not support IVs");
- if (cc->iv_gen_ops->dtr)
- cc->iv_gen_ops->dtr(cc);
- cc->iv_gen_ops = NULL;
- }
- }
- cc->io_pool = mempool_create_slab_pool(MIN_IOS, _crypt_io_pool);
- if (!cc->io_pool) {
- ti->error = "Cannot allocate crypt io mempool";
- goto bad_slab_pool;
- }
- cc->dmreq_start = sizeof(struct ablkcipher_request);
- cc->dmreq_start += crypto_ablkcipher_reqsize(tfm);
- cc->dmreq_start = ALIGN(cc->dmreq_start, crypto_tfm_ctx_alignment());
- cc->dmreq_start += crypto_ablkcipher_alignmask(tfm) &
- ~(crypto_tfm_ctx_alignment() - 1);
- cc->req_pool = mempool_create_kmalloc_pool(MIN_IOS, cc->dmreq_start +
- sizeof(struct dm_crypt_request) + cc->iv_size);
- if (!cc->req_pool) {
- ti->error = "Cannot allocate crypt request mempool";
- goto bad_req_pool;
- }
- cc->req = NULL;
- cc->page_pool = mempool_create_page_pool(MIN_POOL_PAGES, 0);
- if (!cc->page_pool) {
- ti->error = "Cannot allocate page mempool";
- goto bad_page_pool;
- }
- cc->bs = bioset_create(MIN_IOS, 0);
- if (!cc->bs) {
- ti->error = "Cannot allocate crypt bioset";
- goto bad_bs;
- }
- if (sscanf(argv[2], "%llu", &tmpll) != 1) {
- ti->error = "Invalid iv_offset sector";
- goto bad_device;
- }
- cc->iv_offset = tmpll;
- if (sscanf(argv[4], "%llu", &tmpll) != 1) {
- ti->error = "Invalid device sector";
- goto bad_device;
- }
- cc->start = tmpll;
- if (dm_get_device(ti, argv[3], cc->start, ti->len,
- dm_table_get_mode(ti->table), &cc->dev)) {
- ti->error = "Device lookup failed";
- goto bad_device;
- }
- if (ivmode && cc->iv_gen_ops) {
- if (ivopts)
- *(ivopts - 1) = ‘:‘;
- cc->iv_mode = kmalloc(strlen(ivmode) + 1, GFP_KERNEL);
- if (!cc->iv_mode) {
- ti->error = "Error kmallocing iv_mode string";
- goto bad_ivmode_string;
- }
- strcpy(cc->iv_mode, ivmode);
- } else
- cc->iv_mode = NULL;
- cc->io_queue = create_singlethread_workqueue("kcryptd_io");
- if (!cc->io_queue) {
- ti->error = "Couldn‘t create kcryptd io queue";
- goto bad_io_queue;
- }
- cc->crypt_queue = create_singlethread_workqueue("kcryptd");
- if (!cc->crypt_queue) {
- ti->error = "Couldn‘t create kcryptd queue";
- goto bad_crypt_queue;
- }
- ti->num_flush_requests = 1;
- ti->private = cc;
- return 0;
- bad_crypt_queue:
- destroy_workqueue(cc->io_queue);
- bad_io_queue:
- kfree(cc->iv_mode);
- bad_ivmode_string:
- dm_put_device(ti, cc->dev);
- bad_device:
- bioset_free(cc->bs);
- bad_bs:
- mempool_destroy(cc->page_pool);
- bad_page_pool:
- mempool_destroy(cc->req_pool);
- bad_req_pool:
- mempool_destroy(cc->io_pool);
- bad_slab_pool:
- if (cc->iv_gen_ops && cc->iv_gen_ops->dtr)
- cc->iv_gen_ops->dtr(cc);
- bad_ivmode:
- crypto_free_ablkcipher(tfm);
- bad_cipher:
- /* Must zero key material before freeing */
- kzfree(cc);
- return -EINVAL;
- }
crypt_ctr的參數格式是<cipher> <key> <iv_offset> <dev_path> <start>,這些參數在ctr中被一一解析並存放到crypt_config結構中。
<cipher>的格式是cipher-chainmode-ivopts:ivmode。cipher就是算法註冊時的cra_name;chainmode就是前面所說的ecb/cbc之類。chainmode默認是cbc,如果chainmode不是ecb,則必須指定ivmode。ivmode有5種:plain、plain64、essiv、benbi和null,分別對應不同的iv生成算法,而ivopts是傳給這幾種ivmode的ctr的參數,其中null、benbi、plain和plain64沒有使用,而essiv將ivopts作為在系統中註冊的哈希算法名,由該哈希算法生成iv。
<start>是加密的起始塊,start之前不由dm-crypt管理。
<iv_offset>是為了保存iv到磁盤上而預留位置(單位:sector)。因此dm-crypt設備上偏移為sector的bio對應與原始磁盤上sector+<iv_offset>+<start>偏移的塊。但是對於dm-crypt內部來講,偏移為sector+<iv_offset>,也就是說dm-crypt內部將iv所占據的那些塊給隱藏了。但是我沒有在代碼中發現dm-crypt使用了<start>到<iv_offset>之間的空間。
2.2 密碼算法的調用
crypt_map用來修改bio的內容然後轉發。其讀流程是這樣的:
crypt_map
`-> kcryptd_queue_io(io) // io結構包含bio、ti等信息
`-> queue_work(cc->io_queue, &io->work) // 添加到io隊列
(隊列io)
`-> kcryptd_io(struct work_struct *work)
`-> kcryptd_io_read(io) // io 是 work 的容器,反向獲取
`-> generic_make_request(clone); // clone是io->base_bio的克隆,設置有異步回調
(異步io)
`-> crypt_endio(struct bio *clone, int error) // 讀操作完成回調,得到密文,保存在clone中
`-> kcryptd_queue_crypt(io) // io由clone得到
`-> queue_work(cc->crypt_queue, &io->work) // 添加到crypt隊列
(隊列crypt)
`-> kcryptd_crypt(struct work_struct *work)
`-> kcryptd_crypt_read_convert(io); // io 是 work 的容器,反向獲取
`-> crypt_convert(cc, &io->ctx) // cc 由 io 獲得
`-> crypt_convert_block(cc, ctx, cc->req) // 執行請求
`-> crypto_ablkcipher_decrypt(req) // 調用異步密碼算法
(異步crypt)
`-> kcryptd_async_done(struct crypto_async_request *async_req, int error)
`-> kcryptd_crypt_read_done(io, error) // 清理 io,然後結束
寫操作的流程與讀操作不同在於要先encrypt再io,因此寫操作的兩次io異步在兩次crypt異步之後。
由於dm-crypt使用的是異步塊加密算法,那麽就有兩個問題:
1、dm-crypt請求數據肯定比密碼算法處理數據的速度要快,而隊列總有滿的時候。dm-crypt如何知道適可而止?
2、dm-crypt支持同步塊加密甚至普通分組加密算法嗎?
對於第一個問題,我們可以看到crypt_convert_block函數的代碼是這樣的:
[cpp] view plain copy
- static int crypt_convert(struct crypt_config *cc,
- struct convert_context *ctx)
- {
- int r;
- atomic_set(&ctx->pending, 1);
- while(ctx->idx_in < ctx->bio_in->bi_vcnt &&
- ctx->idx_out < ctx->bio_out->bi_vcnt) {
- crypt_alloc_req(cc, ctx);
- atomic_inc(&ctx->pending);
- r = crypt_convert_block(cc, ctx, cc->req);
- switch (r) {
- /* async */
- case -EBUSY:
- wait_for_completion(&ctx->restart);
- INIT_COMPLETION(ctx->restart);
- /* fall through*/
- case -EINPROGRESS:
- cc->req = NULL;
- ctx->sector++;
- continue;
- /* sync */
- case 0:
- atomic_dec(&ctx->pending);
- ctx->sector++;
- cond_resched();
- continue;
- /* error */
- default:
- atomic_dec(&ctx->pending);
- return r;
- }
- }
- return 0;
- }
可以看出,如果異步密碼算法的encrypt/decrypt返回-EBUSY,則dm-crypt陷入等待之中;如果返回-EINPROGRESS表示已將請求移入隊列,dm-crypt會繼續下一個請求;如果返回0表示已經完成,異步變成同步了(從這一點看,dm-crypt是支持同步塊加密的)。
那麽如何將陷入等待的dm-crypt喚醒呢?在密碼算法的異步回調kcryptd_async_done函數(它就是struct crypto_async_request base中的complete函數)中有一段是這樣的:
if (error == -EINPROGRESS) {
complete(&ctx->restart);
return;
}
這說明當一個標以EBUSY的request被error=-EINPROGRESS方式complete的時候,complete異步回調會喚醒dm-crypt而不幹其他的事情。這還說明了另外一個問題:那個被標以EBUSY的request仍然要被異步密碼算法記錄下來,因為這個request必須被額外complete一次,而且dm-crypt不會重發這個request。
對於第二個問題,答案是肯定的。<srcdir>/crypto/ablkcipher.c中crypto_lookup_skcipher函數在沒有找到對應塊加密算法的情況下,創建密碼算法孵化器,並由孵化器創建指定算法的異步塊加密算法。不過這個機制目前還沒研究透徹,因為這種孵化機制與塊加密算法的實現、以及算法模板的實現是緊密關聯的。
三、其他
當為一塊硬件密碼引擎寫異步塊加密驅動時,首先要了解硬件的構架,至少要知道如何把數據傳輸進去,然後再把處理後的數據傳輸出來,而且相關的等待機制也很關鍵。
dm-crypt傳給算法的每一個request只包含一個sector,即512字節。如果硬件密碼引擎每次處理了的數據量遠大於這個數目的話,每次只灌入一個sector的數據是一種浪費。可以考慮把隊列中相鄰甚至不相鄰的sector合並到一個scatterlist裏面進行DMA。這樣設計就不能使用內核中已有的那個請求隊列,而得自己設計一個效率更高的。
從我自己的實踐來看,整個異步的效率瓶頸在數據的準備和DMA上。我使用的硬件加密卡每次只能處理一塊數據,設想流程如果是:準備數據->DMA->等待->DMA->轉發數據,這裏面的兩次DMA加等待和數據的準備與轉發可以並發完成。為了讓DMA和硬件引擎滿負荷,可以設置兩個線程:一個專門DMA-等待-DMA,另外一個專門準備數據和轉發數據;並準備兩塊內存,一塊用於DMA,一塊用於轉發和準備。由於CPU的速度遠大於外設,只要中間不涉及內存拷貝,轉發和準備數據的那個線程總是要快一些的,而DMA的那個線程就可以基本滿負荷運轉了,除了中間等待硬件加解密外。
如果硬件支持流式處理,即上一塊數據正在處理時,可以繼續DMA下一塊數據,那麽就真的可以讓DMA滿負荷了。如果只能使用一個DMA通道,就只用一個DMA線程;如果還可以使用兩個DMA通道,就可以設置兩個DMA線程,一個往裏面灌,另外一個往外倒,分別使用兩個不同的DMA通道。
還有就是scatterlist是為DMA傳輸準備的。但是dm-crypt傳給算法的scatterlist是否可以直接去DMA還有待分析。例如對於一些只支持24位總線尋址的設備,高地址的scatterlist就不能DMA。dm-crypt直接將bio的page設置給scatterlist。這個page能否DMA是未知的。最好是創建pci設備相關的dma內存,然後把數據拷貝過去。數據拷貝是相當費時的,不過這屬於數據準備和轉發階段要做的事情,讓它跟DMA並發就可以了。
總之,效率這回事不光是跟軟件有關,還跟硬件有關。如果硬件不爭氣,那只有拼CPU了。我試驗過,單線程7MBps,多線程可以22MBps。理論最大可以50MBps,因為拷貝操作太多。最後也沒再去優化了。
Linux塊設備加密之dm-crypt分析