news 2026/10/15 15:28:21

Delphi XE2 AES加密实战:ECB/CBC模式与PKCS5Padding填充详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Delphi XE2 AES加密实战:ECB/CBC模式与PKCS5Padding填充详解

简介:一套基于Delphi XE2实现的标准AES加解密源码,支持ECB与CBC模式、PKCS5Padding填充,密钥长度覆盖128/192/256位并自动以0补齐,返回结果兼容Base64编码。面向需要与Java后端做加密联调的Delphi开发者,可直接对照在线AES工具验证互通性。压缩包共33个文件,约4.4MB,主要包括Delphi单元文件(pas/dcu)、工程文件(dproj/dpr)、窗体界面(dfm)及可执行示例(exe),另附一份Java测试类(AESTest.java)用于跨语言比对,整体结构清晰,便于快速定位算法核心。资源已有2167人学习使用。源码在原作者基础上完善了Base64输出支持,并处理了XE及以上版本与Delphi7的差异,适合在Delphi XE2及更高版本中集成复用,也可作为学习标准AES算法及Delphi-Java加密互通的参考样例。

1. 先把标题里的“EBC”纠正过来:这条需求真正要你做的三件事

如果你是因为对接文档里写着“AES/EBC/PKCS5Padding-base64”而搜到这篇,那第一件要做的不是找代码,而是先把“EBC”改成“ECB”——这个笔误在真实需求单里反复出现,它对应的是电子密码本模式,不是某种缩写。拿错模式去联调,加密出来的东西对方永远解不开,这是最典型的开局翻车。

这条标题看着像一个加解密小工具,拆开其实是三件事:按AES算法对二进制分组、用ECB或CBC模式处理块与块之间的关系、在PKCS5Padding填充后用base64把密文变成可传输的文本。Delphi XE2恰好是老工程里存量很大、加密代码最容易被翻出来的版本,很多系统补加密需求都落在这上面。

适合读这篇的人有两类:一类是手里维护着XE2老工程、要给现有接口补AES加密的开发者;另一类是拿到一份跨语言联调文档,发现自己写的Delphi代码和对方Java/PHP/Go对不上的工程师。这篇会把标准、可编译代码和踩过的坑一次讲完。

2. AES 标准与 Delphi XE2 的选型:PKCS5Padding 别名、ECB/CBC 差异与库的选择

2.1 一段历史与一个别名:PKCS5Padding 在 AES 里就是 PKCS7Padding

先说名字。PKCS#5 是早期为 DES 这类 8 字节分组的算法设计的填充规范,规则是“缺几个字节就补几个值为几的字节”;后来 PKCS#7 把同一套规则推广到任意分组长度,AES 的分组是固定的 16 字节,所以严格来说应该叫 PKCS7Padding。但 Java 的密码学接口、各类文档里都习惯写 AES/CBC/PKCS5Padding,调用底层时实际执行的还是 PKCS7 的规则——因为 AES 没有 5,只有 7。

在 Delphi 里,没有哪个自带组件能直接说“给我来一个 PKCS5Padding”,填充逻辑必须自己写,而且必须按“16 - (len mod 16)”计算。这里有个老手都容易看漏的点:当明文长度刚好是 16 的整数倍时,PadLen 算出来是 16,也就是要额外补出完整的一块 16 个 0x10;如果这时候手一抖写成if len mod 16 = 0 then PadLen := 0,那解密方收到的数据就少了一块,PKCS5 校验直接失败。这个细节后面避坑章还会再提,这里先立住。

理解了填充,再看标题里的 base64。AES 加密出来的是二进制字节流,可能含不可见字符、换行符、甚至 0x00,直接塞进 JSON、数据库或者 URL 参数都会被截断或转义。所以标准做法是加密完了再包一层 base64,解密时先解 base64 再做 AES。这一层本身不算算法,但它是跨语言联调里最容易出现“明明算法一样却对不上”的地方。

2.2 ECB 与 CBC 的差别,以及标题为什么把 ECB 放在前面

ECB 和 CBC 的差别一句话能说清:ECB 把每一块明文独立加密,块与块之间没有任何关系;CBC 则是让前一块的密文参与下一块的加密,第一个块先和一个叫 IV 的初始向量做异或。这个差别带来一个非常直观的后果——ECB 模式下,相同的明文块必然产生相同的密文块,16 字节的图片或结构化数据加密后,你甚至能从密文轮廓猜出原图;CBC 则因为前块密文参与了异或,相同的明文块在不同位置产生的密文不同。

对比项ECBCBC
是否需要 IV不需要需要,16 字节
相同明文块是否产生相同密文会不会
密文是否泄露明文结构泄露基本不泄露
典型适用场景单块短数据、与老系统兼容通用数据加密、接口传输
Delphi 实现方式EncryptBlock 逐块调用EncryptCBC 一次性处理

那标题为什么把 ECB 放在前面?我见过的大量跨语言需求文档里,ECB 往往是被当作“最简单模式”写在前面,有些甚至是从十几年前的老接口模板里复制下来的,顺手就写成了“EBC”。这不代表推荐你优先用 ECB,只代表你需要先去确认对方到底要什么。安全性上,凡是明文有重复结构、数据超过一个分组的场景,我都建议直接用 CBC,顶多多传一个 IV。

2.3 常见 Delphi 加密库的取舍:DCPcrypt 与 XE2 自带单元的边界

Delphi XE2 这条版本线比较微妙:IDE 里自带了一些密码学相关的单元,能调底层 API,但接口偏老、封装极薄,写起来像在操作指针,网上能查到的可靠资料也少,对大多数业务工程师来说性价比不高。社区里最常用、老工程里出现频率最高的是 DCPcrypt,它把 Rijndael 算法实现成了标准 AES,支持 ECB/CBC/CFB/CTR 多种模式,XE2 32 位下直接加进工程就能编译。

选择 DCPcrypt 我一般基于三条:第一,它的 Rijndael 实现和标准 AES 完全对齐,密钥长度、分组大小都由算法本身保证;第二,它只提供分组加解密的底层操作,不做自动填充,这反而逼着你把 PKCS5Padding 写明白,联调时少一层黑匣子;第三,它在老版本 Delphi 里的兼容性记录很干净,几乎不需要改代码。

需要注意 DCPcrypt 不是“开箱即填”的库,它把填充决策留给你。好处是你能完全掌控字节流;代价是如果忘了填充、或者填充规则和对方有半点不一致,加密能跑通但对方解不开。我这个章节后面给的完整函数,就是把填充、模式、base64 这三层一次封装好,避免每次写都踩一遍。

3. 用 DCPcrypt 在 XE2 里跑通 AES/ECB 与 AES/CBC:完整可编译函数

3.1 准备工作:把 DCPcrypt2 与 DCPrijndael 两个单元接进 XE2 工程

先把库接进来。DCPcrypt 以源码形式分发,解压后里面最重要的两个单元是 DCPcrypt2(基类与通用枚举定义)和 DCPrijndael(AES 算法实现)。常见做法是把整个目录拷到工程目录下,然后在工程里 Add 这两个 .pas;也有的包把基类单元命名为 DCPcrypt.pas,引用时跟着改名字就行。

uses System.SysUtils, System.Classes, System.NetEncoding, DCPcrypt2, DCPrijndael;

这六个单元各管一摊:前两个是 Delphi 基础运行时,System.NetEncoding 提供 base64 编解码,后面两个是 DCPcrypt 的基类和 AES 实现。如果你在单元引用阶段就报找不到 DCPcrypt2,多半是文件没加进工程或者路径没配,先检查 Project->Options->Directories 里的 Search path。

3.2 核心封装:填充、两模式加解密与 base64 输出

下面这段是我在 XE2 工程里实际用的一套封装,一个加密函数一个解密函数,通过 AMode 参数切换 ECB 和 CBC。代码里把 PKCS5Padding、UTF-8 字节转换、base64 输出全做了,你直接抄进单元里就能跑。

