基于OpenSSL实现AES-CBC 128算法的 Seed&Key DLL的生成与使用

发布于:2025-06-21 ⋅ 阅读:(21) ⋅ 点赞:(0)


📙 1 基础知识储备

🍅1.1 OpenSSL库下载安装

OpenSSL 是一个开源的软件库,主要用于实现安全通信协议和加密算法。提供广泛的加密算法实现,包括对称加密(如 AES)、非对称加密(如 RSA)、哈希函数(如 SHA)等,支持 SSL/TLS 协议,用于实现安全的网络通信,提供证书管理功能,支持 X.509 证书的生成、验证和管理。主要组件:

  • libcrypto:加密算法库,提供各种加密、解密、签名、验证等基础功能
  • libssl:SSL/TLS 协议实现库,用于安全通信
  • openssl 命令行工具:用于执行各种加密操作、证书管理等任务

OpenSSL下载链接:https://slproweb.com/products/Win32OpenSSL.html

  • 用户可以根据实际需要选择64或者32位版本进行安装,在Visual studio中编译的时候也要选择对应的版本来进行编译,
    同时也需与CANoe的RT Kernel的版本(32bit/64bit)保持一致。
  • 本文档使用64位的OpenSSL v3.4.1为例进行介绍,不同版本都会有相应的API改变,可能导致本文示例代码报错等。
    在这里插入图片描述
  • 我这里64/32位的都下载下来了,然后点击Win64OpenSSL-3_4_1.exe就可以安装了,根据需要把安装路径改下,其它直接点下一步即可。
    在这里插入图片描述
  • 安装完成后,记住这个安装路径,其中includelib文件夹待会要用。
    在这里插入图片描述
  • 安装完成后,在CMD中输入openssl,如下图就说明安装成功了。
    在这里插入图片描述

🍅1. 2 AES 加密算法

1.2.1、主要特点

  • 称密钥算法:AES 是一种对称密钥算法,这意味着加密和解密使用相同的密钥。这种特性使得加密和解密的操作相对简单且高效,在密钥管理妥善的情况下,能确保数据的保密性。

  • 分组密码:AES 对固定大小的数据块进行操作,是分组密码类型。其标准块大小为 128 位,这种以固定块大小处理数据的方式有助于保证加密过程的规范性和安全性。

  • 密钥长度:AES 支持 128、192 和 256 位的密钥长度。密钥长度是决定加密强度的重要因素,一般来说,密钥越长,加密越强。较长的密钥意味着更多的密钥组合可能性,从而增加了破解的难度。

  • 安全性:AES 被广泛认为是非常安全的加密算法,并在各种安全协议和应用程序中有着广泛应用。其安全性经过了长期的实践检验和理论分析,能够有效抵御多种攻击手段,保护数据安全。

1.2.2、AES 加密支持的不同模式

  • ECB(Electronic Code Book)模式:这是最简单的加密方式,加密时不需要 IV(偏移量)。输入的明文会被分成块,每个块用提供的密钥进行加密,其特点是相同的明文块被加密成相同的密文块。
  • CBC(密码块链接)模式:强烈推荐使用此模式,它是分组密码加密的一种高级形式。它需要 IV 来使每条消息都具有唯一性,这意味着相同的纯文本块被加密为不同的密码文本块。因此,与 ECB 模式相比,它提供了更强大的加密,但速度相对稍慢。若没有输入 IV,则此处将使用 CBC 模式的默认值,并且默认为从零开始的字节[16]。
  • CTR(计数器)模式:CTR 模式(CM)也称为整数计数器模式(ICM)和分段整数计数器模式(SIC)。计数器模式将分组密码转换为流密码。CTR 模式具有与 OFB 类似的特性,但也允许在解密过程中具有随机访问属性。CTR 模式非常适合在多处理器机器上运行,其中可以并行加密块。
  • GCM(Galois/Counter 模式):GCM 是一种对称密钥分组密码操作模式,使用通用哈希来提供经过认证的加密。GCM 被认为比 CBC 模式更安全,因为它具有内置的身份验证和完整性检查,并且因其性能优势而得到广泛使用。

1.2.3、填充模式

  1. PKCS#5 填充(PKCS#5 Padding)
    PKCS#5 是为 8 字节块大小设计的填充方案。不过在实际应用中,当 AES 块大小为 16 字节时,通常使用的是 PKCS#7 填充。其填充规则为:在数据末尾添加字节,每个填充字节的值等于需要填充的字节数。例如,若数据长度比块大小少 3 字节,就添加 3 个值为 0x03 的字节;若数据长度恰好是块大小的整数倍,会添加一个完整块,块内所有字节值为块大小数值(AES 中为 0x10)。

  2. PKCS#7 填充(PKCS#7 Padding)
    PKCS#7 是 PKCS#5 的扩展,支持任意块大小。在 AES 加密中,PKCS#7 填充和 PKCS#5 填充在行为上一致。它是最常用的填充模式之一,能确保数据长度为块大小的整数倍。

  3. ISO 10126 填充(ISO 10126 Padding)
    此填充模式在数据末尾添加填充字节,除最后一个字节外,其他填充字节填充随机值,最后一个字节的值为需要填充的字节数。例如,若需填充 5 个字节,前 4 个字节是随机值,最后一个字节为 0x05。

  4. Zero Padding
    这种填充模式在数据末尾填充 0 字节,直到数据长度达到块大小的整数倍。不过它存在一个问题,若原始数据末尾本身就有 0 字节,解密时难以区分哪些 0 字节是原始数据,哪些是填充的,所以使用时需要额外处理。

  5. No Padding
    该模式要求输入数据长度必须是块大小的整数倍,若不满足则会报错。在实际应用中,需要提前确保数据长度符合要求,使用场景相对受限。

1.2.4、AES 密钥大小

  • IV 即初始化向量(Initialization Vector),是一个固定长度的随机值,无论是 AES 256、AES 192 还是 AES 128,其在使用需要初始化向量(IV)的加密模式(如 CBC、CFB、OFB 等)时,IV 的字节大小都是 128 比特(即 16 字节)
  • 原因
    这是因为IV 的长度取决于加密算法的块大小,而不是密钥长度。AES 是一种分组加密算法,其块大小固定为 128 比特,与选择使用 128 比特、192 比特还是 256 比特的密钥无关。因此,在使用需要 IV 的加密模式时,IV 的长度必须和块大小一致,也就是 128 比特。
  • 作用
    IV 的主要作用是增加加密的随机性和安全性。在像 CBC 这样的模式中,每个明文块在加密前会与前一个密文块进行异或操作,第一个明文块则与 IV 异或。使用不同的 IV 可以使相同的明文在相同密钥下产生不同的密文,防止攻击者通过分析密文模式来破解加密信息。

🍅1.3 在Visual studio 环境中配置OpenSSL

  • 博主这里使用的是visual studio 2022 社区版

1.3.1 新建 visual studio 控制台应用

  • 这里我们先创建一个 控制台应用 测试下OpenSSL的应用,等调试通过后,再创建动态库工程

在这里插入图片描述
在这里插入图片描述

1.3.2 配置OpenSSL环境

  • 选择工程>>属性
    在这里插入图片描述

  • 将OpenSSL的includelib目录添加到 VC++目录下的 外部包含目录库目录
    在这里插入图片描述

  • 连接器>>输入>>附加依赖项 选项中填入 libcrypto.lib;libssl.lib;
    在这里插入图片描述

  • 在默认文件的头部引入 #include <openssl/evp.h>下图配错),并且执行程序,不报错,就说明我们OpenSSL库的环境配置成功了。
    在这里插入图片描述

📙 2、AES CBC 算法测试

  • 不同的厂商选择的加密算法可能都不一样,但是只要是OpenSSL的支持算法,都可以参考下面代码示例仿写。
  • 下面是 AES CBC 加密解密的示例**,用于 Seed&Key DLL的AES算法一般要求加密的数据都是16的倍数,如果您的密钥不是16的整数倍,请根据需求填充至16的整数倍**,核心参数是IV和Key参数值。
