OpenSSL库中使用EVP(Enhanced Verification Package增强验证包)进行SM3相关的摘要生成。
其逻辑为:可分为 初始化上下文→配置算法→分块处理数据→生成最终哈希→释放资源 五个步骤。
完整代码如下:
cpp
#include <QCoreApplication>
#include <iostream>
#include <openssl/opensslv.h>
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/err.h>
using namespace std;
// 计算 SM3 哈希值
int sm3_hash(const unsigned char *data, size_t data_len, unsigned char *hash, unsigned int *hash_len) {
// 创建 EVP 上下文
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (ctx == NULL) {
fprintf(stderr, "无法创建 EVP 上下文\n");
return 0;
}
// 初始化 SM3 哈希计算
if (EVP_DigestInit_ex(ctx, EVP_sm3(), NULL) != 1) {
fprintf(stderr, "SM3 初始化失败\n");
ERR_print_errors_fp(stderr);
EVP_MD_CTX_free(ctx);
return 0;
}
// 更新哈希计算(可以分多次调用更新不同的数据块)
if (EVP_DigestUpdate(ctx, data, data_len) != 1) {
fprintf(stderr, "SM3 数据更新失败\n");
ERR_print_errors_fp(stderr);
EVP_MD_CTX_free(ctx);
return 0;
}
// 完成哈希计算并获取结果
if (EVP_DigestFinal_ex(ctx, hash, hash_len) != 1) {
fprintf(stderr, "SM3 计算完成失败\n");
ERR_print_errors_fp(stderr);
EVP_MD_CTX_free(ctx);
return 0;
}
// 释放上下文
EVP_MD_CTX_free(ctx);
return 1;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 待计算哈希的数据
const char *input = "Hello, SM3!";
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len;
// 初始化 OpenSSL 错误信息
ERR_load_crypto_strings();
// 计算 SM3 哈希
if (sm3_hash((const unsigned char *)input, strlen(input), hash, &hash_len)) {
printf("input data: %s\n", input);
printf("SM3 hash: ");
for (unsigned int i = 0; i < hash_len; i++) {
printf("%02x", hash[i]);
}
printf("\nhash length: %u byte\n", hash_len);
}
// 清理错误信息
ERR_free_strings();
return a.exec();
}
这里有一个需要注意的地方:
EVP_DigestUpdate 支持分多次调用,核心原因是为了处理大数据或流式数据:
- 对于大文件(如几个 GB 的视频)或网络流(如实时接收的数据),无法一次性将所有数据加载到内存中(会导致内存溢出)。
- 分多次调用时,每次只需传入一部分数据(例如每次 4KB),上下文 ctx 会保存中间状态,最终合并计算结果,与一次性处理的结果完全一致。
- 这种设计既节省内存,又支持流式处理(边接收数据边计算哈希),是处理大数据场景的必需特性。
那么问题来了
为什么需要分块更新哈希计算?
分块调用 EVP_DigestUpdate 与 "哈希长度是否固定" 无关,核心原因是哈希算法的底层设计原理和实际工程需求:
- 哈希算法的 "迭代式计算" 本质
SM3(以及几乎所有现代哈希算法,如 SHA256、MD5 等)的计算过程是迭代式的,而非 "一次性计算"。其底层逻辑是:
- 将输入数据按固定大小(SM3 中是 512 位,即 64 字节)分成若干块(最后一块不足时会进行补位处理)。
- 从初始状态开始,每处理一块数据,就用该块数据更新内部状态(一个 256 位的中间变量)。
- 所有块处理完成后,将最终的内部状态转换为 32 字节的哈希值。
例如:
- 输入 "abc" 时,数据量小于 512 位,补位后形成一个块,一次处理完成。
- 输入 1GB 文件时,数据会被分成约 200 万个 512 位块,必须分块处理,每块更新一次内部状态,最终合并为同一个 32 字节哈希值。
EVP_DigestUpdate 的作用就是向算法传递这些分块数据,让算法逐步更新内部状态,它是哈希算法迭代特性的直接体现。
- 工程上的 "内存限制" 需求
即使哈希算法支持一次性处理数据,实际工程中也必须分块:
- 对于大文件(如 10GB 的视频),无法将全部数据一次性加载到内存(会导致内存溢出)。
- 分块处理时,只需在内存中保留一个小块缓冲区(如 4KB),每次读取一块数据并传递给 EVP_DigestUpdate,内存占用始终保持在极低水平。
这种方式既符合哈希算法的迭代逻辑,又能高效处理任意大小的输入(从几字节到几十 GB)。
分块计算的例子。
cpp
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/err.h>
// 计算文件的 SM3 哈希值(分块处理大文件)
int sm3_file_hash(const char *file_path, unsigned char *hash, unsigned int *hash_len) {
// 打开文件(二进制模式,避免文本模式下的换行符转换)
FILE *file = fopen(file_path, "rb");
if (!file) {
fprintf(stderr, "无法打开文件: %s\n", file_path);
return 0;
}
// 创建 EVP 上下文
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) {
fprintf(stderr, "无法创建 EVP 上下文\n");
fclose(file);
return 0;
}
// 初始化 SM3 算法
if (EVP_DigestInit_ex(ctx, EVP_sm3(), NULL) != 1) {
fprintf(stderr, "SM3 初始化失败\n");
ERR_print_errors_fp(stderr);
EVP_MD_CTX_free(ctx);
fclose(file);
return 0;
}
// 分块读取文件并更新哈希(每次读取 4KB 块)
unsigned char buffer[4096]; // 缓冲区大小,可根据需求调整(如 8192、16384 等)
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
// 每次读取一块数据,更新哈希计算
if (EVP_DigestUpdate(ctx, buffer, bytes_read) != 1) {
fprintf(stderr, "哈希更新失败(文件读取到 %zu 字节时)\n", bytes_read);
ERR_print_errors_fp(stderr);
EVP_MD_CTX_free(ctx);
fclose(file);
return 0;
}
}
// 检查文件读取是否出错
if (ferror(file)) {
fprintf(stderr, "文件读取错误\n");
EVP_MD_CTX_free(ctx);
fclose(file);
return 0;
}
// 完成哈希计算并获取结果
if (EVP_DigestFinal_ex(ctx, hash, hash_len) != 1) {
fprintf(stderr, "SM3 计算完成失败\n");
ERR_print_errors_fp(stderr);
EVP_MD_CTX_free(ctx);
fclose(file);
return 0;
}
// 释放资源
EVP_MD_CTX_free(ctx);
fclose(file);
return 1;
}
int main(int argc, char *argv[]) {
const char *file_path = "D:/Neo4j/neo4j-community-3.5.5-windows.zip";
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len;
// 初始化 OpenSSL 错误信息
ERR_load_crypto_strings();
// 计算文件的 SM3 哈希
if (sm3_file_hash(file_path, hash, &hash_len)) {
printf("文件: %s\n", file_path);
printf("SM3 哈希值: ");
for (unsigned int i = 0; i < hash_len; i++) {
printf("%02x", hash[i]);
}
printf("\n哈希长度: %u 字节\n", hash_len);
}
// 清理错误信息
ERR_free_strings();
return 0;
}
运行截图如下:
