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和数据结构大多与用户态不太相同,这个时候需要查看内核中的相关代码寻找线索。
可以使用在线文档工具查看相关的函数定义。