#include <iostream>
#include <openssl/evp.h>
#include <cstring>
#include <windows.h>


// AES CBC 加密
void aes_cbc_encrypt(const unsigned char plaintext[], size_t length,
    unsigned char ciphertext[],
    const unsigned char key[],
    const unsigned char iv[]) {
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    int len;
    int ciphertext_len;

    // 初始化加密操作,设置无填充模式
    EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);
    EVP_CIPHER_CTX_set_padding(ctx, 0);  // 禁用填充

    // 加密
    EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, length);
    ciphertext_len = len;

    // 完成加密
    EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);
    ciphertext_len += len;

    EVP_CIPHER_CTX_free(ctx);
}

// AES CBC 解密
void aes_cbc_decrypt(const unsigned char ciphertext[], size_t length,
    unsigned char plaintext[],
    const unsigned char key[],
    const unsigned char iv[]) 
{
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    int len;
    int plaintext_len;

    // 初始化解密操作,设置无填充模式
    EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);
    EVP_CIPHER_CTX_set_padding(ctx, 0);  // 禁用填充

    // 解密
    EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, length);
    plaintext_len = len;

    // 完成解密
    EVP_DecryptFinal_ex(ctx, plaintext + len, &len);
    plaintext_len += len;

    EVP_CIPHER_CTX_free(ctx);
}


