简介:这份源码面向使用 Delphi 开发、需要与 Java 端或其他平台进行加密数据交换的开发者,提供标准 AES 加解密实现,覆盖 AES/ECB/PKCS5Padding 与 AES/CBC/PKCS5Padding 两种模式,密钥长度支持 128、192、256 bit,并采用密钥 0 填充策略;加密结果可直接以 Base64 返回,便于与在线 AES 工具及 Java 程序互解验证,适合接口联调、报文加密等场景。资源包共 33 个文件、约 4.4MB,包含核心 pas 单元、编译产物 dcu、可直接运行的 exe 演示程序、dpr/dproj 工程文件,以及一份 Java 对照实现,方便比对两端算法细节;Readme 说明使用要点。目前已有 2170 人学习下载。需要留意的是 Base64 返回仅在 XE 及以上版本可用,Delphi 7 的 Base64 支持有限,源码在此基础上做了完善,可作为跨语言加密互通的参考实现。
1. 接口联调那天,Java 那边回了一句“解密失败”
接口联调那天,Java 端回过来一句“解密失败”,我盯着 Delphi 客户端发出去的 Base64 密文看了半小时,最后发现问题出在密钥字符串被逐字符塞进了 TBytes,十六个字符变成了十六个字节的 Unicode 低字节,密钥直接错位。这类事在 Delphi AES 跨语言对接里太常见了。这套跑在 Delphi XE2 上的标准 AES 加解密单元,做的是把这件事收成一行调用:AES/ECB 与 AES/CBC 两种模式,PKCS5Padding 填充,Base64 输出,密钥支持 128/192/256 位,和 Java 的Cipher.getInstance("AES/CBC/PKCS5Padding")能对上同一组密文。适合手上有 Delphi 存量系统、又必须和 Java 服务端交换敏感字段的开发者,尤其是那种不方便给老工程引入大型加密组件、只想丢一个 pas 文件进去的场景。
2. 工程结构与选型:AES.pas 里到底分了哪几层
拿到这类资源,我不急着看加密算法本身,先看单元怎么切。切得糙的,所有东西塞在一个 pas 里,两千行,S 盒常量、模式循环、Base64、字符串转换、测试代码混在一起,出问题只能全局搜。切得好的,接口边界清楚,替换实现的时候不用动调用方。这份工程属于后者,它把“分组加密主干”和“业务字符串”之间隔了两层,中间任何一层出问题都能单独定位。
2.1 目录与单元职责划分
工程拆出来大概是这么几个文件,每个文件的职责我标在后面:
AESDemo\ AESDemo.dpr 工程入口,控制台或 VCL 窗体都行,不承载业务逻辑 uAESCore.pas AES 分组算法主干:S 盒、密钥扩展、四轮变换、单块加解密 uAESMode.pas ECB / CBC 模式封装,处理 IV 与块间链接 uAESPadding.pas PKCS5 / PKCS7 填充与去填充,含非法填充检测 uAESUtil.pas 对外门面:字符串进、字符串出,负责 UTF-8 与 Base64 uAESTestCase.pas 测试向量与自检用例,不参与发布uAESCore只认字节数组,函数签名全是TBytes进TBytes出,不认识 string,也不认识 Base64。这样做的直接好处是:你在 Java 那边跑 NIST 测试向量,Delphi 这边也能跑同一组,中间不掺编码转换,出问题一定在算法层,不会绕进字符集的玄学里。
uAESUtil是唯一对外的单元。业务代码uses uAESUtil就够了,其余三个是它内部的实现细节。我一般会把它做成两三个全局函数的形式,因为老 Delph i工程里到处 new 对象、忘记 Free 的情况太多了,纯函数没有生命周期问题。
unit uAESUtil; interface uses System.SysUtils, System.Classes, IdCoderMIME, IdGlobal, uAESCore, uAESMode, uAESPadding; // ECB:无 IV,相同明文永远得到相同密文,只适合短标识类字段 function AESEncryptECB(const APlain, AKey: string): string; function AESDecryptECB(const ACipherBase64, AKey: string): string; // CBC:IV 长度必须 16 字节,建议随密文一起传输 function AESEncryptCBC(const APlain, AKey, AIV: string): string; function AESDecryptCBC(const ACipherBase64, AKey, AIV: string): string; implementation接口里我特意把 ECB 和 CBC 拆成两组函数,而不是用一个AMode枚举参数。原因是 ECB 根本不该出现 IV 参数,硬塞一个会让调用方以为 ECB 也要传 IV,这种误解在生产里真的会造成“两边都能加密、就是解不开”的场面。
2.2 选型理由:为什么是 ECB/CBC + PKCS5Padding + Base64 这组搭配
AES 本身只是分组算法,真正决定跨语言能不能互通的是外面这三层选择。Java 的 JCE 默认提供AES/ECB/PKCS5Padding和AES/CBC/PKCS5Padding两种最常见的组合,Delphi 侧如果不对齐这三项,密文永远对不上。
| 选择项 | 本项目取值 | 为什么不选另一个 |
|---|---|---|
| 分组模式 | ECB、CBC | CTR/GCM 在 XE2 时代的老 JCE 默认提供者里不一定齐全,服务端未必放开 |
| 填充方式 | PKCS5Padding | ZeroPadding 遇到明文尾部本身就是 0x00 时无法还原,NoPadding 要求调用方自己保证 16 字节对齐 |
| 密文编码 | Base64 | Hex 体积多三分之一,且转 JSON、塞 URL 时更容易被中间件改写 |
| 密钥长度 | 128/192/256 | 老系统多用 128 位,256 位在部分 JDK 上要装无限制策略文件 |
关于 ECB 这个模式,需要说清楚它的问题:相同的 16 字节明文块会得到相同的密文块,攻击者能看出数据里的重复结构。所以 ECB 我只在两种场景用——一种是加密固定长度的令牌、流水号这类本身没有结构的短数据;另一种是对端接口已经定死只能用 ECB,改不动。凡是加密用户信息、报文正文这类有重复模式的长文本,一律走 CBC,并且 IV 每次随机生成。
提示:CBC 的 IV 不是密钥,不需要保密,但必须随机且每次不同。我通常的做法是把 16 字节 IV 直接拼在密文字节前面,整体再 Base64,接收方取前 16 字节当 IV、剩下的当密文,这样接口只需要传一个字符串。
2.3 密钥和 IV 放在哪,别等到上线才想
这一步最容易被跳过。Demo 里密钥写死成'1234567890abcdef'无所谓,真上生产就不行了。老 Delphi 工程的常见做法是把密钥放在加密后的配置文件或者数据库配置表里,启动时读一次缓存在内存,别在每个调用点重新拼字符串。我见过的翻车案例是密钥字符串散落在十几个 pas 文件里,换一次密钥要重新编译整个工程。
IV 的处理反过来——不要把 IV 写成常量。固定 IV 的 CBC 在密码学上退化得比 ECB 好不了多少,第一块明文的加密结果永远一样。真要做成固定值,那还不如老老实实用 ECB,至少少一层误解。
3. 核心代码拆解:一个字符串从明文到 Base64 密文的完整链路
链路一共四步:字符串转 UTF-8 字节 → PKCS5 填充补齐 16 字节整数倍 → 分组加密(ECB 逐块独立,CBC 逐块异或上一块密文)→ Base64 编码。任何一步错位,最后都是“解密失败”这一个笼统的结果,所以看代码时我习惯从后往前推:先确认 Base64 解出来的字节长度是不是 16 的整数倍,再确认密钥长度,最后才怀疑算法实现。
3.1 密钥扩展与分组加密主干
AES 的密钥扩展(Key Expansion)是把 16/24/32 字节的原始密钥扩展成 11/13/15 组轮密钥,每组 16 字节。这一步只跟密钥有关,跟明文无关,所以可以只算一次然后缓存复用。
type TAESKeySize = (ks128, ks192, ks256); TAESContext = record RoundKeys: array[0..59] of LongWord; // 最多 15 组 × 4 个 32 位字 Nr: Integer; // 轮数:10 / 12 / 14 end; // 按密钥长度选择轮数并完成扩展 procedure InitKey(var Ctx: TAESContext; const AKey: TBytes); begin case Length(AKey) of 16: Ctx.Nr := 10; 24: Ctx.Nr := 12; 32: Ctx.Nr := 14; else raise EAESError.CreateFmt('密钥长度必须为 16/24/32 字节,当前 %d', [Length(AKey)]); end; ExpandKey(Ctx, AKey); // 内部按 Nk = Length(AKey) div 4 走标准扩展流程 end; // 单块 16 字节加密,所有模式最终都落到这里 procedure EncryptBlock(var Ctx: TAESContext; const AIn: T16Bytes; var AOut: T16Bytes); var Round: Integer; State: T16Bytes; begin State := AIn; AddRoundKey(State, Ctx.RoundKeys, 0); for Round := 1 to Ctx.Nr - 1 do begin SubBytes(State); // 逐字节查 S 盒,唯一的非线性环节 ShiftRows(State); // 第 r 行循环左移 r 字节 MixColumns(State); // GF(2^8) 上的列混合 AddRoundKey(State, Ctx.RoundKeys, Round); end; SubBytes(State); ShiftRows(State); AddRoundKey(State, Ctx.RoundKeys, Ctx.Nr); // 末轮没有 MixColumns,少写就全错 AOut := State; end;InitKey里的长度检查是全线最早的一道防火墙。密钥长度不对,扩展出来的轮密钥数量就不对,后面的加密不会报错,只会静默产出垃圾密文,到 Java 那边才炸。EncryptBlock里最容易被抄错的是最后一轮——标准里第十轮不做MixColumns,很多网上流传的实现漏掉这个细节,加密解密自己配自己能对上,跟标准实现就对不上,测试向量一跑立刻现形。
解密走的是逆变换序列:AddRoundKey→ 逆列混合 → 逆行移位 → 逆 S 盒,轮密钥的取用顺序也要反过来。为了少写一份逆 S 盒,也有实现用等价逆密码(Equivalent Inverse Cipher)改写解密流程,复用同一套轮密钥,代价是可读性变差。我一般选前者,代码长一点,但对得上标准文档,调试时好对照。
3.2 ECB 与 CBC 两种模式的封装
模式层的差别其实只有一句:CBC 每块加密前先跟上一块密文异或。但就是这一句,决定了 IV 要不要传、传在哪、长度对不对。
// ECB:块与块之间完全独立,可以并行,也可以只解其中一块 procedure EncryptECBRaw(var Ctx: TAESContext; const ASrc: TBytes; var ADst: TBytes); var I, Offset: Integer; InBlock, OutBlock: T16Bytes; begin for I := 0 to (Length(ASrc) div 16) - 1 do begin Offset := I * 16; Move(ASrc[Offset], InBlock[0], 16); EncryptBlock(Ctx, InBlock, OutBlock); Move(OutBlock[0], ADst[Offset], 16); end; end; // CBC:OutBlock 复用为「上一块密文」,首块用 IV 顶上 procedure EncryptCBCRaw(var Ctx: TAESContext; const AIV: TBytes; const ASrc: TBytes; var ADst: TBytes); var I, J, Offset: Integer; InBlock, OutBlock: T16Bytes; begin if Length(AIV) <> 16 then raise EAESError.Create('IV 必须为 16 字节'); Move(AIV[0], OutBlock[0], 16); for I := 0 to (Length(ASrc) div 16) - 1 do begin Offset := I * 16; Move(ASrc[Offset], InBlock[0], 16); for J := 0 to 15 do InBlock[J] := InBlock[J] xor OutBlock[J]; EncryptBlock(Ctx, InBlock, OutBlock); Move(OutBlock[0], ADst[Offset], 16); end; end;参数上需要注意ASrc的长度必须已经是 16 的整数倍,这两个过程不做填充,填充放在上一层uAESPadding完成。我这么分层是刻意的:填充逻辑跟模式无关,ECB 和 CBC 用的完全一样,混在一起写,将来想加 PKCS7 或 ZeroPadding 就得改两处。
OutBlock在 CBC 里同时承担“本块密文”和“下一块的异或因子”两个角色,省掉一次 16 字节的Move。这种写法在纯 Pascal 实现里很常见,代价是可读性,第一次读的人容易以为漏了变量。
3.3 PKCS5Padding 与 Base64 的边界处理
填充这段代码不长,但它是最容易在跨语言时出问题的地方,因为 Java 那边的PKCS5Padding对 AES 而言实际执行的是 PKCS7 语义——块长 16 字节,填充值就是 1 到 16。名字叫 PKCS5,行为是 PKCS7,这一点不搞清楚,自己实现时按 8 字节块写就全歪了。
function PKCS5Pad(const AData: TBytes; ABlockSize: Integer): TBytes; var OldLen, PadLen: Integer; begin OldLen := Length(AData); // 恰好整除时也要补满一整块,否则解密端无法判断尾部有没有填充 PadLen := ABlockSize - (OldLen mod ABlockSize); SetLength(Result, OldLen + PadLen); if OldLen > 0 then Move(AData[0], Result[0], OldLen); FillChar(Result[OldLen], PadLen, Byte(PadLen)); end; function PKCS5UnPad(const AData: TBytes): TBytes; var PadLen, I: Integer; begin if Length(AData) = 0 then raise EAESError.Create('密文为空'); PadLen := AData[High(AData)]; if (PadLen = 0) or (PadLen > 16) or (PadLen > Length(AData)) then raise EAESError.Create('填充字节非法,优先怀疑密钥或 IV 不对'); // 逐字节复核,防止把随机数据误判成合法填充 for I := Length(AData) - PadLen to High(AData) do if AData[I] <> Byte(PadLen) then raise EAESError.Create('填充内容不一致'); SetLength(Result, Length(AData) - PadLen); Move(AData[0], Result[0], Length(Result)); end;去填充时“逐字节复核”这一步不是多余的。CBC 用错 IV 解密第一块会得到随机数据,如果只检查最后一个字节是否在 1 到 16 之间,有约 6% 的概率蒙混过关,得到一段长度错误的乱码,反而更难排查。加上复核以后,错密钥、错 IV 基本都会在第一层被拦下来。
Base64 用 Indy 自带的编码器就够了,XE2 安装包自带 Indy 10,不用额外引第三方库:
function BytesToBase64(const AData: TBytes): string; begin // Indy 的 TIdBytes 与 TBytes 是两种动态数组类型,需要显式转换 Result := TIdEncoderMIME.EncodeBytes(TIdBytes(AData)); end; function Base64ToBytes(const AText: string): TBytes; begin Result := TBytes(TIdDecoderMIME.DecodeBytes(AText)); end;TIdBytes和TBytes都是array of Byte,但在 Delphi 里属于不同的动态数组类型,直接传参编译器会报类型不匹配。第一次遇到这个报错时我以为是 Indy 版本问题,折腾了半天。
4. 跑通验证:新建 XE2 工程并和 Java 端对上同一组密文
光看代码不算跑通。判断一个 AES 实现对不对,唯一可靠的办法是拿标准测试向量去撞——撞过了说明算法层没问题,撞不过就老老实实在 Delphi 侧调,别去怀疑 Java。
4.1 建工程、引单元、配 Indy
打开 XE2,File → New → VCL Forms Application,或者干脆建个 Console Application,验证阶段用控制台更省事,不用拖控件。把五个 pas 文件拷到工程目录,在 dpr 的 uses 里加进去,或者在项目管理器里Add进来。
program AESDemo; {$APPTYPE CONSOLE} uses System.SysUtils, uAESUtil, // 对外门面 uAESTestCase; // 测试向量,验证完可以从发布工程里移除 var CipherText, PlainText: string; begin try // 1. 先跑标准向量,确认算法主干没问题 RunAllVectors; // 2. 再做一次业务往返,确认编码层没问题 CipherText := AESEncryptCBC('订单金额=199.00&用户=A同学', '1234567890abcdef', '0001020304050607'); Writeln('密文: ', CipherText); PlainText := AESDecryptCBC(CipherText, '1234567890abcdef', '0001020304050607'); Writeln('明文: ', PlainText); except on E: Exception do Writeln('失败: ', E.ClassName, ' / ', E.Message); end; Readln; end.编译时如果报IdCoderMIME找不到,检查 XE2 的 Indy 组件包有没有装;报TIdBytes无法转换,回到 3.3 那一节看类型转换。这两类报错占了新手第一次编译失败原因的绝大多数。
4.2 用 NIST 测试向量先自证
这组向量出自 NIST SP 800-38A,AES-128,密钥000102...0F,明文块00112233445566778899AABBCCDDEEFF。ECB 和 CBC 的期望密文不一样,正好能验证模式层有没有写对。
procedure TAESTestCase.RunAllVectors; var Key, Plain, IV, Got: TBytes; begin Key := HexToBytes('000102030405060708090A0B0C0D0E0F'); Plain := HexToBytes('00112233445566778899AABBCCDDEEFF'); // ECB 期望值 Got := AESEncryptRawECB(Key, Plain); Check(BytesToHex(Got) = '69C4E0D86A7B0430D8CDB78070B4C55A', 'ECB 向量不匹配'); // CBC 期望值,IV 与密钥同值 IV := Key; Got := AESEncryptRawCBC(Key, IV, Plain); Check(BytesToHex(Got) = '7649ABAC8119B246CEE98E9B12E9197D', 'CBC 向量不匹配'); // 往返自检:加密再解密必须回到原值 Plain := TEncoding.UTF8.GetBytes('中文 + emoji 一起过一遍'); Check(BytesEqual(AESDecryptRawECB(Key, AESEncryptRawECB(Key, Plain)), Plain), 'ECB 往返不一致'); end;AESEncryptRawECB这类函数是不带填充的裸接口,专门为测试准备。业务接口带填充会改变长度,拿来对标准向量永远对不上——这也是一个常见误区:有人拿业务函数去撞 NIST 向量,怎么调都差最后一块。
4.3 Java 侧的对等实现与联调
Delphi 这边向量过了,就轮到 Java 端配合。下面这份是服务端最常见的写法,模式、填充、编码三项必须和 Delphi 侧完全一致:
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.IvParameterSpec; import java.util.Base64; public class AesBridge { // ECB:不需要 IV,适合加密短标识 public static String encryptEcb(String plain, String key) throws Exception { SecretKeySpec spec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, spec); return Base64.getEncoder().encodeToString( cipher.doFinal(plain.getBytes("UTF-8"))); } // CBC:IV 必须 16 字节,且加解密两端一致 public static String encryptCbc(String plain, String key, String iv) throws Exception { SecretKeySpec spec = new SecretKeySpec(key.getBytes("UTF-8"), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes("UTF-8")); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, spec, ivSpec); return Base64.getEncoder().encodeToString( cipher.doFinal(plain.getBytes("UTF-8"))); } }联调顺序我建议固定成三步:先用同一个明文、同一把密钥,在两边各自加密,比较 Base64 字符串是否逐字符相同;再让 Delphi 加密、Java 解密;最后反向来一遍。第一步不一致说明参数没对齐,第二步失败通常是填充或 IV 的锅,第三步失败要检查 Java 用的 Base64 编码器是不是 MIME 变体(Base64.getMimeEncoder()会每 76 字符插换行,这个坑我踩过)。
5. 避坑排查:跨语言对接时最容易翻车的 5 个点
5.1 中文变问号,或者密文长度比预期短一截
现象:英文加解密完全正常,一掺中文,Java 解出来是乱码,或者解出的字符串长度只有原文的一半。
原因:Delphi 侧把 string 一个字符一个字符地Ord()转字节,XE2 的 string 是 Unicode,一个汉字变成两个字节的 Unicode 码元,Java 那边按 UTF-8 解自然对不上。正确做法是统一走TEncoding.UTF8.GetBytes/GetString,别手写字符循环。
解决:把门面函数里所有字符串到字节的转换都换成TEncoding.UTF8,并且在 Java 侧固定用getBytes("UTF-8")和new String(bytes, "UTF-8"),不要依赖平台默认编码。
5.2 密文塞进 URL 或 JSON 之后丢字符
现象:本地测一百次都对,走 HTTP 接口就偶尔解密失败,把密文打印出来比原长度少几个字符。
原因:Base64 的字母表里有+、/、=三个字符。+放在 URL query 里如果不做百分号编码,会被服务端框架当成空格;=在部分拼接逻辑里被截断。JSON 里虽然安全一些,但有些手写的拼接没做转义。
解决:传输前对 Base64 结果做一次 URL 编码,或者改用 URL 安全变体(-和_替换+和/),但两边必须同时改。
5.3 Java 端抛 InvalidKeyException,或者 256 位密钥加密失败
现象:128 位密钥一切正常,换成 32 字节密钥,Java 直接抛异常或抛Illegal key size。
原因:早期 JDK 受出口限制,默认只允许 128 位 AES,256 位需要安装 JCE 无限制强度策略文件;另外密钥长度不是 16/24/32 字节时,SecretKeySpec也会拒绝。
解决:先确认密钥字节数,中文密钥按 UTF-8 算下来经常是 17 或 18 字节,而不是你以为的 16。位数不够就补位或改用哈希派生,别指望两端各自“看着差不多”。
5.4 第一块明文总是解错,后面几块正常
现象:CBC 解密,报文越长错得越奇怪,第一块 16 字节是乱的,后面能读。
原因:IV 不一致。常见于加解密两端其中一个用了全零 IV,或者 IV 被随手写成密钥的前 16 字节,又或者 IV 在传输过程中被截断补齐。
解决:把 IV 和密文绑在一起传,拼在密文前面统一 Base64,接收方按固定长度切分。这样 IV 不可能出现两端不一致的情况。
5.5 偶发“填充非法”,重试又好了
现象:同一个接口,大多数请求成功,少数请求报填充异常,重发一次可能就过了。
原因:多线程共用同一个TAESContext。密钥扩展后的上下文在加密过程中不修改,但很多实现为了省内存会把中间状态也放进去,并发时就串了。另一种情况是全局缓存了Cipher对象,Java 的Cipher本身不是线程安全的。
解决:TAESContext要么每个线程一份,要么在进入加密函数时用局部变量重新InitKey。密钥扩展的开销很小,别为这点性能埋雷。
6. 二次开发:把 AES 单元收成一个能换实现的加解密门面
第一次接手这套单元时,我图省事直接在业务窗体里uses uAESUtil,散着调了三十多个地方。后来需求变了——某几个字段要从 ECB 切到 CBC,还要支持“新老密文都能解”的过渡期。那次改得我很难受,从那以后我每次接类似需求都强制先抽一层接口。
做法很简单,定义一个加解密门面,把模式、IV、编码这些差异关在实现类里,业务代码只认接口:
type IAESCipher = interface ['{6C1F0E52-1B3B-4E2C-9A55-4F2B7E0C0D11}'] function Encrypt(const APlain: string): string; function Decrypt(const ACipherBase64: string): string; function Name: string; end; TAESECBCipher = class(TInterfacedObject, IAESCipher) private FKey: string; public constructor Create(const AKey: string); function Encrypt(const APlain: string): string; function Decrypt(const ACipherBase64: string): string; function Name: string; end; TAESCBCRandomIVCipher = class(TInterfacedObject, IAESCipher) private FKey: string; public constructor Create(const AKey: string); function Encrypt(const APlain: string): string; // 输出 IV + 密文,整体 Base64 function Decrypt(const ACipherBase64: string): string; function Name: string; end;TAESCBCRandomIVCipher.Encrypt内部先生成 16 字节随机 IV,然后拼接、加密、整体 Base64;Decrypt先 Base64 解码,切前 16 字节当 IV,剩下的走 CBC 解密。这样一来接口只传一个字符串,调用方不需要知道 IV 存在过,也就不会有人在业务层传错 IV。
再往上加一层选择逻辑,就能处理过渡期:
function CreateCipherFor(const AScene: string): IAESCipher; begin if AScene = 'legacy' then Result := TAESECBCipher.Create(GetKey('legacy')) else Result := TAESCBCRandomIVCipher.Create(GetKey('default')); end;GetKey从配置里读,别在代码里写死。老密文走legacy,新数据全部走带随机 IV 的 CBC,等老数据自然淘汰完再删掉那个分支。这套路子在很多存量系统改造里都适用,关键在于新旧实现能同时存在,而不是一刀切。
验证方法建议做成自检,放在程序启动时跑一遍,几十毫秒的事:用固定密钥和固定明文加密一次,比对一个硬编码的期望密文。这样换编译器版本、升级 Indy、改动了单元,一启动就知道有没有破坏兼容性,比等到线上发现“新版本发的密文老服务端解不开”要划算太多。
几个可以继续改的方向:把输出编码做成可插拔的(Base64 / Hex / URL 安全 Base64),应对不同后端;把uAESCore里的 S 盒换成运行时生成的逆表,减小 exe 体积;给门面加一个TryDecrypt版本,返回布尔值而不是抛异常,避免解密失败时异常把整个批处理打断。
我现在的习惯是,凡是涉及加解密的模块,交付前一定先写一个测试向量用例加上一组跨语言往返样例,跑通了再谈业务逻辑。这个动作花不了二十分钟,但能省掉后面一整晚的联调。希望帮到你。
本文还有配套的精品资源,点击获取