项目中遇到了需要与JAVA WEB接口使用SM3,SM4加密数据对接的需求,于是简单了解了下SM3与SM4加密算法在C++环境下的实现。并使用Qt/C++还原了在线SM3国密加密工具和在线SM4国密加密解密工具网页的示例功能的实现
目录导读
- 前言
- SM3算法简介
- SM4算法简介
- 实现示例
- 字符串HEX,BASE64格式转换
- 使用CryptBinaryToString 函数
- 使用QByteArray类中的静态函数
- 使用PKCS5Padding标准填充
- 代码示例
前言
简单介绍下SM3,SM4的C++算法
SM3是中华人民共和国政府采用的一种密码散列函数标准,由国家密码管理局于2010年12月17日发布。相关标准为“GM/T 0004-2012 《SM3密码杂凑算法》”。
在商用密码体系中,SM3主要用于数字签名及验证、消息认证码生成及验证、随机数生成等,其算法公开。据国家密码管理局表示,其安全性及效率与SHA-256相当。
摘要出自:在线SM3国密加密工具
- SM3加密C++代码实现:
SM3密码杂凑算法 国密pdf文件说明 文档看着也比较头疼,这里直接借鉴使用 SM3算法C语言实现-下载中的代码,实测没任何问题,这里不贴出代码,如有需要请前往下载。
- 参考文章:
国密SM3杂凑算法与实现
SM3算法C语言实现-下载
国密SM3算法在linux和windows平台结果不一致问题
sm3算法实现
SM3密码杂凑算法 国密pdf文件说明
SM4算法简介
SM4.0(原名SMS4.0)是中华人民共和国政府采用的一种分组密码标准,由国家密码管理局于2012年3月21日发布。相关标准为“GM/T 0002-2012《SM4分组密码算法》(原SMS4分组密码算法)”。
在商用密码体系中,SM4主要用于数据加密,其算法公开,分组长度与密钥长度均为128bit,加密算法与密钥扩展算法都采用32轮非线性迭代结构,S盒为固定的8比特输入8比特输出。
SM4.0中的指令长度被提升到大于64K(即64×1024)的水平,这是SM 3.0规格(渲染指令长度允许大于512)的128倍。
摘要出自:在线SM4国密加密解密工具
- SM4加密C++代码实现:
同样SM4的C++代码直接使用国密SM4分组加密算法实现 (C++)中实现的SM4源码,
可以直接在国密SM4分组加密算法实现 (C++)文章末尾下载,如有需要请前往下载。
- 参考文章:
SM4国密算法实现分析
国密SM4分组加密算法实现 (C++)
实现示例
尝试使用Qt Creator 5.13.1 MSCV2017编写一个实现和在线SM4国密加密工具一样的功能的Demo,
整个样式也是借鉴的在线SM4国密加密工具界面样式。
- 如下图示:
示例执行程序见文章附件资源;
在线SM4国密加密工具中的功能实现其实并不复杂,
SM3,SM4的C++算法实现都可以通过上面的链接下载后直接使用,
再通过HEX,BASE64字符串格式转换和字符串填充就能直接输出一致的加密解密内容
在进行数据加密的时候需要将PBYTE(unsigned char*)字符串转换为HEX(十六进制) 或者 Base64格式;
一开始我是打算使用CryptBinaryToStringW函数进行转换,
后来发现QByteArray类能直接转换成HEX(十六进制) 和Base64格式字符串
CryptBinaryToString 函数将字节数组转换为格式化字符串。
传入PBYTE数据字符串并指定长度,
并输出标准化的HEX(16进制)或BASE64字符串
- 语法:
BOOL CryptBinaryToStringW(
//!指向要转换为字符串的字节数组的指针。
[in] const BYTE *pbBinary,
//! pbBinary 数组中的元素数。
[in] DWORD cbBinary,
//! 指定生成的格式化字符串的格式 CRYPT_STRING_HEX or CRYPT_STRING_BASE64
[in] DWORD dwFlags,
//! 指向接收转换字符串的缓冲区的指针。 若要计算必须分配以保存返回的字符串的字符数,请将此参数设置为 NULL。 函数会将所需数量的字符(包括终止 NULL 字符)放在 pcchString 指向的值中
[out, optional] LPWSTR pszString,
//! 指向 DWORD 变量的指针,该变量包含 pszString 缓冲区的大小(以 TCHAR为单位)。 如果 pszString 为 NULL,则函数计算返回字符串的长度, (包括 TCHARs 中的终止 null 字符) ,并在此参数中返回它。 如果 pszString 不为 NULL 且不够大,则该函数会将二进制数据转换为指定的字符串格式(包括终止 null 字符),但 pcchString 接收 TCHARs 的长度,不包括终止 null 字符。
[in, out] DWORD *pcchString
);
- C++代码示例:
引用头文件:
#include <Windows.h>
#include <wincrypt.h>
#include <string.h>
#include <stdio.h>
#pragma comment(lib, "Crypt32.lib")
class WinApi_Binary
{
public:
WinApi_Binary();
/*
* https://learn.microsoft.com/zh-cn/windows/win32/api/wincrypt/nf-wincrypt-cryptbinarytostringw
CryptBinaryToStringW
*/
//! 将PBYTE转换成十六进制字符串
QString TO_HEX32(const BYTE * pbBinary);
//! 将PBYTE转换成BASE64字符串
QString TO_BASE64(const BYTE * pbBinary);
private:
//! CryptBinaryToString 函数将字节数组转换为格式化字符串。
//! https://learn.microsoft.com/zh-cn/windows/win32/api/wincrypt/nf-wincrypt-cryptbinarytostringw
QString ToCryptBinaryToString(const BYTE *pbBinary,DWORD cbBinary,DWORD dwFlags);
};
具体实现:
WinApi_Binary::WinApi_Binary()
{
}
QString WinApi_Binary::ToCryptBinaryToString(const BYTE *pbBinary,DWORD cbBinary,DWORD dwFlags)
{
DWORD pcchString=0;
QString ValString="";
if(CryptBinaryToStringW(pbBinary,cbBinary,dwFlags,NULL,&pcchString))
{
if(pcchString==0)
return ValString;
LPWSTR pszString=new TCHAR[pcchString];
if(CryptBinaryToStringW(pbBinary,cbBinary,dwFlags,pszString,&pcchString))
{
ValString=QString::fromWCharArray(pszString);
}
delete pszString;
pszString=nullptr;
}
return ValString;
}
QString WinApi_Binary::TO_HEX32(const BYTE * pbBinary)
{
return ToCryptBinaryToString(pbBinary,32,CRYPT_STRING_HEX|CRYPT_STRING_NOCRLF);
}
QString WinApi_Binary::TO_BASE64(const BYTE * pbBinary)
{
return ToCryptBinaryToString(pbBinary,32,CRYPT_STRING_BASE64|CRYPT_STRING_NOCRLF);
}
CryptBinaryToStringW 转换字符串的使用还是过于繁琐了,使用Qt自带的QByteArray类转换字符串更快更方便;
使用
QByteArray::fromHex(Text.toLatin1())
或QByteArray::fromBase64(Text.toLatin1())
获取base64,hex(16进制)格式数据;
使用
QByteArray((char*)DestBuffer,actualen).toHex().toUpper();
QByteArray((char*)DestBuffer,actualen).toBase64()
转换成字base64,hex(16进制)格式符串,
其中DestBuffer是PBYTE类型,actualen是字符长度;
使用QByteArray类进行字符串格式转换比较简单。直接可以转换成QString输出文本。
SM3加密不需要进行对字符串进行填充,
SM4加密时需要对字符串进行PKCS5Padding标准填充,否则计算除了的加密/解密字符串会与在线SM4国密加密工具计算出来的结果不一致。
在通过SM4加密与在线SM4国密加密工具计算出来的结果对比失败N次后才发现需要进行PKCS5Padding标准字符串填充,PKCS5Padding填充规则并不复杂:
PKCS5Padding:填充的原则是,如果长度少于16个字节,需要补满16个字节,补(16-len)个(16-len)例如:
huguozhen这个节符串是9个字节,16-9= 7,补满后如:huguozhen+7个十进制的7
如果字符串长度正好是16字节,则需要再补16个字节的十进制的16。
参考出自:关于C++和JAVA,AES/ECB/PKCS5Padding 互相通信的问题
- C++代码实现:
//! input 需要填充的字符串
//! inputLen 字符串长度
//! actualen 填充后的字符串长度
//! 返回填充后的字符串
unsigned char * PKCS5Padding(unsigned char *input,int inputLen,int& actualen)
{
actualen=0;
int diff=0;
if(inputLen%16==0)
{
actualen=inputLen+16;
diff=16;
}
else
{
int num= (int)ceil((double)inputLen/16.0);
actualen=num*16;
diff=actualen-inputLen;
}
qDebug()<<"[len] "<<inputLen<<" [actualen] "<<actualen<<" [diff] "<<diff;
unsigned char * SrcBuffer=(unsigned char *)malloc(sizeof (unsigned char)*actualen);
memset(SrcBuffer, diff, actualen);
for(int i=0;i<inputLen;i++)
{
SrcBuffer[i]=input[i];
}
return SrcBuffer;
}
SM4加密填充,解密后同样需要移除填充的内容:
因为加密时补的是十进制1到16,解密时,需要把这部分补位的去掉,判断要解密的字符串,每个字节是不是 char>=1 && char<= 16,如果是的话,就用0来替换以前的值,直到结束。
参考出自:关于C++和JAVA,AES/ECB/PKCS5Padding 互相通信的问题
通过上述对字符串进行HEX,BASE64格式转换再PKCS5Padding填充后,
加密/解密的结果基本就和在线SM4国密加密工具一致了。
这里简单演示调用上面的SM3和SM4加密的C++的代码示例;
- SM3加密示例
使用libsm3.sm3(Data, Lens, OutText)
直接调用加密,这是上面SM3加密C++ Demo中已经实现的方法,
使用QTextCodec
进行文本字符串格式转换,测试使用Utf-8和Gb2312与在线工具输出一致。
使用QByteArray
输入输出HEX(16进制)或Base64格式数据
QString Operate_Command::SM3_Encrypt(QString Text,int TextType,QString codeType,int outType)
{
QByteArray encodedData;
switch (TextType) {
case 0:
{
//! 文本
QTextCodec * Codec=QTextCodec::codecForName(codeType.toStdString().c_str());
qDebug()<<"Codec: "<<Codec->name();
encodedData = Codec->fromUnicode(Text);
break;
}
case 1:
{
//! HEX
encodedData=QByteArray::fromHex(Text.toLatin1());
break;
}
case 2:
{
//! BASE64
encodedData=QByteArray::fromBase64(Text.toLatin1());
break;
}
}
Lib_SM3 libsm3;
//! 字符长度
int Lens=encodedData.size();
PBYTE Data=(PBYTE)encodedData.data();
PBYTE OutText=new BYTE[32];
//! SM3加密
libsm3.sm3(Data, Lens, OutText);
// WinApi_Binary binary;
QString middlekey="";
//! 加密输出格式
switch (outType) {
case 0:
{
//! 输出 HEX 十六进制
middlekey=QByteArray((char*)OutText,32).toHex().toUpper();
break;
}
case 1:
{
//! 输出 base64格式
middlekey=QByteArray((char*)OutText,32).toBase64();
break;
}
}
return middlekey;
}
- SM4加密示例
使用SM4 ECB PKCS5Padding
加密示例:
先使用sm4_setkey_enc函数
设置密钥,
在通过PKCS5Padding填充
字符串,
在使用sm4_crypt_ecb函数
加密内容,
sm4_setkey_enc函数
和sm4_crypt_ecb函数
是上面SM4加密C++ Demo中已经实现的方法,只需要调用。
代码示例:
QString Operate_Command::SM4_Encrypt_ECB(QString Key,int keyType,QString Text,int TextType,QString codeType,int outType)
{
qDebug()<<" --- --- ---> ";
QByteArray encodedData;
switch (TextType) {
case 0:
{
//! 文本
QTextCodec * Codec=QTextCodec::codecForName(codeType.toStdString().c_str());
encodedData = Codec->fromUnicode(Text);
break;
}
case 1:
{
//! HEX
encodedData=QByteArray::fromHex(Text.toLatin1());
break;
}
case 2:
{
//! BASE64
encodedData=QByteArray::fromBase64(Text.toLatin1());
break;
}
}
qDebug("%s",encodedData.data());
QByteArray keyData;
switch (keyType) {
case 0:
{
//! 文本
QTextCodec * Codec=QTextCodec::codecForName(codeType.toStdString().c_str());
keyData = Codec->fromUnicode(Key.mid(0,16));
break;
}
case 1:
{
//! HEX
keyData=QByteArray::fromHex(Key.toLatin1());
break;
}
case 2:
{
//! BASE64
keyData=QByteArray::fromBase64(Key.toLatin1());
break;
}
}
qDebug("%s",keyData.data());
int length=encodedData.size();
PBYTE SrcBuffer=(PBYTE)encodedData.data();
PBYTE keyBuffer=(PBYTE)keyData.data();
Lib_SM4 libsm4;
sm4_context ctx;
//设置 加密秘钥
libsm4.sm4_setkey_enc(&ctx,keyBuffer);
//字符串填充
int actualen=length;
PBYTE ToPKCS5Pad=libsm4.PKCS5Padding(SrcBuffer,length,actualen);
PBYTE DestBuffer=new BYTE[actualen];
//加密
libsm4.sm4_crypt_ecb(&ctx, 1, actualen, ToPKCS5Pad, DestBuffer); //1 为加密 0为解密
QString middlekey="";
//! 加密输出格式
switch (outType) {
case 1:
{
//! 输出 HEX 十六进制
middlekey=QByteArray((char*)DestBuffer,actualen).toHex().toUpper();
break;
}
case 0:
case 2:
{
//! 输出 base64格式
middlekey=QByteArray((char*)DestBuffer,actualen).toBase64();
break;
}
}
QString VStr="";
QString SRCSTR="";
for(int i=0;i<actualen;i++)
{
VStr+=QString("%1").arg(DestBuffer[i],2,16,QLatin1Char('0'));
SRCSTR+=QString("%1 ").arg(ToPKCS5Pad[i],2,16,QLatin1Char('0'));
}
qDebug()<<"[VStr] "<<VStr;
qDebug()<<"[SRCSTR] "<<SRCSTR;
delete DestBuffer;
DestBuffer=nullptr;
free(ToPKCS5Pad);
return middlekey;
}