Delphi XE2集成GmSSL实现SM2国密算法,打通与Web后端的安全通信
1. 项目概述:为什么要在Delphi XE2里折腾SM2?
如果你是一个用Delphi做桌面应用或者工业上位机软件的老手,最近可能被一个需求卡住了:客户或者项目要求,软件和后台Web服务之间的数据传输,必须使用国密算法进行加密。后台那边,Java或者Go的兄弟们三下五除二就用上了Bouncy Castle或者GmSSL,但你这边的Delphi XE2,翻遍了VCL和Indy组件库,发现对SM2、SM3、SM4这些国密算法压根没有原生支持。这感觉就像大家都在用5G视频通话了,你手里还攥着一部只能发短信的功能机。
这个项目标题“delphi XE2实现与网页互通的SM2国密加解密算法”,精准地戳中了这个痛点。它的核心目标不是研究算法本身,而是在Delphi这个经典的Windows桌面开发环境中,搭建一座能与现代Web后端(通常是Java/Spring Boot, Node.js, Python等)安全通信的“国密桥梁”。SM2作为非对称加密算法,负责密钥交换和数字签名,是这套国密体系的门户和基石。互通性(Interoperability)是这里最大的挑战,也是成败的关键。你不能自己加密的数据,对方解不开;对方发来的签名,你验证不了。
我最近刚在一个数据采集上报的项目里完整走通了这条路。客户的后台是Java Spring Cloud,要求所有终端上报的数据必须用SM2签名、SM4加密。我用Delphi XE2成功实现了整套流程,并且和后台联调通过。这个过程里踩的坑、试的错,比最后成功的代码要多得多。这篇文章,我就把这套从零搭建、确保互通、可直接复现的方案拆开揉碎了讲给你听,重点不止是“怎么做”,更是“为什么这么做”以及“怎么避开那些深坑”。
2. 整体方案设计与核心思路拆解
在Delphi里实现一个算法,通常有三条路:纯Pascal自己实现、调用DLL(C/C++编译的动态库)、或者寻找现成的ActiveX或COM组件。对于SM2这种涉及大量椭圆曲线运算的复杂算法,第一条路对于大多数以业务开发为主的Delphi程序员来说,时间成本和出错风险都太高。所以,调用成熟的、经过验证的C语言国密库,封装成Delphi能调用的DLL,是唯一务实且可靠的选择。
2.1 国密算法库选型:GmSSL vs. 其他
目前主流的开源国密算法C实现库,主要有以下几个:
- GmSSL:北京大学团队维护,目前最活跃、最完整的国密算法开源工具包。它不仅是库,还是一个命令行工具(可以理解为国密版的OpenSSL)。它支持SM2、SM3、SM4、SM9等全套算法,且代码质量高,社区认可度强。最关键的是,它的API相对清晰,编译成Windows DLL的教程也比较多。
- Bouncy Castle:这是一个Java和C#的加密库,虽然也有C#版本,但无法直接给Delphi使用。不过,很多基于Bouncy Castle的C#国密实现,可以通过.NET COM Interop的方式让Delphi调用,这条路更曲折,且依赖.NET框架。
- 各种“国密算法C源码”:GitHub上能找到一些独立的SM2 C实现代码。这些代码通常比较原始,可能只实现了核心算法,缺少完整的ASN.1编码解码、密钥格式处理等周边功能,与外界互通时需要自己处理大量细节,极易出错。
注意:网络上搜索“gmssl是国密调用window电脑”这个热词,反映的正是大家想在Windows环境下使用GmSSL的普遍需求。这恰恰证明了我们的方向是对的。
为什么坚定选择GmSSL?首先,它的功能完整度最高,我们需要的SM2加密、解密、签名、验签以及密钥对生成、PEM格式导入导出,它都提供了现成的、稳定的API。其次,它的文档和社区资源相对丰富,遇到问题有更多线索可查。最后,它的License(主要是Apache 2.0)对商业应用比较友好。因此,我们的技术栈就确定为:Delphi XE2(前端/客户端) + GmSSL编译的DLL(算法核心) + 自定义的Delphi封装单元(业务桥梁)。
2.2 互通性关键:理解数据格式与编码
这是整个项目最容易栽跟头的地方。SM2算法操作的对象(公钥、私钥、密文、签名)都不是简单的字节数组(Byte Array),它们有严格且复杂的格式规范。如果双方格式不统一,就像你用中文写信,对方却用摩斯密码解码,必然失败。
- 密钥格式:最常用的是PEM格式。这是一种用
-----BEGIN XXX-----和-----END XXX-----包裹的Base64编码文本。例如,SM2私钥PEM文件头通常是-----BEGIN PRIVATE KEY-----。GmSSL生成的PEM,和Java Bouncy Castle或hutool-sm2生成的PEM,在内部ASN.1结构上必须兼容。幸运的是,遵循国密标准规范的实现,其PEM格式是互通的。 - 密文格式:SM2加密后的输出,并不是简单的“密文”。根据国标GM/T 0009-2012,SM2加密输出的密文结构是
C1C2C3或C1C3C2的ASN.1编码或裸拼接。其中C1是椭圆曲线点,C2是密文消息本身,C3是SM3杂凑值。GmSSL默认输出的是ASN.1 DER编码的密文。而很多在线工具(如搜索“sm2在线加解密”找到的那些)或Java库,可能默认输出或接受的是C1C3C2的十六进制拼接字符串。格式不匹配是解密失败的首要原因。 - 签名格式:SM2签名输出通常是一对整数(r, s)。在传输时,也需要进行ASN.1 DER编码,变成一个字节序列。验签方必须用同样的格式解析。
我们的Delphi封装层,一个核心任务就是正确调用GmSSL的API,并处理好与后端约定的数据格式。通常,为了最大化兼容性,我会建议前后端统一使用“裸拼接的十六进制字符串(C1C3C2)”作为密文传输格式,或者统一使用“ASN.1 DER编码后的Base64字符串”。在项目开始前,必须和后端团队明确这一点。
2.3 Delphi封装层设计思路
我们不直接在Delphi项目里写一堆晦涩的External函数声明去调用DLL。那样做可维护性太差。正确的做法是创建一个独立的Unit(例如uGmSSLWrapper.pas),在这个单元里完成所有工作:
- DLL函数接口声明:使用
stdcall调用约定,精确声明需要使用的GmSSL函数。 - 高级对象封装:设计如
TSM2KeyPair、TSM2Cipher这样的类,将底层DLL调用、内存管理、错误处理封装起来,对外提供Encrypt,Decrypt,Sign,Verify等易于理解的方法。 - 编码解码工具函数:提供
HexToStr,StrToHex,Base64Encode,Base64Decode,以及最重要的DerToRawC1C3C2和RawC1C3C2ToDer等格式转换函数。 - 集中式错误处理:捕获DLL调用返回的错误码,转换成有意义的异常信息抛出。
这样,在主程序中,你只需要uses uGmSSLWrapper,然后像使用普通VCL组件一样创建对象、调用方法,代码会非常清晰。
3. 环境准备与GmSSL DLL编译
这是实操的第一步,也是基础。你需要一个能工作的GmSSL DLL文件。
3.1 编译环境搭建
GmSSL主要是在Linux环境下用GCC编译的,但在Windows上编译也不复杂。推荐使用MSYS2+MinGW-w64这套工具链,它提供了一个类Linux的Shell环境,可以很好地运行GmSSL的configure和make脚本。
- 安装MSYS2:从官网下载安装。安装后,从开始菜单打开
MSYS2 MinGW 64-bit(注意,是64位,因为Delphi XE2也主要生成64位程序了)。 - 安装编译工具:在MSYS2终端中,运行以下命令安装必要的工具。
pacman -Syu # 更新系统 pacman -S --needed base-devel mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake git - 获取GmSSL源码:使用git克隆最新代码。
cd /d/DevLibraries # 找一个你喜欢的目录 git clone https://github.com/guanzhi/GmSSL.git cd GmSSL - 配置与编译:GmSSL默认编译为静态库(.a)和可执行文件。我们需要的是动态库(.dll)。
执行./config shared no-asm --prefix=/mingw64 make make installmake install后,编译好的文件会安装到MSYS2的/mingw64目录下。我们需要的libcrypto-3-x64.dll(或者类似名字,版本号可能不同)和libssl-3-x64.dll通常就在/mingw64/bin目录下。但注意,GmSSL可能不会直接生成一个独立的gmssl.dll,它的功能主要集成在libcrypto中。
实操心得:如果
./config或make过程报错,大概率是环境问题。一个更稳妥的办法是,直接去GmSSL项目的GitHub Release页面,看看有没有官方预编译好的Windows版本。或者,在网络上搜索“GmSSL Windows binary”,有时能找到热心网友编译好的DLL。这是快速启动项目的捷径,但务必从可信来源获取。
3.2 获取关键DLL与头文件
编译成功后,在/mingw64/bin里找到libcrypto-3-x64.dll(我们主要用它)和libssl-3-x64.dll。将它们复制到你的Delphi项目目录下。
更重要的是头文件(.h文件)。在/mingw64/include目录下,会有openssl文件夹,里面包含了sm2.h,ec.h,evp.h,bio.h,pem.h等大量头文件。我们不需要全部,但需要参考sm2.h等来了解GmSSL提供的SM2相关函数原型,以便在Delphi中正确声明。
4. Delphi封装层核心代码实现
这是最核心的部分。我将分步骤展示如何构建这个封装单元。
4.1 定义DLL导入函数
首先,我们需要知道GmSSL提供了哪些函数。由于GmSSL兼容OpenSSL的EVP API,我们通常使用更高层的EVP接口,而不是直接调用SM2_encrypt这样的底层函数。EVP接口更统一,也更安全(自动处理内存和上下文)。
在你的uGmSSLWrapper.pas单元开头,定义常量、类型和函数声明。
unit uGmSSLWrapper; interface uses SysUtils, Classes; const LIB_CRYPTO = 'libcrypto-3-x64.dll'; // 你的DLL文件名 type EVP_PKEY = Pointer; EVP_PKEY_CTX = Pointer; ENGINE = Pointer; BIO = Pointer; // 关键函数声明 function EVP_PKEY_new(): EVP_PKEY; cdecl; external LIB_CRYPTO; procedure EVP_PKEY_free(pkey: EVP_PKEY); cdecl; external LIB_CRYPTO; function EVP_PKEY_CTX_new(pkey: EVP_PKEY; e: ENGINE): EVP_PKEY_CTX; cdecl; external LIB_CRYPTO; procedure EVP_PKEY_CTX_free(ctx: EVP_PKEY_CTX); cdecl; external LIB_CRYPTO; // 更多函数声明,如 PEM_read_bio_PrivateKey, PEM_read_bio_PUBKEY, // EVP_PKEY_decrypt_init, EVP_PKEY_decrypt, EVP_PKEY_sign_init, // EVP_PKEY_verify_init, BIO_new_mem_buf, BIO_free 等等。 // 这里需要根据GmSSL的头文件仔细声明,参数和调用约定必须完全正确。声明这些函数非常繁琐且容易出错。一个更高效的方法是,直接利用GmSSL自带的libcrypto.dll的导入库(.a或.lib),但Delphi使用它们比较麻烦。因此,手动声明虽然笨,但最直接可控。你可以先从实现最核心的加解密开始,逐步添加函数。
4.2 封装SM2密钥对加载
假设我们已有PEM格式的私钥和公钥文件(或字符串)。我们需要将它们加载到EVP_PKEY结构中。
function LoadPrivateKeyFromPemStr(const APemStr: AnsiString): EVP_PKEY; var bio: BIO; pkey: EVP_PKEY; begin pkey := nil; bio := BIO_new_mem_buf(PAnsiChar(APemStr), Length(APemStr)); try // GmSSL中,SM2私钥通常以“PRIVATE KEY”格式存储 pkey := PEM_read_bio_PrivateKey(bio, nil, nil, nil); if pkey = nil then raise Exception.Create('Failed to load private key from PEM.'); finally BIO_free(bio); end; Result := pkey; end; function LoadPublicKeyFromPemStr(const APemStr: AnsiString): EVP_PKEY; var bio: BIO; pkey: EVP_PKEY; begin pkey := nil; bio := BIO_new_mem_bio(PAnsiChar(APemStr), Length(APemStr)); try pkey := PEM_read_bio_PUBKEY(bio, nil, nil, nil); if pkey = nil then raise Exception.Create('Failed to load public key from PEM.'); finally BIO_free(bio); end; Result := pkey; end;4.3 实现SM2加密与解密
这是互通性的核心。我们必须明确输入输出的格式。这里我以实现“明文/密文为字节流,输出为C1C3C2拼接的十六进制字符串”为例,因为这种格式与许多在线工具和Java库(如Hutool的SM2)默认方式兼容。
function SM2Encrypt(const APublicKeyPem: AnsiString; const APlainData: TBytes): AnsiString; var pkey: EVP_PKEY; ctx: EVP_PKEY_CTX; outlen: NativeUInt; outbuf: TBytes; begin Result := ''; pkey := LoadPublicKeyFromPemStr(APublicKeyPem); if pkey = nil then Exit; try ctx := EVP_PKEY_CTX_new(pkey, nil); if ctx = nil then raise Exception.Create('Failed to create PKEY context.'); try // 初始化加密上下文,使用SM2算法 if EVP_PKEY_encrypt_init(ctx) <= 0 then raise Exception.Create('EVP_PKEY_encrypt_init failed.'); // 第一次调用,获取输出缓冲区的长度 if EVP_PKEY_encrypt(ctx, nil, @outlen, @APlainData[0], Length(APlainData)) <= 0 then raise Exception.Create('EVP_PKEY_encrypt (get length) failed.'); // 分配缓冲区并执行加密 SetLength(outbuf, outlen); if EVP_PKEY_encrypt(ctx, @outbuf[0], @outlen, @APlainData[0], Length(APlainData)) <= 0 then raise Exception.Create('EVP_PKEY_encrypt failed.'); // 此时 outbuf 中是 ASN.1 DER 编码的密文。 // 我们需要将其转换为 C1C3C2 的裸拼接格式。 Result := DerCipherToRawHex(outbuf); // 这是一个需要自己实现的格式转换函数 finally EVP_PKEY_CTX_free(ctx); end; finally EVP_PKEY_free(pkey); end; end; function SM2Decrypt(const APrivateKeyPem: AnsiString; const ACipherHex: AnsiString): TBytes; var pkey: EVP_PKEY; ctx: EVP_PKEY_CTX; derCipher: TBytes; outlen: NativeUInt; begin SetLength(Result, 0); pkey := LoadPrivateKeyFromPemStr(APrivateKeyPem); if pkey = nil then Exit; try ctx := EVP_PKEY_CTX_new(pkey, nil); if ctx = nil then raise Exception.Create('Failed to create PKEY context.'); try // 解密前,需要将十六进制的 C1C3C2 裸拼接格式,转换回 GmSSL 期望的 ASN.1 DER 格式。 derCipher := RawHexCipherToDer(ACipherHex); // 逆向转换函数 if EVP_PKEY_decrypt_init(ctx) <= 0 then raise Exception.Create('EVP_PKEY_decrypt_init failed.'); // 第一次调用,获取输出缓冲区长度 if EVP_PKEY_decrypt(ctx, nil, @outlen, @derCipher[0], Length(derCipher)) <= 0 then raise Exception.Create('EVP_PKEY_decrypt (get length) failed.'); // 分配缓冲区并执行解密 SetLength(Result, outlen); if EVP_PKEY_decrypt(ctx, @Result[0], @outlen, @derCipher[0], Length(derCipher)) <= 0 then raise Exception.Create('EVP_PKEY_decrypt failed.'); SetLength(Result, outlen); // 调整到实际解密出的长度 finally EVP_PKEY_CTX_free(ctx); end; finally EVP_PKEY_free(pkey); end; end;关键点解析:DerCipherToRawHex和RawHexCipherToDer这两个函数是实现互通性的灵魂。GmSSL的EVP_PKEY_encrypt输出的是ASN.1 DER编码的数据(一个TLV结构)。而很多其他平台(如用hutool-sm2)默认使用简单的C1C3C2字节拼接。你需要解析DER结构,提取出C1, C2, C3三个部分,然后按约定顺序(C1C3C2)拼接,再转成十六进制字符串。反之,解密时,需要将十六进制字符串还原成字节,拆分成三部分,再构造成GmSSL能识别的DER格式。这个过程需要你对ASN.1和SM2密文结构有清晰的理解。网上可以找到一些现成的C或Java代码实现这个转换,你需要将其“翻译”成Delphi。
4.4 实现SM2签名与验签
签名验签的流程与加解密类似,但通常不涉及复杂的格式转换,因为签名值本身就是(r, s)的DER编码。
function SM2Sign(const APrivateKeyPem: AnsiString; const AData: TBytes; const AId: AnsiString = '1234567812345678'): AnsiString; var pkey: EVP_PKEY; ctx: EVP_PKEY_CTX; md_ctx: EVP_MD_CTX; siglen: NativeUInt; sigbuf: TBytes; begin Result := ''; pkey := LoadPrivateKeyFromPemStr(APrivateKeyPem); if pkey = nil then Exit; try md_ctx := EVP_MD_CTX_new(); if md_ctx = nil then raise Exception.Create('Failed to create MD_CTX.'); try // 初始化签名上下文,指定摘要算法为SM3 if EVP_DigestSignInit(md_ctx, @ctx, EVP_sm3(), nil, pkey) <= 0 then raise Exception.Create('EVP_DigestSignInit failed.'); // 设置SM2签名使用的用户ID(Z值),这是SM2与ECDSA的重要区别 if EVP_PKEY_CTX_set1_id(ctx, PAnsiChar(AId), Length(AId)) <= 0 then raise Exception.Create('Failed to set SM2 ID.'); // 传入待签名数据 if EVP_DigestUpdate(md_ctx, @AData[0], Length(AData)) <= 0 then raise Exception.Create('EVP_DigestUpdate failed.'); // 第一次调用,获取签名长度 if EVP_DigestSignFinal(md_ctx, nil, @siglen) <= 0 then raise Exception.Create('EVP_DigestSignFinal (get length) failed.'); // 分配缓冲区并获取签名 SetLength(sigbuf, siglen); if EVP_DigestSignFinal(md_ctx, @sigbuf[0], @siglen) <= 0 then raise Exception.Create('EVP_DigestSignFinal failed.'); SetLength(sigbuf, siglen); // 签名值 sigbuf 已经是 DER 编码的 (r, s)。通常我们将其转为Base64或Hex传输。 Result := BytesToHex(sigbuf); // 或者使用 Base64Encode(sigbuf) finally EVP_MD_CTX_free(md_ctx); end; finally EVP_PKEY_free(pkey); end; end; function SM2Verify(const APublicKeyPem: AnsiString; const AData: TBytes; const ASignatureHex: AnsiString; const AId: AnsiString = '1234567812345678'): Boolean; var pkey: EVP_PKEY; ctx: EVP_PKEY_CTX; md_ctx: EVP_MD_CTX; sigbuf: TBytes; begin Result := False; pkey := LoadPublicKeyFromPemStr(APublicKeyPem); if pkey = nil then Exit; try md_ctx := EVP_MD_CTX_new(); if md_ctx = nil then Exit; try sigbuf := HexToBytes(ASignatureHex); // 将传输来的签名Hex转回字节 if EVP_DigestVerifyInit(md_ctx, @ctx, EVP_sm3(), nil, pkey) <= 0 then Exit; if EVP_PKEY_CTX_set1_id(ctx, PAnsiChar(AId), Length(AId)) <= 0 then Exit; if EVP_DigestUpdate(md_ctx, @AData[0], Length(AData)) <= 0 then Exit; // 执行验签,成功返回1,失败返回0 Result := (EVP_DigestVerifyFinal(md_ctx, @sigbuf[0], Length(sigbuf)) = 1); finally EVP_MD_CTX_free(md_ctx); end; finally EVP_PKEY_free(pkey); end; end;注意事项:SM2签名验签必须设置正确的用户ID(
AId参数),通常使用默认的1234567812345678(16字节)。这个ID值会影响最终生成的签名Z值,前后端必须使用完全相同的ID,否则验签必定失败。这是SM2与普通ECDSA的另一个关键区别。
5. 与网页后端联调实战与问题排查
封装好DLL后,真正的挑战才刚刚开始:和后台联调。这里记录几个我踩过的典型大坑和解决方法。
5.1 联调环境搭建与测试工具
在写任何网络代码之前,先用最直接的方式验证算法本身的互通性。
- 准备测试密钥对:使用GmSSL命令行生成一对SM2密钥。
# 在MSYS2或已安装GmSSL的环境下 gmssl sm2 -genkey -out sm2_private.pem gmssl sm2 -pubout -in sm2_private.pem -out sm2_public.pem - 准备对比方:找一个你确信正确的国密工具。可以是:
- 一个已知正确的Java程序(使用Bouncy Castle或
hutool-sm2)。 - 一个可靠的在线SM2加解密网站(搜索“sm2在线加解密”能找到很多,但要注意选择口碑好的,并理解其使用的格式)。
- 一个已知正确的Java程序(使用Bouncy Castle或
- 分步测试:
- 加密/解密自测:用你的Delphi程序加载公钥,加密一段文本,输出Hex。然后用GmSSL命令行工具解密,看是否能还原。反之亦然。
- 与Java对比:让Java后端用同样的公钥加密一段数据,将密文Hex发给你,你用Delphi解密。同时,你用Delphi加密一段数据发给Java,让他解密。
- 签名/验签自测与互验:流程同上。
5.2 常见互通性问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| Delphi加密,对方无法解密 | 密文格式不一致。Delphi输出的是DER,对方期待的是Raw C1C3C2 Hex(或反之)。 | 1. 确认双方约定的密文格式。2. 在Delphi端,加密后输出两种格式(DER Base64 和 Raw Hex),让对方分别尝试解密。3. 实现并检查DerCipherToRawHex函数的正确性。 |
| 对方加密,Delphi无法解密 | 同上,格式问题。或者对方使用的曲线参数与GmSSL不完全一致(概率极低)。 | 1. 让对方提供密文的同时,注明格式。2. 在RawHexCipherToDer函数中加详细日志,打印出转换后DER数据的Hex,与GmSSL命令行工具对一个已知明文加密产生的DER数据对比。 |
| 签名验签失败 | 1. 用户ID(Z值)不一致。2. 签名值编码格式不一致(DER vs Raw r+s)。3. 待签名数据本身不同(如多了空格、编码不同)。 | 1.首要检查:确认双方set1_id的值完全一样,默认都是1234567812345678。2. 确认签名值的传输格式。是DER的Hex,还是(r,s)拼接的Hex?3. 对完全相同的原始数据字节数组进行签名,排除数据预处理差异。 |
| 加载PEM密钥失败 | 1. PEM文件格式错误(如头尾标识不对)。2. 密钥不是SM2类型。3. Delphi字符串编码问题(AnsiString/UnicodeString)。 | 1. 用文本编辑器打开PEM文件,检查头尾行。2. 用gmssl pkey -in key.pem -text -noout检查密钥信息。3. 确保传递给DLL的PEM字符串是AnsiString类型。 |
| 调用DLL函数时程序崩溃 | 1. 函数声明错误(调用约定、参数类型)。2. DLL未正确加载或版本不匹配。3. 内存管理错误(如未初始化指针)。 | 1. 使用Depends.exe工具查看DLL导出函数的确切名称和序号,确保声明一致。2. 将DLL放在exe同目录或系统路径。3. 使用try...except包裹所有DLL调用,并输出详细的错误信息(如GetLastError)。 |
5.3 网络传输中的实践要点
当算法层调通后,集成到HTTP/HTTPS通信中时,还需注意:
- 数据编码:二进制数据(加密后的密文、签名)在JSON中传输时,需要编码为可打印字符。Base64是比Hex更优的选择,因为体积更小。确保双方编解码方式一致。
- HTTP请求示例:假设你使用
TIdHTTP组件向后台发送加密数据。procedure SendEncryptedData; var HTTP: TIdHTTP; ReqStream: TStringStream; PlainText, CipherText, Signature: string; JsonToSend: string; begin PlainText := '{"sensorId":1,"value":25.5}'; // 1. 加密数据 CipherText := SM2Encrypt(ServerPublicKeyPem, TEncoding.UTF8.GetBytes(PlainText)); // 2. 签名(签名原始数据或密文,需与后端约定) Signature := SM2Sign(MyPrivateKeyPem, TEncoding.UTF8.GetBytes(PlainText)); // 3. 构造JSON请求体 JsonToSend := Format('{"cipherData":"%s","signature":"%s"}', [Base64Encode(CipherText), Base64Encode(Signature)]); HTTP := TIdHTTP.Create(nil); ReqStream := TStringStream.Create(JsonToSend, TEncoding.UTF8); try HTTP.Request.ContentType := 'application/json'; // 发送请求 HTTP.Post('https://api.example.com/data', ReqStream); finally ReqStream.Free; HTTP.Free; end; end; - 性能考虑:SM2非对称加密较慢,不适合加密大量数据。常规做法是:用SM2加密一个随机生成的SM4对称密钥,然后用这个SM4密钥加密实际业务数据。将SM4密文和加密后的SM4密钥一起传输。这需要你在Delphi端也实现SM4算法,同样可以通过封装GmSSL的
EVP_sm4_cbc等接口来实现。
6. 项目总结与进阶思考
走通整个流程后,你会发现,在Delphi XE2中实现国密算法互通,技术难点并不在Delphi语言本身,而在于对国密算法标准、数据格式、以及C语言库调用的深入理解。这个项目更像是一个“系统集成”工作。
我个人最大的体会是:前期与后端团队的沟通约定,比后期埋头写代码更重要。必须在一开始就明确:
- 双方使用哪个曲线参数(默认都是
sm2p256v1,但需确认)。 - 密文的交换格式(强烈建议统一为C1C3C2拼接的Hex或ASN.1 DER的Base64)。
- 签名的用户ID值。
- 签名是针对原始数据还是密文(通常签原始数据)。
- 网络传输时,二进制数据的编码方式(Base64)。
把这些写在联调文档里,能节省大量的调试时间。
最后,如果你想更进一步,可以考虑将整个封装层打包成一个设计良好的组件(Component),安装到Delphi的IDE工具栏上,并为其设计属性(如PublicKeyPem, PrivateKeyPem)和方法(OnEncrypt, OnDecrypt等)。这样,团队里的其他开发者就可以像使用TIdSSLIOHandlerSocketOpenSSL一样,通过拖拽组件、设置属性来轻松实现国密通信,极大提升开发效率。这将是这个项目从“可用”到“好用”的关键一步。