function AESEncrypt(const APlainText, AKey, AIV: string; const AMode: TCipherMode): string; var Cipher: TDCP_rijndael; DataBytes, KeyBytes, IVBytes: TBytes; Padded, OutBytes: TBytes; PadLen, KeyLen, I, BlockCount, Offset: Integer; begin // 1. 明文统一转 UTF-8,避免 ANSI 编码差异 DataBytes := TEncoding.UTF8.GetBytes(APlainText); // 2. PKCS5Padding:AES 固定 16 字节一块,整倍数也要补满一块 PadLen := 16 - (Length(DataBytes) mod 16); SetLength(Padded, Length(DataBytes) + PadLen); if Length(DataBytes) > 0 then Move(DataBytes[0], Padded[0], Length(DataBytes)); for I := Length(DataBytes) to Length(Padded) - 1 do Padded[I] := PadLen; // 3. 密钥转 UTF-8,AES-128 取 16 字节;演示代码做了补零/截断 KeyBytes := TEncoding.UTF8.GetBytes(AKey); KeyLen := Length(KeyBytes); if KeyLen < 16 then begin SetLength(KeyBytes, 16); for I := KeyLen to 15 do KeyBytes[I] := 0; end else if KeyLen > 16 then SetLength(KeyBytes, 16); // 4. CBC 模式必须准备 16 字节 IV,ECB 模式忽略 IV if AMode = cmECB then SetLength(IVBytes, 0) else begin IVBytes := TEncoding.UTF8.GetBytes(AIV); if Length(IVBytes) <> 16 then raise Exception.Create('CBC 模式要求 16 字节 IV'); end; Cipher := TDCP_rijndael.Create(nil); try // 5. 先指定模式再初始化密钥,避免部分版本 Reset 时把 IV 清掉 Cipher.CipherMode := AMode; if AMode = cmECB then Cipher.InitKey(KeyBytes[0], 128, nil) else Cipher.InitKey(KeyBytes[0], 128, @IVBytes[0]); SetLength(OutBytes, Length(Padded)); if AMode = cmECB then begin // DCPcrypt 没有现成的 EncryptECB,按块循环调用即可 BlockCount := Length(Padded) div 16; for I := 0 to BlockCount - 1 do begin Offset := I * 16; Cipher.EncryptBlock(Padded[Offset], OutBytes[Offset]); end; end else Cipher.EncryptCBC(Padded[0], OutBytes[0], Length(Padded)); finally Cipher.Free; end; // 6. 二进制密文转 base64,方便 JSON 和数据库传输 Result := TNetEncoding.Base64.EncodeBytesToString(OutBytes); end; function AESDecrypt(const AEncoded, AKey, AIV: string; const AMode: TCipherMode): string; var Cipher: TDCP_rijndael; DataBytes, KeyBytes, IVBytes, OutBytes: TBytes; PadLen, KeyLen, I, ValidLen: Integer; begin // 1. 先解 base64,得到原始密文字节 DataBytes := TNetEncoding.Base64.DecodeStringToBytes(AEncoded); if (Length(DataBytes) = 0) or (Length(DataBytes) mod 16 <> 0) then raise Exception.Create('密文长度必须是 16 的倍数且不能为空'); // 2. 密钥处理与加密侧保持一致 KeyBytes := TEncoding.UTF8.GetBytes(AKey); KeyLen := Length(KeyBytes); if KeyLen < 16 then begin SetLength(KeyBytes, 16); for I := KeyLen to 15 do KeyBytes[I] := 0; end else if KeyLen > 16 then SetLength(KeyBytes, 16); if AMode = cmECB then SetLength(IVBytes, 0) else begin IVBytes := TEncoding.UTF8.GetBytes(AIV); if Length(IVBytes) <> 16 then raise Exception.Create('CBC 模式要求 16 字节 IV'); end; Cipher := TDCP_rijndael.Create(nil); try Cipher.CipherMode := AMode; if AMode = cmECB then Cipher.InitKey(KeyBytes[0], 128, nil) else Cipher.InitKey(KeyBytes[0], 128, @IVBytes[0]); SetLength(OutBytes, Length(DataBytes)); if AMode = cmECB then begin for I := 0 to (Length(DataBytes) div 16) - 1 do Cipher.DecryptBlock(DataBytes[I * 16], OutBytes[I * 16]); end else Cipher.DecryptCBC(DataBytes[0], OutBytes[0], Length(DataBytes)); finally Cipher.Free; end; // 3. 解析 PKCS5 填充:最后一个字节就是填充长度,并逐字节校验 PadLen := OutBytes[Length(OutBytes) - 1]; if (PadLen < 1) or (PadLen > 16) then raise Exception.Create('填充字节非法,请检查 Key/IV/模式'); for I := Length(OutBytes) - PadLen to Length(OutBytes) - 1 do if OutBytes[I] <> PadLen then raise Exception.Create('填充校验失败,密钥或密文有误'); ValidLen := Length(OutBytes) - PadLen; Result := TEncoding.UTF8.GetString(OutBytes, 0, ValidLen); end;

