相关的分析工作一年前就做完了,一直懒得写下来。现在觉得还是写下来,以来怕自己忘记了,二来可以给大家分享一下自己的研究经验。
这篇文章算是《Device Mapper代码分析》的后续篇,因为dm-crypt是基于dm框架的,因此与上一篇一样,也以2.6.33内核代码为基础来讲述代码的分析过程。但是本文侧重点不同在于着重分析一下三个方面:
1、Linux密码管理
2、dm-crypt到与Linux密码的关联
3、dm-crypt的异步处理
Linux内核中,密码相关的头文件在
我们可以从内核代码中挑一个简单而普通的加密算法来研究一下,例如
所有加密算法都是以内核模块方式编写的。所有内核模块的代码都是先看关键数据结构,再看算法。aes先声明了一个叫做crypto_alg的结构体,如下:
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的完整定义在
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的定义在
#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);来进行转换,该函数也在
现在可以梳理一下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指针,由此获得其他的结构指针。当然,你无需直接访问成员,而是使用
现在,一个普通的分组加密算法就很好理解了。首先声明一个crypto_alg结构注册到密码管理器中,当外界使用到该算法时,密码管理器会自动创建crypto_tfm并调用那三大函数进行加解密操作,对应函数返回就表示操作已完成,属于同步操作。接下来要讲的块加密其实也是类似的。
我们挑一个功能比较全面的块加密算法来看看,如
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是贯穿始终的数据结构,该结构定义在
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的定义是体系结构相关的,因此定义在
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的线性表操作定义在
异步块加密以
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 代表的是异步请求:
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指针,第二个参数是系统错误码。
因为是异步操作,因此系统已经为算法提供了一个请求缓存池,可以通过
dm-crypt是dm构架中用于块设备加密的模块。dm-crypt通过dm虚拟一个块设备,并在bio转发的时候将数据加密后存储来实现块设备的加密,而这些对于应用层是透明的。dm-crypt的target_type定义如下:
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转发在前文中已经讲过。这里重点分析与密码算法的关联。
crypt_ctr函数的代码很长,我贴在这里,一般情况下就没必要展开了。
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:
crypt_ctr的参数格式是
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函数的代码是这样的:
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。
对于第二个问题,答案是肯定的。
当为一块硬件密码引擎写异步块加密驱动时,首先要了解硬件的构架,至少要知道如何把数据传输进去,然后再把处理后的数据传输出来,而且相关的等待机制也很关键。
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,因为拷贝操作太多。最后也没再去优化了。
[1]Block cipher mode of operation -- wikipedia