相关的分析工作一年前就做完了,一直懒得写下来。现在觉得还是写下来,以来怕自己忘记了,二来可以给大家分享一下自己的研究经验。
这篇文章算是《》的后续篇,因为 dm-crypt 是基于 dm 框架的,因此与上一篇一样,也以 2.6.33 内核代码为基础来讲述代码的分析过程。但是本文侧重点不同在于着重分析一下三个方面:
1、Linux 密码管理
2、dm-crypt 到与 Linux 密码的关联
3、dm-crypt 的异步处理
Linux 内核中,密码相关的头文件在 <srcdir>/include/crypto / 下,实现文件在 <srcdir>/crypto / 下。相关的概念大致有加密、块加密、异步块加密、哈希、分组加密模式(ECB/CBC/CFB/OFB/CTR)等。接下来一一进行简单分析。
我们可以从内核代码中挑一个简单而普通的加密算法来研究一下,例如 <srcdir>/crypto/aes_generic.c 描述的 AES 算法。
所有加密算法都是以内核模块方式编写的。所有内核模块的代码都是先看关键数据结构,再看算法。aes 先声明了一个叫做 crypto_alg 的结构体,如下:
alg 是 algorithm 的缩写。所有的加密、哈希等算法注册用数据结构都叫做 xxx_alg,crypto_alg 的完整定义在 <srcdir>/include/linux/crypto.h 中:
- 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
- }
- }
- };
- 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 对应的三大函数)。
in 和 out 参数就是加解密之前和之后的传入传出数据,长度就是 alg 中的 blocksize。这三个函数都有类型为 struct crypto_tfm * 的参数。tfm 是 transform 的缩写。所有加密、哈希算法的 set_key、encrypt、decrypt 都带有这个参数。crypto_tfm 的定义在 <srcdir>/include/linux/crypto.h 中:
- 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);
- #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 指针,由此获得其他的结构指针。当然,你无需直接访问成员,而是使用
现在,一个普通的分组加密算法就很好理解了。首先声明一个 crypto_alg 结构注册到密码管理器中,当外界使用到该算法时,密码管理器会自动创建 crypto_tfm 并调用那三大函数进行加解密操作,对应函数返回就表示操作已完成,属于同步操作。接下来要讲的块加密其实也是类似的。
我们挑一个功能比较全面的块加密算法来看看,如 <srcdir>/drivers/crypto/geode-aes.c。这是 AMD 的一个硬件加密引擎的驱动,以算法模块方式插入到内核中,驱动硬件进行加解密。我们可以快速的找到它注册用的数据结构:
- 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。我对密码算法这一块也不是很熟,不过可以稍微介绍一下 ecb 和 cbc,有什么不对可以指正。分组密码算法通常是将固定大小的一整块数据使用对称密钥进行直接加密,这就是 ecb。在 ecb 模式下,每块数据的明文和密文是一一对应的,前后块数据之间可以分别加解密,没有关联关系。而在 cbc 模式中,前一块数据被加密得到的密文与后一块数据进行 XOR 运算之后再进行加密,因此前后相连的数据块对应的密文有了关联,解密的时候反过来操作。在 cbc 操作中就出现了一个用于 xor 的 vector,也就是前一块数据的密文。而第一块数据使用的 vector 叫做 initial vector(就是前文提到的 iv),通常由用户指定某个哈希算法生成。ivsize 就是指定 initial vector 的大小,还有一个这里没使用的成员 geniv 是一个字符串,表示 iv 生成算法的算法名,这个 iv 生成算法必须是在 Linux 密码算法管理里注册的算法。
同步块加密的三大函数原型为:
这些函数的返回值是 int,代表一个系统错误码。blkcipher_desc 是贯穿始终的数据结构,该结构定义在 <srcdir>/include/linux/crypto.h 中:
- 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);
- 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 中。
- 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 中。大家可以自己去看,代码并不长,而且都很简单。
异步块加密以 <srcdir>/drivers/crypto/mv_cesa.c 为例,该驱动是 Marvell 的一个硬件加密引擎驱动。它的 alg 是这样定义的:
它与上面同步块加密的区别在于 cra_flags 和 cra_type 不同,更重要的区别是指定. cra_u.ablkcipher 结构,该结构与 blkcipher 差不多,也有 set_key、encrypt、decrypt 三大函数。但是函数原型不同:
- 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,
- },
- },
- };
set_key 第一个参数 struct crypto_ablkcipher *cipher 其实就是 crypto_tfm。而 encrypt/decrypt 的参数 struct ablkcipher_request *req 代表的是异步请求:
- 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);
- 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 构架中用于块设备加密的模块。dm-crypt 通过 dm 虚拟一个块设备,并在 bio 转发的时候将数据加密后存储来实现块设备的加密,而这些对于应用层是透明的。dm-crypt 的 target_type 定义如下:
这里重点分析 ctr 和 map 函数。ctr 决定了设备的创建过程、也决定了与密码算法的关联过程;map 决定了 bio 转发,也决定了对密码算法调用的步骤。设备创建和 bio 转发在前文中已经讲过。这里重点分析与密码算法的关联。
- 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,
- };
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:<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> 之间的空间。
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。
对于第二个问题,答案是肯定的。<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,因为拷贝操作太多。最后也没再去优化了。
Block cipher mode of operation -- wikipedia
http://blog.csdn.net/sonicling/article/details/6275898
[1]
来源: http://lib.csdn.net/article/linux/37794