• openssl3.0 加密算法库编程精要 03 详解 EVP API 对称加密


      3.1 简介

      Openssl 提供的 EVP API 提供了高层级、相对抽象的密码加密接口,是我们最常用的 Openssl 模块之一。它提供了丰富的

    功能和特性:

      (1)一系列抽象的密码接口,用户无需考虑底层复杂的实现,使用简单;

      (2)支持对称加密和解密操作,并且支持使用对称算法的各种常用的模式;

      (3)支持消息摘要算法;

      (4)支持非对称加解密和签名验签;

      (5)支持密钥派生;

      (6)支持生成消息认证码。

      3.2 EVP API 的调用规律

      我们从上一章节的内容可以知道,调用高层次的 EVP API 基本都遵循固定的规律:

      (1)获取算法实现;

      (2)创建上下文 CTX;

      (3)调用 EVP_***Init;

      (4)调用 EVP_***Update;

      (5)调用 EVP_***Final;

      (6)销毁上下文,清理资源。

      虽然表面看起来确实如此,但是对于很多大部分公开密钥算法而言,由于这些算法的数学原理大相径庭,所以将它们抽象成

    类似对称算法和摘要算法那样形式的接口非常的困难,所以以上规律在我们使用非对称公开密钥算法时并不适用,这里各位读者

    需要注意,以后有机会我会对公开密钥算法的调用做专门的说明。

      3.3 对称加密系列接口

      在 EVP API 中,和对称算法相关的接口的前缀都是 EVP_CIPHER,我从这里面挑出我认为比较常用的几个接口来说明一下。

      首先是获取算法接口:

    #include <openssl/evp.h>
    
    /**
     * 获取算法
     * 
     * ctx[in] -- Openssl 库上下文
     * algorithm[in] -- 算法名称
     * properties[in] -- 属性查询字符串
     *
     * 成功找到算法,返回算法实现,否则返回 NULL
     */
    EVP_CIPHER *EVP_CIPHER_fetch(OSSL_LIB_CTX *ctx, const char *algorithm, const char *properties);
    
    /**
     * 销毁算法
     * 
     * cipher[in] -- 算法实现
     */
    void EVP_CIPHER_free(EVP_CIPHER *cipher);
    

      调用 EVP_CIPHER_fetch 接口第一个参数可以传 NULL,会默认采用全局库上下文。

      使用这个接口获取算法在 Openssl 内部被称为“显式获取(Explicit fetching)”,不同于旧版本的直接指定算法实现的方式,新版

    本在调用 EVP_CIPHER_fetch 获取算法后,需要调用 EVP_CIPHER_free 接口来释放 EVP_CIPHER 对象。

      调用 EVP_CIPHER_fetch 接口获取到算法实现后,我们可以通过该实现获取一系列的对称算法信息,比如密钥长度、初始向量

    长度、分组长度等,接口说明如下:

    #include <openssl/evp.h>
    
    /**
     * 获取分组长度
     * 
     * cipher[in] -- 算法实现
     * 返回分组长度
     */
    int EVP_CIPHER_get_block_size(const EVP_CIPHER *cipher);
    
    /**
     * 获取密钥长度
     * 
     * cipher[in] -- 算法实现
     * 返回密钥长度
     */
    int EVP_CIPHER_get_key_length(const EVP_CIPHER *cipher);
    
    /**
     * 获取初始化向量长度
     * 
     * cipher[in] -- 算法实现
     * 返回初始化向量长度
     */
    int EVP_CIPHER_get_iv_length(const EVP_CIPHER *cipher);
    

      具体的调用示例如下 -- 示例1:

    #include <openssl/evp.h>
    
    #include <trace/trace.h>
    
    int main(int argc, char *argv[])
    {
        int klen = 0;
        int ilen = 0;
        int blen = 0;
    
        EVP_CIPHER *sm4 = EVP_CIPHER_fetch(NULL, "SM4-CBC", NULL);
    
        if (!sm4) {
            return 0;
        }
    
        klen = EVP_CIPHER_get_key_length(sm4);
        ilen = EVP_CIPHER_get_iv_length(sm4);
        blen = EVP_CIPHER_get_block_size(sm4);
    
        TRACE("key len = %d, iv len = %d, block len = %d\n", klen, ilen, blen);
    
        EVP_CIPHER_free(sm4);
        return 0;
    }
    

      结果如下:

    key len = 16, iv len = 16, block len = 16
    

      可以看到我们正确获取到了 SM4 算法分组链接模式的算法信息。

      在使用获取到的算法实现进行运算之前,我们需要获取到加密上下文 -- cipher context,对应 Openssl 中则是 EVP_CIPHER_CTX

    对象,创建和销毁上下文的接口说明如下:

    #include <openssl/evp.h>
    
    /**
     * 创建加密上下文
     * 
     * 返回加密上下文对象
     */
    EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
    
    /**
     * 销毁加密上下文
     * 
     * ctx[in] -- 加密上下文对象
     */
    void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx);
    

      加密上下文相当于执行加密运算时的环境,里面维护了加解密运算时需要的临时数据,在执行加密运算初始化前,必须首先创建

    加密上下文,我们还是以 SM4-CBC 算法为例,先看一下完整的示例程序 -- 示例 2:

    #include <openssl/evp.h>
     
    #include <trace/trace.h>
     
    /* 加密和解密标记 */
    #define ENCRYPT 1
    #define DECRYPT 0
     
    /* 定义缓冲区长度 */
    #define DATA_BUF_LEN 256
     
    /* 全局缓冲区 */
    static unsigned char DATA_BUF[DATA_BUF_LEN] = { 0 };
     
    /* 定义二进制数据块 */
    struct BIN_DATA {
        unsigned char *data; /* 数据首地址 */
        int len;             /* 数据长度(字节) */
    };
     
    /* 加密或解密 */
    int cipher(
        EVP_CIPHER_CTX *ctx, EVP_CIPHER *cipher, const struct BIN_DATA *in, struct BIN_DATA *out, int enc)
    {
        unsigned char key[] = {
            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
            0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00
        };
     
        unsigned char iv[] = {
            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
            0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF
        };
     
        int padlen = 0;
     
        if (!ctx || !cipher || !in || !out) {
            return 0;
        }
     
        if (EVP_CipherInit_ex2(ctx, cipher, key, iv, enc, NULL) != 1) {
            return 0;
        }
     
        if (EVP_CipherUpdate(ctx, out->data, &out->len, in->data, in->len) != 1) {
            return 0;
        }
     
        if (EVP_CipherFinal_ex(ctx, out->data + out->len, &padlen) != 1) {
            return 0;
        }
     
        /* 计算最终长度 */
        out->len += padlen;
        return 1;
    }
     
    int main(int argc, char *argv[])
    {
        char data[] = "12345678901234567890abcdefgABCDEFGIOPBNM1235678";
        int len = sizeof(data);
     
        /* 原文数据 */
        struct BIN_DATA in = {
            (unsigned char *)data,
            len
        };
     
        /* 加密缓冲区 */
        struct BIN_DATA enc = {
            DATA_BUF,
            DATA_BUF_LEN / 2
        };
     
        /* 解密缓冲区 */
        struct BIN_DATA dec = {
            DATA_BUF + DATA_BUF_LEN / 2,
            DATA_BUF_LEN / 2
        };
     
        /* 加密上下文 */
        EVP_CIPHER_CTX *ctx = NULL;
     
        /* 获取算法 */
        EVP_CIPHER *sm4 = EVP_CIPHER_fetch(NULL, "SM4-CBC", NULL);
     
        if (!sm4) {
            return 0;
        }
     
        /* 创建加密上下文 */
        ctx = EVP_CIPHER_CTX_new();
        if (!ctx) {
            goto end;
        }
     
        /* 加密 */
        if (cipher(ctx, sm4, &in, &enc, ENCRYPT) != 1) {
            TRACE("加密失败!\n");
            goto end;
        }
     
        /* 重置上下文 */
        if (EVP_CIPHER_CTX_reset(ctx) != 1) {
            TRACE("上下文重置失败!\n");
            goto end;
        }
     
        /* 解密 */
        if (cipher(ctx, sm4, &enc, &dec, DECRYPT) != 1) {
            TRACE("解密失败!\n");
            goto end;
        }
     
        /* 打印信息 */
     
        /* 打印原文数据 */
        TRACE_BIN("原文数据", in.data, in.len);
         
        /* 打印加密数据 */
        TRACE_BIN("加密数据", enc.data, enc.len);
     
        /* 打印解密数据 */
        TRACE_BIN("解密数据", dec.data, dec.len);
    
    end:
        if (ctx) {
            EVP_CIPHER_CTX_free(ctx);
        }
    
        if (sm4) {
            EVP_CIPHER_free(sm4);
        }
    
        return 0;
    }

      这里我们单独定义了一个名为 cipher 的函数,cipher 函数中首先定义了密钥 key 和初始化向量 iv,这里我提供几个生成 key 和 iv

    方法:

      (1)拍脑门随机想两个对应长度的值;

      (2)直接调用随机数生成器 RAND_bytes 生成两个对应的长度的值;

      (3)创建 EVP_CIPHER_CTX 后,调用 EVP_CIPHER_CTX_rand_key 接口生成随机密钥, iv 可以采用(1)或者(2)的方式

        生成。

      我们采用第一种方法… 然后分别调用了 EVP_CipherInit_ex2、EVP_CipherUpdate 和 EVP_CipherFinal_ex 三个接口来完成对称

    加密解密操作,这三个接口定义如下:

    #include <openssl/evp.h>
    
    /**
     * 分组加解密初始化
     * 
     * ctx[in] -- 加密上下文
     * type[in] -- 算法实现
     * key[in] -- 对称密钥
     * iv[in] -- 初始化向量
     * enc[in] -- 加密或者解密标志,传 1 为加密,传 0 为解密
     * params[in] -- 扩展参数
     *
     * 调用成功,返回 1,否则返回 0
     */
    int EVP_CipherInit_ex2(
        EVP_CIPHER_CTX *ctx,
        const EVP_CIPHER *type,
        const unsigned char *key,
        const unsigned char *iv,
        int enc,
        const OSSL_PARAM params[]
    );
    
    /**
     * 分组加解密
     * 
     * ctx[in] -- 加密上下文
     * out[out] -- 存放加密或者解密结果的内存首地址
     * outl[out] -- 结果长度
     * in[in] -- 存放原文数据的内存首地址
     * inl[in] -- 原文数据长度
     *
     * 调用成功,返回 1,否则返回 0
     */
    int EVP_CipherUpdate(
        EVP_CIPHER_CTX *ctx,
        unsigned char *out,
        int *outl,
        const unsigned char *in,
        int inl
    );
    
    /**
     * 加解密收尾
     * 
     * ctx[in] -- 加密上下文对象
     * outm[out] -- 存放剩余加密或者解密结果的内存首地址
     * outl[out] -- 收尾时处理的数据长度
     *
     * 调用成功,返回 1,否则返回 0
     */
    int EVP_CipherFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);
    

      可以参考之前的示例 2,在 cipher 函数中,我们通过对 EVP_CipherInit_ex2 的 enc 参数传入不同的值来控制程序的逻辑是加密还

    是解密,而且对于加解密所需要的密钥和初始化向量(如果算法模式是 ECB 模式则为 NULL)也是在调用该接口传入。

      默认情况下,Openssl 会对原文的数据进行 PKCS7 填充以满足加密算法的应用需求。大多数的分组加密算法的块长度都是 8 字节

    或者是 16 字节,在加密数据之前,如果原文长度不是分组长度的整数倍,那么就需要用当前缺失的字节数来作为填充内容,填充至原

    文末尾,直至原文的长度为加密块长度的整数倍;如果当前原文长度恰好为加密块的整数倍,那么仍然需要在原文后添加一个块长度的

    填充数据,以该块长度作为填充内容。为了演示填充操作,我们将示例 2 的程序稍加修改,如下所示 -- 示例 3:

    ……(同示例2)
    
    int main(int argc, char *argv[])
    {
        char data[] = "12345678901234567890abcdefgABCDEFGIOPBNM1235678";
        int len = sizeof(data);
    
        /* 原文数据 */
        struct BIN_DATA in = {
            (unsigned char *)data,
            len
        };
    
        /* 加密缓冲区 */
        struct BIN_DATA enc = {
            DATA_BUF,
            DATA_BUF_LEN / 2
        };
    
        /* 解密缓冲区 */
        struct BIN_DATA dec = {
            DATA_BUF + DATA_BUF_LEN / 2,
            DATA_BUF_LEN / 2
        };
    
        /* 加密上下文 */
        EVP_CIPHER_CTX *ctx = NULL;
    
        /* 获取算法 */
        EVP_CIPHER *sm4 = EVP_CIPHER_fetch(NULL, "SM4-CBC", NULL);
    
        if (!sm4) {
            return 0;
        }
    
        /* 创建加密上下文 */
        ctx = EVP_CIPHER_CTX_new();
        if (!ctx) {
            goto end;
        }
    
        /* 加密 */
        if (cipher(ctx, sm4, &in, &enc, ENCRYPT) != 1) {
            TRACE("加密失败!\n");
            goto end;
        }
    
        /* 重置上下文 */
        if (EVP_CIPHER_CTX_reset(ctx) != 1) {
            TRACE("上下文重置失败!\n");
            goto end;
        }
    
        /* 取消填充 */
        EVP_CIPHER_CTX_set_padding(ctx, 0);
    
        /* 解密 */
        if (cipher(ctx, sm4, &enc, &dec, DECRYPT) != 1) {
            TRACE("解密失败!\n");
            goto end;
        }
    
        ……(以下内容同示例 2)
    }
    

      运算结果为:

    原文数据 size:48
    ------------------------+------------------------
    31 32 33 34 35 36 37 38 | 39 30 31 32 33 34 35 36
    37 38 39 30 61 62 63 64 | 65 66 67 41 42 43 44 45
    46 47 49 4f 50 42 4e 4d | 31 32 33 35 36 37 38 00
    ------------------------+------------------------
    加密数据 size:64
    ------------------------+------------------------
    7a 7b 69 88 23 12 70 e7 | f0 d9 48 d9 dc 94 68 cf
    01 19 d2 d4 92 3f f6 2f | dc 30 a6 b5 d7 b7 f2 47
    87 eb 7b 08 a1 4d c2 f1 | d2 f5 28 c3 d2 2a e1 44
    77 16 70 d5 3c ce d6 7f | 20 fb 78 45 b4 ea f8 78
    ------------------------+------------------------
    解密数据 size:64
    ------------------------+------------------------
    31 32 33 34 35 36 37 38 | 39 30 31 32 33 34 35 36
    37 38 39 30 61 62 63 64 | 65 66 67 41 42 43 44 45
    46 47 49 4f 50 42 4e 4d | 31 32 33 35 36 37 38 00
    10 10 10 10 10 10 10 10 | 10 10 10 10 10 10 10 10
    ------------------------+------------------------
    

      我们在这里使用了名为 EVP_CIPHER_CTX_set_padding 的函数,这个函数的作用是启用和禁用填充。在调用加密原文数据之后,

    我们禁用填充,那么解密的时候就会将填充数据打印出来(默认情况下解密时会反填充恢复数据,但是禁用填充功能后则不做任何操作

    直接输出解密数据),我们可以看到填充长度等于 SM4加密块长度,均为 16 字节,填充内容也是 0x10。

      以上是加密原文恰好是 48 字节的情况,然后我们将 data 的内容改为 "12345678901234567890abcdefgABCDEFGIOPBNM12356

    789",包含‘\0’ 的情况下恰好49 个字节,然后运行程序,结果如下:

    原文数据 size:49
    ------------------------+------------------------
    31 32 33 34 35 36 37 38 | 39 30 31 32 33 34 35 36
    37 38 39 30 61 62 63 64 | 65 66 67 41 42 43 44 45
    46 47 49 4f 50 42 4e 4d | 31 32 33 35 36 37 38 39
    00 
    ------------------------+------------------------
    加密数据 size:64
    ------------------------+------------------------
    7a 7b 69 88 23 12 70 e7 | f0 d9 48 d9 dc 94 68 cf
    01 19 d2 d4 92 3f f6 2f | dc 30 a6 b5 d7 b7 f2 47
    1e eb b0 4c 16 6a 1f 7b | ca fc dd e8 ed f5 8d 07
    52 a9 42 5b de 37 72 70 | d9 57 64 5d 26 15 0e 95
    ------------------------+------------------------
    解密数据 size:64
    ------------------------+------------------------
    31 32 33 34 35 36 37 38 | 39 30 31 32 33 34 35 36
    37 38 39 30 61 62 63 64 | 65 66 67 41 42 43 44 45
    46 47 49 4f 50 42 4e 4d | 31 32 33 35 36 37 38 39
    00 0f 0f 0f 0f 0f 0f 0f | 0f 0f 0f 0f 0f 0f 0f 0f
    ------------------------+------------------------
    

      我们可以看到 Openssl 自动在原文数据的末尾填充了 15 个 0x0F,恰好是原文长度和分组长度整数倍(4倍 64 字节)的差值。

      以上是我们对常用的分组密码算法 API 的讨论,在实际生产环境中,应该根据自己的项目的需求选择不同的分组密码模式,对

    我来说一般在加密较小的数据或者密钥时直接采用ECB 模式,简单快捷,加密大一点的文件或者数据时采用 CBC 模式。CFB、OFB

    和 CTR 等模式笔者基本没有接触或者使用过,这里不做讨论。

      以上就是本章的内容,下一章我们将讨论摘要算法。

      参考资料:

        1.Openssl 官方文档 -- https://www.openssl.org/docs/

        2.Openssl Wiki -- https://wiki.openssl.org/index.php/Main_Page

        3.《密码学原理与实践(第三版)》,Douglas R.Stinson 冯登国等译

        4.《GB/T 32907-2016 信息安全技术 SM4 分组密码算法》

  • 相关阅读:
    MySQL 函数
    ARC072_F Dam
    1373D
    科目二和科目三找准30厘米位置的点位
    MySQL 数字保留两位小数
    IntelliJ IDEA 中,项目文件右键菜单没有svn选项解决办法
    MySQL SQL语句书写顺序和执行顺序
    科目三道路驾驶技能考试之百米加减挡操作
    上海科目三道路驾驶技能考试夜间灯光模拟操作
    上海 科目三大路考试攻略
  • 原文地址:https://www.cnblogs.com/huowenjie/p/15770318.html
Copyright © 2020-2023  润新知