Linux内核加密模块crypto的使用

最近接手了一个Linux下内核编程的项目,在阅读项目原有代码的基础上,学到了很多新知识,总结一下记录在这里。

在这个项目中,编写内核模块进行加解密操作,使用了Linux内核提供的crypto加密API。

环境

操作系统:Ubuntu18.10(使用Linux 4.18.0-25内核)

使用的头文件如下:

#include <crypto/hash.h>
#include <crypto/skcipher.h>
#include <linux/cred.h>
#include <linux/scatterlist.h>

概述

在Linux内核中提供了加密API,通过一组头文件crypto引出。整体的思路为首先创建加密上下文,并且在上下文中注册使用的算法,最后使用内核API完成加解密的操作。这里以散列计算和对称加密为例。

散列操作

函数目标为通过用户的ID(长度为4Byte)生成长度为32Byte的序列作为对称加密的密钥。

static void generate_key(unsigned char* key) {
    // 创建散列操作
    struct shash_desc sdesc;
    uid_t uid = current_uid().val;
    short i;
    // 申请运算上下文,指定算法为crc32-pclmul
    sdesc.tfm = crypto_alloc_shash("crc32-pclmul", 0, 0);
    // 这里选择的hash算法每次生成4Byte(32bit)长度的输出
    // 满足32Byte(256bit)长度的密钥需要迭代生成
    // 每次使用之前生成的部分计算哈希,将结果与之前的结果拼接起来
    crypto_shash_digest(&sdesc, (char*)&uid, sizeof(uid_t), key + 28);
    for (i = 28; i > 0; i -= 4) {
        crypto_shash_digest(&sdesc, key + i, 32 - i, key + i - 4);
    }
    // 释放空间
    crypto_free_shash(sdesc.tfm);
}

对称加密

函数目标为当读写文件时,使用加密读写,加密是使用的参数来自用户、文件Inode、读写的位置。实现的细节参见注释。

static void transform(char* ubuf, unsigned long inode, loff_t offset, size_t count) {
    // 创建对称加密操作
    struct crypto_skcipher* skcipher = NULL;
    struct skcipher_request* req = NULL;
    struct scatterlist sg;
    // 密钥和初始化向量的空间
    unsigned char key[32] = { 0 };
    char ivdata[16] = { 0 };
    // 处理时以16Byte(128bit)为单位
    // 将文件分为16Byte的分段时,偏移量低四位表示位于上一分段的字节数
    // 因此需要额外处理,将上一分段读取出来
    short pre_len = offset & 0xf;
    char prefix[15] = { 0 };
    //
    char* buf;
    buf = (char*)kmalloc(count + pre_len, GFP_KERNEL);
    copy_from_user(buf + pre_len, (void *)ubuf, count);

    // 为算法申请内核中运算的上下文
    // 在crypto_alg_list链表中查询,找到AES的CTR模式并注册
    // 在内核中为该算法的各个函数指针初始化
    skcipher = crypto_alloc_skcipher("ctr-aes-aesni", 0, 0);
    // 在该上下文空间中申请数据处理请求
    // 实际上完成了后台的内存申请和绑定
    req = skcipher_request_alloc(skcipher, GFP_KERNEL);

    // 创建256bit的密钥,并写入本次运算的上下文内存中
    generate_key(key);
    crypto_skcipher_setkey(skcipher, key, 32);

    // 创建初始化向量iv
    generate_iv(ivdata, inode, offset >> 4);
    // 在内存空间中开辟并维护一段内存
    // scatterlist用于维护大段的被多个组件访问的内存(例如,CPU和DMA)
    // 根据位于上一分段的字节数扩展需要的内存
    sg_init_one(&sg, buf, count + pre_len);

    // 将待加密数据放入本次运算的请求空间
    // 第二/三参数分别表示source和destination
    // 第四/五参数为待加密数据的长度和初始化向量
    skcipher_request_set_crypt(req, &sg, &sg, count + pre_len, ivdata);

    // 开始加密
    // 将位于上一分段的数据保护在prefix中,防止被二次加密
    memcpy(prefix, buf, pre_len);
    crypto_skcipher_encrypt(req);
    memcpy(buf, prefix, pre_len);

    copy_to_user((void *)ubuf, buf + pre_len, count);
    kfree(buf);
    // 清空本次处理的内存,释放空间
    skcipher_request_free(req);
    crypto_free_skcipher(skcipher);
}

总结

在Linux内核编程的过程中,需要注意使用的API和数据结构大多与用户态不太相同,这个时候需要查看内核中的相关代码寻找线索。

可以使用在线文档工具查看相关的函数定义。

Lei Yang
Lei Yang
PhD candidate

My research interests include visual speech recognition and semantics segmentation.