对这段代码有四个关键参数要说明。AMode 直接用 DCPcrypt2 里定义的 cmECB 和 cmCBC 两个枚举值,传给加密和解密函数时必须一致,混合使用会直接解不出来。AKey 在演示代码里做了“不足 16 字节补零、超过 16 字节截断”的处理,这是为了让函数在任何输入下都能跑通;但正式联调时我建议你直接要求密钥精确为 16 字节,并把这段补零逻辑删掉,否则密钥长度不对的时候,错误会被悄悄掩盖。AIV 仅在 CBC 模式下使用,固定 16 字节;ECB 模式下传什么都被忽略,所以我把 ECB 分支的 IVBytes 直接置空,避免误导。

填充和解密校验是这段代码的核心。加密侧先算 PadLen,再在循环里给新增的每个字节赋这个值;解密侧从末尾取一个字节判断填充长度,然后检查最后 PadLen 个字节是否全部等于 PadLen。这个校验不是可选项——密钥错误时,解密出来的是一个随机尾块,有了校验就能直接抛出异常,而不是返回一段乱码让你误以为“解密成功了”。很多线上问题都是因为少了这道校验,把乱码当数据存进了库。

3.3 base64 交换层:为什么密文必须过一遍 EncodeBase64

看完代码你会发现整个流程是“UTF-8 明文 -> AES 密文 -> base64 文本”,顺序不能反。AES 输出的原始字节是二进制,直接放进 JSON 字符串会被转义破坏,放进数据库字段又可能被按字符串截断,就算你把字节硬塞进参数,对方拿到的也未必是原始字节。base64 把这堆二进制重排成 64 个可见字符集,任何文本通道都能无损传输。

XE2 里做 base64 有两套写法,一个是我代码里用的 TNetEncoding.Base64,另一个是 Soap.EncdDecd 单元里的 EncodeBase64 函数。两者结果一样,我推荐前者,因为它在 System 命名空间下,不引入额外的 Soap 依赖。解密时用对应的 DecodeStringToBytes,注意别用 DecodeStringBase64 这种老重载,它返回的是字符串,不是字节数组,吞进去再转字节会出乱码。

base64 还有一个变种坑:标准 base64 包含 +、/、= 三个特殊字符,如果密文要放进 URL 参数或者文件名,/ 和 + 可能被服务端解析掉,这时候需要把 + 换成 -、/ 换成 _,并去掉末尾的 =。大部分接口联调用的是标准 base64,只有当对端明确说“URL safe”时才做替换,这个判断不要自作主张。

4. 与 Java/PHP/Go 联调时最容易翻车的 4 个坑

4.1 “EBC”还是“ECB”:拼写错误让方案从一开始就对不上

现象:对接文档写着“AES/EBC”,你按 ECB 实现,加密、解密在本地都正常,发给对方却全部解密失败。对方接口人也坚持说“就是 EBC,按文档写的”。