int main() 
{
    // 示例密钥和IV(必须是16字节)
    unsigned char key[16] ={ 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x10,0x21,0x31,0x41,0x51,0x61,0x71 };
    unsigned char iv[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

    // 示例明文
    const unsigned char plaintext[64] = { 0xF6, 0xFE, 0xEB, 0x57, 0xFB, 0xAD, 0xBA, 0xE6, 0x67, 0xB9, 0x9C, 0x2E, 0xDF, 0xF5, 0xE2, 0x42,
                                          0x5F, 0x9B, 0x88, 0x71, 0xF5, 0x77, 0xC9, 0xD3, 0xD1, 0x5E, 0xDB, 0x77, 0xDE, 0xE9, 0xAD, 0x97, 
                                          0x7F, 0xE7, 0x91, 0x59, 0xB0, 0xEF, 0x2B, 0x4F, 0x7B, 0x6B, 0x2B, 0x6A, 0xF5, 0xFF, 0xB3, 0xD6, 
                                          0xF8, 0x9F, 0xBF, 0x85, 0x6F, 0xF3, 0xA1, 0xFD, 0xED, 0xB7, 0x3B, 0xF9, 0x71, 0xEF, 0xAA, 0x59 };
    size_t length = 64;
    char unsigned ciphertext[64] ;
    char unsigned decryptedText[64] ;

    std::cout << "原始加密数据: ";
    for (size_t i = 0; i < length; ++i) {
        printf("%02X ", plaintext[i]);
    }
    std::cout << "\n";

    // 加密
    aes_cbc_encrypt(plaintext, length,ciphertext, key, iv);

    // 打印加密后的数据
    std::cout << "加密后的数据: ";
    for (size_t i = 0; i < length; ++i) {
        printf("%02X ", ciphertext[i]);
    }
    std::cout << "\n";

    // 解密
    aes_cbc_decrypt(ciphertext, length,decryptedText, key, iv);

    std::cout << "解密后的数据: ";
    for (size_t i = 0; i < length; ++i) {
        printf("%02X ", decryptedText[i]);
    }
    std::cout << "\n";

    return 0;
}
  • 测试结果 如图,可以正常的加密和解密。

在这里插入图片描述

📙 3、生成 Seed&Key DLL

在如下路径下找到Vector官方自带的创建Seed&Key DLL的示例代码

C:\Users\Public\Documents\Vector\CANoe\Sample Configurations 16.4.4\CAN\Diagnostics\UDSSystem\SecurityAccess\Sources\KeyGenDll_GenerateKeyEx
  • 第一步,我们把如下两个文件拷贝到AES_CBC文件夹下。

在这里插入图片描述

  • 第二步,把GenerateKeyExImpl.cpp文件中代码拷贝到AES_CBC.cpp文件中,引入KeyGenAlgoInterfaceEx.h头文件,并根据您的加密算法完善GenerateKeyEx函数。

  • 第三步,把属性的配置类型改为 动态库(.dll)

在这里插入图片描述

  • 第四步,点击 生成>>生成解决方案 ,即可生成 Seed&Key DLL。

在这里插入图片描述

📙 4、CANoe中配置Seed&Key DLL


切换CANoe RT kernel

  • CANoe版本低于13,建议使用32bit的库进行开发。
  • CANoe版本高于13,切换RT Kernel 32bit 和64 bit 路径如下

在这里插入图片描述

  • 在诊断控制台中加载刚才生成的dll即可使用。

在这里插入图片描述

  • 在诊断控制台中可以实现AES解锁

在这里插入图片描述

  • 用户也可以在CAPL脚本中也可通过内置函数DiagGenerateKeyFromSeed 实现计算AES,当用户调用DiagGenerateKeyFromSeed函数时,CANoe内部机制会去调用诊断控制台中加载的AES_CBC.dll中的GenerateKeyEx接口,从而实现加密计算。
long Secuiry_Cal_Key(byte gSecurityLevel,byte seed[],byte seedkey[],byte size)  
{

    long i, resSize,ret; 
    char gVariant[200]    = "Variant1";
    char gOption[200]     = "option";
    byte gKeyArray[255];
    int  gMaxKeyArraySize = 255;
    dword gActualSize     = 0;
      //计算 Key      
      ret =  DiagGenerateKeyFromSeed (seed, size , gSecurityLevel, gVariant, gOption, seedkey, gMaxKeyArraySize, gActualSize);   //generate key                     
      return ret; 
}

📙 5、不使用诊断控制台计算AES

  • 上面生成的SeedKey.dll只适用于在CANoe软件中加载了PDX/CDD等诊断数据库的情况下使用。且该dll在CAPL文件中无法通过#pragma library("xxxx.dll")语法加载。如果用户不使用CANoe的诊断控制台,只想通过CAPL实现AES的加密算法,则需要通过 C:\Users\Public\Documents\Vector\CANoe\Sample Configurations 16.7.5\Programming\CAPLdll 路径下示例工程的语法方式创建DLL。

  • 如下面CAPL代码示例,通过visual studio 封装的一个函数aes_cbc_encrypt用于计算seed生成Key,该函数的函数参数解释如下:

    • iSeedArray : 用于计算的seed数组
    • Length:seed数组长度
    • ioKeyArray:返回的密钥数组
    • Key : AES算法加密密钥
    • IV:AES算法初始向量数组
    • error_info:函数报错信息
    • error_info_size:函数报错信息字符串长度
    • 返回值:返回的密钥数组长度
/*@!Encoding:65001*/
includes
{
  #if X64
    #pragma library("X64\AES_CBC_X64.dll")
  #else
    #pragma library("X86\AES_CBC_X86.dll")
  #endif
}


on key '1'
{
    byte _key[16] = { 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x10,0x21,0x31,0x41,0x51,0x61,0x71 };
    byte _iv[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    
   byte Seed[64] =  { 0xF6, 0xFE, 0xEB, 0x57, 0xFB, 0xAD, 0xBA, 0xE6, 0x67, 0xB9, 0x9C, 0x2E, 0xDF, 0xF5, 0xE2, 0x42,
                      0x5F, 0x9B, 0x88, 0x71, 0xF5, 0x77, 0xC9, 0xD3, 0xD1, 0x5E, 0xDB, 0x77, 0xDE, 0xE9, 0xAD, 0x97, 
                      0x7F, 0xE7, 0x91, 0x59, 0xB0, 0xEF, 0x2B, 0x4F, 0x7B, 0x6B, 0x2B, 0x6A, 0xF5, 0xFF, 0xB3, 0xD6, 
                      0xF8, 0x9F, 0xBF, 0x85, 0x6F, 0xF3, 0xA1, 0xFD, 0xED, 0xB7, 0x3B, 0xF9, 0x71, 0xEF, 0xAA, 0x59 };

    byte outKey[64];
    char err_info[0x256];
    long outKey_Size,i;
    
    outKey_Size = aes_cbc_encrypt(Seed,elcount(Seed),outKey,_key,_iv,err_info,elcount(err_info));
    
    write("retVal = %d",outKey_Size);
    
    if(outKey_Size!=-1)
    {
      for(i = 0;i<outKey_Size;i++)
        write("outKey[%d] = 0x%X",i,outKey[i]);
    }
}

  • 测试结果如下:

在这里插入图片描述

📙 6、CAPL内置AES函数

  • CANoe软件内置了一个SecMgrCANoeClient.dll文件,该文件封装了一些互联网常用的加密算法,如RSA/AES/CMAC,以及X509证书相关等,该文件的目录如下(以CANoe 16 安装目录为例):
    在这里插入图片描述

  • 该DLL内,封装了很多的函数,本文用到了AES-128-CBC,就以这两个函数为例,讲解下如何使用
    在这里插入图片描述

  • 在使用的Test/simulation 节点中加载SecMgrCANoeClient.dll文件
    在这里插入图片描述

  • 在使用的Test/simulation 节点的CAPL文件的测试代码如下:
    Note :
    1、如果运行脚本导致软件崩溃的话,在 includes代码中再引用一遍SecMgrCANoeClient.dll函数
    2、(dword outKey_Size = elcount(outKey) + 16 )为什么这段代码要加 16 ,参考下文的帮助文档

/*@!Encoding:65001*/
includes
{ 
//  #pragma library("C:\ProgramData\Vector\Security Manager\General\Binaries\SecMgrCANoeClient\CANoe_13.0.117\Windows-x64\SecMgrCANoeClient.dll")
}

on key '1'
{
    byte _key[16] = { 0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x10,0x21,0x31,0x41,0x51,0x61,0x71 };
    byte _iv[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

    byte Seed[64] =  { 0xF6, 0xFE, 0xEB, 0x57, 0xFB, 0xAD, 0xBA, 0xE6, 0x67, 0xB9, 0x9C, 0x2E, 0xDF, 0xF5, 0xE2, 0x42,
                      0x5F, 0x9B, 0x88, 0x71, 0xF5, 0x77, 0xC9, 0xD3, 0xD1, 0x5E, 0xDB, 0x77, 0xDE, 0xE9, 0xAD, 0x97, 
                      0x7F, 0xE7, 0x91, 0x59, 0xB0, 0xEF, 0x2B, 0x4F, 0x7B, 0x6B, 0x2B, 0x6A, 0xF5, 0xFF, 0xB3, 0xD6, 
                      0xF8, 0x9F, 0xBF, 0x85, 0x6F, 0xF3, 0xA1, 0xFD, 0xED, 0xB7, 0x3B, 0xF9, 0x71, 0xEF, 0xAA, 0x59 };

    byte outKey[64];
    long Ret,i;
    dword outKey_Size = elcount(outKey) + 16 ;

    Ret = SecurityLocalEncryptAES128CBC(_key,elcount(_key),Seed,elcount(Seed),_iv,elcount(_iv),outKey,outKey_Size);

    write("outKey_Size = %d,Ret = %d",outKey_Size,Ret);

    if(Ret == 1)
    {
      for(i = 0;i<elcount(outKey);i++)
        write("outKey[%d] = 0x%X",i,outKey[i]);
    }
}
  • 测试结果如下:
    在这里插入图片描述
  • SecurityLocalEncryptAES128CBC函数的help文档解释如下:
    使用该函数需要注意:这个函数默认是采用PKCS5方式填充的。输入cipheredDataLength参数的长度必须比cipheredData数组大小 大16字节,否则函数返回值为0.
    在这里插入图片描述

📙 7、源码获取

23

7

  • 🚩要有最朴素的生活,最遥远的梦想,即使明天天寒地冻,路遥马亡!

  • 🚩如果这篇博客对你有帮助,请 “点赞” “评论”“收藏”一键三连 哦!码字不易,大家的支持就是我坚持下去的动力。
    18

网站公告

今日签到

点亮在社区的每一天
去签到