Openssl 加密算法库编程精要 03 详解 EVP API 对称加密
Openssl EVP 对称加密指南
参考资料
- Openssl 官方网站
- Openssl 官方文档
- 《密码学原理与实践(第三版)》,Douglas R.Stinson 冯登国等译
- 《GB/T 32907-2016 信息安全技术 SM4 分组密码算法》
3.1 简介
Openssl 提供的 EVP API 提供了高层级、相对抽象的密码加密接口,是我们最常用的 Openssl 模块之一。它提供了丰富的功能和特性:
- 一系列抽象的密码接口,用户无需考虑底层复杂的实现,使用简单
- 支持对称加密和解密操作,并且支持使用对称算法的各种常用的模式
- 支持消息摘要算法
- 支持非对称加解密和签名验签
- 支持密钥派生
- 支持生成消息认证码
3.2 EVP API 的调用规律
我们从上一章节的内容可以知道,调用高层次的 EVP API 基本都遵循固定的规律:
- 获取算法实现
- 创建上下文 CTX
- 调用 EVP_***Init
- 调用 EVP_***Update
- 调用 EVP_***Final
- 销毁上下文,清理资源
虽然表面看起来确实如此,但是对于很多大部分公开密钥算法而言,由于这些算法的数学原理大相径庭,所以将它们抽象成类似对称算法和摘要算法那样形式的接口非常的困难,所以以上规律在我们使用非对称公开密钥算法时并不适用,这里各位读者需要注意,以后有机会我会对公开密钥算法的调用做专门的说明。
3.3 获取算法
在 EVP API 中,和对称算法相关的接口的前缀都是 EVP_CIPHER,我从这里面挑出我认为比较常用的几个接口来说明一下。
首先是获取算法接口:
1 |
|
调用 EVP_CIPHER_fetch 接口第一个参数可以传 NULL,会默认采用全局库上下文。
使用这个接口获取算法在 Openssl 内部被称为“显式获取(Explicit fetching)”,不同于旧版本的直接指定算法实现的方式,新版本在调用 EVP_CIPHER_fetch 获取算法后,需要调用 EVP_CIPHER_free 接口来释放 EVP_CIPHER 对象。
调用 EVP_CIPHER_fetch 接口获取到算法实现后,我们可以通过该实现获取一系列的对称算法信息,比如密钥长度、初始向量长度、分组长度等,接口说明如下:
1 |
|
具体的调用示例如下 – 示例1:
1 |
|
结果如下:
1 |
|
可以看到我们正确获取到了 SM4 算法分组链接模式的算法信息。
在使用获取到的算法实现进行运算之前,我们需要获取到加密上下文 – cipher context,对应 Openssl 中则是 EVP_CIPHER_CTX 对象,创建和销毁上下文的接口说明如下:
1 |
|
加密上下文相当于执行加密运算时的环境,里面维护了加解密运算时需要的临时数据,在执行加密运算初始化前,必须首先创建加密上下文,我们还是以 SM4-CBC 算法为例,先看一下完整的示例程序示例 2:
1 |
|
这里我们单独定义了一个名为 cipher 的函数,cipher 函数中首先定义了密钥 key 和初始化向量 iv,这里我提供几个生成 key 和 iv 的方法:
- 拍脑门随机想两个对应长度的值
- 直接调用随机数生成器 RAND_bytes 生成两个对应的长度的值
- 创建 EVP_CIPHER_CTX 后,调用 EVP_CIPHER_CTX_rand_key 接口生成随机密钥, iv 可以采用 1 或者 2 的方式生成
我们采用第一种方法… 然后分别调用了 EVP_CipherInit_ex2、EVP_CipherUpdate 和 EVP_CipherFinal_ex 三个接口来完成对称加密解密操作,这三个接口定义如下:
1 |
|
可以参考之前的示例 2,在 cipher 函数中,我们通过对 EVP_CipherInit_ex2 的 enc 参数传入不同的值来控制程序的逻辑是加密还是解密,而且对于加解密所需要的密钥和初始化向量(如果算法模式是 ECB 模式则为 NULL)也是在调用该接口传入。
默认情况下,Openssl 会对原文的数据进行 PKCS7 填充以满足加密算法的应用需求。大多数的分组加密算法的块长度都是 8 字节或者是 16 字节,在加密数据之前,如果原文长度不是分组长度的整数倍,那么就需要用当前缺失的字节数来作为填充内容,填充至原文末尾,直至原文的长度为加密块长度的整数倍;如果当前原文长度恰好为加密块的整数倍,那么仍然需要在原文后添加一个块长度的填充数据,以该块长度作为填充内容。为了演示填充操作,我们将示例 2 的程序稍加修改,如下示例 3 所示:
1 |
|
运算结果为:
1 |
|
我们在这里使用了名为 EVP_CIPHER_CTX_set_padding 的函数,这个函数的作用是启用和禁用填充。在调用加密原文数据之后,我们禁用填充,那么解密的时候就会将填充数据打印出来(默认情况下解密时会反填充恢复数据,但是禁用填充功能后则不做任何操作直接输出解密数据),我们可以看到填充长度等于 SM4加密块长度,均为 16 字节,填充内容也是 0x10。
以上是加密原文恰好是 48 字节的情况,然后我们将 data 的内容改为 “12345678901234567890abcdefgABCDEFGIOPBNM12356789”,包含‘\0’ 的情况下恰好 49 个字节,然后运行程序,结果如下:
1 |
|
我们可以看到 Openssl 自动在原文数据的末尾填充了 15 个 0x0F,恰好是原文长度和分组长度整数倍(4 倍 64 字节)的差值。
以上是我们对常用的分组密码算法 API 的讨论,在实际生产环境中,应该根据自己的项目的需求选择不同的分组密码模式,对我来说一般在加密较小的数据或者密钥时直接采用 ECB 模式,简单快捷,加密大一点的文件或者数据时采用 CBC 模式。CFB、OFB和 CTR 等模式笔者基本没有接触或者使用过,这里不做讨论。
以上就是本章的内容,下一章我们将讨论摘要算法。