原因:ECB 是 Electronic Codebook 的缩写,EBC 是笔误,几乎可以确定是对接文档从旧模板复制时手滑。这类拼写错误危害不在于算法差异,而在于双方都拿着错误的共识去对参数,越对越乱。

解决:第一时间把全套参数写清楚发过去——算法、模式、填充、密钥长度、base64 是否启用。我会直接列一行:“AES/ECB/PKCS5Padding,密钥 16 字节 UTF-8,输出 base64”,让对端逐项确认。这个动作能过滤掉九成以上的拼写和参数误解。

4.2 密钥编码不一致:UTF-8 与老工程默认编码的错位

现象:Delphi 端加密正常,Java 端拿到 base64 后解密报“Given final block not properly padded”。密钥里带了一个中文或特殊符号时必现,纯 ASCII 密钥时反而不出现。

原因:XE2 的 string 默认是 Unicode,但老工程里大量代码用了 AnsiString 或者依赖当前系统代码页取字节。Java 端统一按 UTF-8 取密钥字节,Delphi 端如果用了 ANSI 编码,两边拿到的密钥字节不一样,算法再标准也解不开。明文也是同理,加密前必须和对方确认用 UTF-8 还是 GBK。

解决:所有字节转换统一走 TEncoding.UTF8,代码里取明文、密钥、IV 三个地方都用同一套编码。我在第 3 章函数里写的 GetBytes 全部指定了 UTF-8,就是为了杜绝这类“能加密不能解密”的玄学问题。如果你的老工程里某个环节必须保留 ANSI 字符串,请至少保证密钥和 IV 在传入加解密函数前已经被转成 TBytes。

4.3 长度为 16 的整数倍时,PKCS5 也要补满 16 个字节

现象:解密时报填充错误,或者解密出来的明文尾部多出一串不可见的字符。只发生在明文长度恰好为 16、32、48 字节的时候。

原因:明文长度正好是 16 的倍数时,PKCS5 规则要求补一个完整块,即 16 个 0x10。有些实现者会想“都满了还补什么”,于是跳过填充直接加密;解密方却按标准去掉末尾 16 字节,结果要么报错要么丢内容。

解决:填充逻辑里不写任何“整倍数就不补”的例外分支。我代码里 PadLen 的计算是16 - (Length(DataBytes) mod 16),当 mod 结果为 0 时 PadLen 是 16,天然补满一块。这行公式是 PKCS5 的唯一正确写法,任何试图“优化”它的分支都是在给联调埋雷。

4.4 CBC 的 IV 固定不变:能通但迟早出问题

现象:CBC 模式联调全通,功能上线后安全测试不过,报告里写着“IV 固定导致重放攻击”。另一个更隐蔽的现象是:同一个明文用同一个密钥加密多次,密文完全相同,CBC 的安全性优势完全失效。

原因:CBC 的安全设计前提是 IV 每次随机,IV 固定会让 CBC 退化成确定性加密,攻击者可以重放请求、比对密文块推断明文结构。很多联调文档为了图省事把 IV 写成固定字符串,两边代码都能跑,但漏洞是实打实的。

解决:密钥不变,IV 每次加密随机生成,并把 16 字节 IV 拼在密文前一起传输;解密时从密文中截出前 16 字节做 IV,剩下的才是真正密文。这样对端不需要额外约定 IV,只靠密文本身就能解开。这个方案我下一章会给一个最小实现,属于安全性和易用性兼顾的常用做法。

5. 把 Key/IV/Padding 收敛成一份配置,顺便留一组自测用例

5.1 一个最小配置结构与随机 IV 方案

联调最怕的不是算法写不出来,而是参数散落在调用方手里,这个函数传一个 Key,那个函数传一组固定 IV,出了事谁能查清楚。我现在的习惯是建一个 TAESConfig 记录体,把算法层面的决定一次性收敛起来,业务代码里只传明文和密文。

type TAESConfig = record Key: TBytes; // 固定 16 字节,UTF-8 UseRandomIV: Boolean; // True 时自动生成随机 IV,False 时用 FixedIV FixedIV: TBytes; // UseRandomIV=False 时使用 end;

如果 UseRandomIV 为 True,加密时生成随机 16 字节,输出密文前先拼上这 16 字节,解密时前 16 字节当 IV、后面才是密文。这样对端拿到的是一条完整的 base64 串,不需要额外传 IV 参数;风险只是对端必须知道“密文前 16 字节是 IV”这个约定,在文档里写清楚就行。用固定的 Key 加密多份数据时,这个方案能让密文全部不同,同时也让联调参数从“四个字段”减到“一个密钥”。

5.2 自测用例写法与联调核对顺序

代码抄过去,第一步先做本地自测,别直接连对端。我的做法是固定一组输入:明文Hello AES、密钥0123456789abcdef、CBC 模式、固定 IV0000000000000000,加密后立刻解密比对是否还原;然后跑 16 字节整倍数的明文(比如 32 个A),验证填充边界没问题;最后再跑一次空字符串,确保空输入不会崩。

自测通过后,再和对端做交叉验证:你加密一段固定明文,把 base64 结果发给对方解;对方加密一段固定明文,发给你解。这个顺序能快速隔离问题——如果你能解对方、对方不能解你,多半是填充或 base64 的细节;如果两边互相解不开,先回去核对密钥和 IV 的编码,那才是真正的血泪经验区。

就这一点,我吃过亏的教训是:永远不要把“本地加密再解密成功”当成联调通过。本地自测只能证明代码自洽,证明不了字节流兼容。我现在的收尾动作永远是拿对端给的密文做一次真实解密,通过了才算数。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 15:28:21

凸优化帧间预测:11种可解释运动估计方法实战指南

简介&#xff1a;本资源是面向图像处理与计算机视觉方向高年级本科生及研究生的凸优化实践项目&#xff0c;聚焦视频帧间预测这一核心运动估计任务&#xff0c;系统实现并对比了十一种主流算法方案。资源涵盖运动估计&#xff08;穷尽块匹配、三步搜索法、分层块匹配&#xff0…

作者头像 李华
网站建设 2026/10/15 15:28:18

ABP框架示例项目深度解析:模块化、工作单元与实战避坑

简介&#xff1a;面向ASP.NET开发者与Abp框架初学者&#xff0c;这份名为FirstAbp的示例项目是理解ASP.NET Boilerplate&#xff08;Abp&#xff09;核心用法的实用入口。Abp作为开源框架&#xff0c;内置分层架构、模块化设计&#xff0c;并提供身份验证、授权、多租户、动态W…

作者头像 李华
网站建设 2026/10/15 15:28:00

构建可验证知识库:从Wikidata建模到LLM校验的实战路径

简介&#xff1a;本资源是一个轻量级百度百科模仿系统&#xff0c;面向Web开发初学者、知识平台搭建爱好者及开源项目实践者&#xff0c;旨在帮助用户快速部署具备词条创建、编辑与浏览功能的本地化百科网站&#xff0c;解决个人或小团队知识库建设需求。压缩包为ZIP格式&#…

作者头像 李华
网站建设 2026/10/15 15:27:47

SAP供应商寄售机制详解:库存权与结算权分离的实时协同模式

简介&#xff1a;本资源是一份面向SAP MM模块初学者与实施顾问的供应商寄售全流程实操指南&#xff0c;聚焦采购与库存管理中的寄售业务场景&#xff0c;系统解决寄售配置、主数据维护、收料结算及常见报错处理等核心问题。文档以清晰步骤关键事务码&#xff08;如ME11、MIGO、…

作者头像 李华
网站建设 2026/10/15 15:27:46

异步Rust中的Sink:从trait协议到反压与实现详解

写异步 Rust 写久了你会发现&#xff0c;真正折磨人的往往不是 borrow checker&#xff0c;而是那些一眼看到底、用起来却到处是暗坑的 stream 概念。Stream 是读&#xff0c;Sink 是写&#xff0c;二者一进一出&#xff0c;组成了异步 IO 世界里最基础的骨架。futures-rs 里的…

作者头像 李华