Delphi AES加解密核心实现与嵌入式联调实战
2026/6/6 12:12:44 网站建设 项目流程

1. 项目概述:一个Delphi AES加解密的“瑞士军刀”

最近在调试一个嵌入式设备与上位机的安全通信协议,核心需求是数据在传输前必须经过可靠的加密。协议栈跑在资源受限的MCU上,而负责配置和监控的上位机软件则是用经典的Delphi 7开发的。市面上虽然有很多现成的加密库,但要么太臃肿,要么授权协议复杂,要么和MCU那边用C语言写的轻量级AES算法对不上。为了保证两端加解密结果完全一致,避免“鸡同鸭讲”的尴尬,我决定自己动手,用Delphi实现一个与嵌入式端算法匹配的AES加解密核心,并封装成一个直观的测试程序。这个程序不仅仅是个演示,更像是一把“瑞士军刀”,能用来验证算法正确性、测试不同模式、甚至作为后续项目加密模块的基石。

对于嵌入式、物联网开发,或者任何涉及软硬件数据安全交互的工程师来说,自己掌控核心加密流程是常有的事。你可能在STM32上写好了AES-128的ECB模式加密,但到了PC端用某个库解密却发现乱码。问题往往出在密钥调度、字节序、或者填充方式这些细节上。这个用Delphi写的测试程序,正是为了解决这种“对齐”问题而生。它剥离了复杂的界面和无关功能,直指AES算法的核心调用流程,让你能清晰地看到每一步数据的变化,特别适合开发阶段的调试、算法验证和教学演示。无论你是Delphi开发者想集成加密功能,还是嵌入式工程师需要配套的上位机工具,这个简洁直接的例子都能提供一个可靠的起点。

2. 核心思路与AES单元设计解析

2.1 为什么选择纯Pascal实现AES?

在项目初期,我评估了几个方案:调用Windows的CryptoAPI、使用开源的Delphi加密库(如LockBox)、或者直接引入C语言编译的DLL。最终选择用纯Object Pascal从头实现AES算法,主要基于以下几点考量:

  1. 绝对的控制权与一致性:这是最重要的原因。嵌入式端的C代码是我根据标准AES算法编写的,为了确保从密钥扩展、字节替换到列混淆每一个步骤都完全一致,必须拥有算法的完整源代码。使用外部黑盒库,一旦出现解密失败,排查将是噩梦。自己实现的单元,我可以逐行调试,亲眼看到中间状态,与MCU端的调试输出进行比对。
  2. 零依赖与可移植性:纯Pascal代码编译后就是一个独立的.dcu或直接链接进exe,无需携带额外的DLL文件或担心目标机器没有特定的系统组件。这对于需要分发到不同环境的上位机软件来说,部署起来非常干净。
  3. 资源透明与可优化:我知道我的数据块大小(通常是16字节的倍数),通信频率也不高。自己实现的算法可以避免大型通用库带来的额外开销,我可以根据实际情况选择只实现AES-128(密钥长度128位)来减小代码体积,毕竟我的MCU也只支持AES-128。
  4. 深入理解算法:对于安全相关功能,知其然更要知其所以然。亲手实现一遍AES的SubBytes、ShiftRows、MixColumns和AddRoundKey,对理解其安全强度和潜在弱点有巨大帮助。这不仅是完成一个功能,更是一次宝贵的学习过程。

基于此,我设计的AES单元目标非常明确:提供一个与常见嵌入式C语言AES实现兼容的、接口极其简洁的核心操作集,专注于ECB(电子密码本)模式,因为这是我当前项目硬件端支持的模式。更复杂的模式如CBC(密码分组链接)可以在应用层基于这个核心去构建。

2.2 核心函数接口设计哲学

我暴露给外部的函数只有五个,这体现了“单一职责”和“分层清晰”的设计思想:

procedure aesKey(key: PByteArray; len: Integer); procedure aesEncInit; procedure aesEncrypt(buffer, chainBlock: PByteArray); procedure aesDecInit; procedure aesDecrypt(buffer, chainBlock: PByteArray);
  • aesKey:这是起点。它接受一个指向密钥数组的指针和密钥长度。内部它会根据这个原始密钥,执行复杂的密钥扩展算法,生成每一轮加密所需的轮密钥。这个计算过程相对耗时,所以设计为一次设置,多次使用。
  • aesEncInit/aesDecInit:初始化函数。在调用具体的加密/解密函数前,必须调用一次。它们的作用主要是重置内部状态(比如对于CBC模式,这里会初始化初始化向量IV)。在我的ECB基础版本中,这个函数可能只是简单地设置一个标志位或重置计数器,为后续的块处理做好准备。这是一个良好的习惯,为未来扩展其他模式留出了钩子。
  • aesEncrypt/aesDecrypt:核心的块处理函数。它们一次处理一个16字节的数据块。参数buffer是输入也是输出(原地操作),chainBlock在ECB模式下其实没有被使用(可以传nil),但在CBC模式下,它指向上一个密文块,用于异或操作。这样的参数设计保持了接口的统一性。

注意:这里有一个关键点,PByteArray通常定义为array[0..15] of Byte的指针。这意味着调用者必须确保传入的缓冲区至少有16字节有效数据。这种设计效率高,但也把边界检查的责任交给了调用者。

这种“设置密钥 -> 初始化 -> 循环处理块”的流程,是底层加密库的典型模式。它把控制权完全交给了开发者,上层可以灵活地组织数据分块、处理填充、选择加密模式。

3. 加密解密流程的详细拆解与实操

3.1 数据准备:填充与分块

AES是块加密算法,一次处理16个字节(128位)。但我们的原始数据长度几乎不可能是16的整数倍,因此填充是必不可少的一步。在我的测试程序中,我采用了最常用的PKCS#7填充方式。

PKCS#7填充规则:假设最后一个块缺少n个字节(1 <= n <= 16),那么就填充n个值为n的字节。例如,如果最后一块缺3字节,则填充03 03 03。如果数据长度恰好是16的倍数,则需要额外添加一个完整的填充块,内容为16个0x10

// 一个简单的PKCS#7填充函数示例 function AddPKCS7Padding(const Input: TBytes): TBytes; var OriginalLen, PaddingLen: Integer; begin OriginalLen := Length(Input); PaddingLen := 16 - (OriginalLen mod 16); if PaddingLen = 0 then PaddingLen := 16; // 需要填充一个完整块 SetLength(Result, OriginalLen + PaddingLen); Move(Input[0], Result[0], OriginalLen); // 拷贝原始数据 FillChar(Result[OriginalLen], PaddingLen, PaddingLen); // 填充 end;

填充完成后,数据就被分割成若干个16字节的块,可以送入加密函数进行循环处理。

3.2 加密操作的完整代码流程

下面结合代码,详细说明从原始字符串到加密结果的每一步。假设我们要加密字符串"Hello, AES!"

var key: array[0..15] of Byte; // AES-128密钥,16字节 plainText, paddedData, encryptedData: TBytes; i, blockCount: Integer; pBlock: PByteArray; begin // 1. 准备密钥 (例如,用一个简单的字符串转换,实际应用应从安全源获取) // 注意:密钥必须是够随机,这里仅为演示。 FillChar(key, SizeOf(key), 0); StrPCopy(PAnsiChar(@key[0]), 'MySecretKey12345'); // 确保密钥长度足够 // 2. 设置密钥到AES核心 aesKey(@key, 16); // 第二个参数是密钥字节长度,16对应AES-128 // 3. 准备明文并填充 plainText := TEncoding.UTF8.GetBytes('Hello, AES!'); paddedData := AddPKCS7Padding(plainText); // 4. 初始化加密状态 aesEncInit; // 5. 分配空间给密文(长度与填充后明文相同) SetLength(encryptedData, Length(paddedData)); // 6. 分块加密 blockCount := Length(paddedData) div 16; for i := 0 to blockCount - 1 do begin // 指向当前要加密的明文块 pBlock := @paddedData[i * 16]; // 调用加密函数。对于ECB模式,chainBlock参数传nil。 aesEncrypt(pBlock, nil); // 将加密后的结果(pBlock已被原地修改)拷贝到密文数组 Move(pBlock^, encryptedData[i * 16], 16); end; // 此时,encryptedData 中就是AES-128-ECB加密后的密文。 // 通常我们会将其转换为Base64或Hex字符串以便查看和传输。 Memo1.Lines.Add('密文 (Hex): ' + BytesToHex(encryptedData)); end;

关键点解析

  • aesEncrypt是原地操作。传入的pBlock指针指向的数据在函数返回后就被修改为密文。因此我们需要在加密前将明文块拷贝到pBlock指向的地址,或者像上面一样,直接对填充后的数组进行操作,然后再将结果转移到密文数组。上述代码选择后者更清晰。
  • ECB模式每个块独立加密,因此chainBlocknil。如果实现CBC模式,chainBlock需要指向上一个密文块,并且在循环中不断更新。

3.3 解密操作与填充移除

解密是加密的逆过程,但顺序和细节上有讲究。

var cipherText: TBytes; // 假设这是接收到的密文 decryptedPaddedData, decryptedData: TBytes; i, blockCount, paddingLen: Integer; pBlock: PByteArray; begin // 0. 前提:密钥已经用相同的aesKey过程设置过 // aesKey(@key, 16); // 如果上下文已设置,则无需重复 // 1. 初始化解密状态 aesDecInit; // 2. 分配空间给解密后的数据(带填充) SetLength(decryptedPaddedData, Length(cipherText)); // 3. 分块解密 blockCount := Length(cipherText) div 16; for i := 0 to blockCount - 1 do begin pBlock := @cipherText[i * 16]; // 注意:aesDecrypt也是原地操作。为了不破坏原始密文,可以先拷贝。 // 这里为简化,假设cipherText可被修改。安全起见应先拷贝块数据。 aesDecrypt(pBlock, nil); Move(pBlock^, decryptedPaddedData[i * 16], 16); end; // 4. 移除PKCS#7填充 paddingLen := decryptedPaddedData[High(decryptedPaddedData)]; // 获取最后一个字节的值 // 验证填充有效性(重要!防止Padding Oracle攻击的初级措施) if (paddingLen < 1) or (paddingLen > 16) then raise Exception.Create('无效的填充数据!'); for i := High(decryptedPaddedData) downto High(decryptedPaddedData) - paddingLen + 1 do begin if decryptedPaddedData[i] <> paddingLen then raise Exception.Create('填充数据损坏!'); end; // 计算原始数据长度并拷贝 SetLength(decryptedData, Length(decryptedPaddedData) - paddingLen); Move(decryptedPaddedData[0], decryptedData[0], Length(decryptedData)); // 5. 得到原始明文 Memo1.Lines.Add('解密明文: ' + TEncoding.UTF8.GetString(decryptedData)); end;

重要安全提示:在实际通信中,解密后的填充验证必须进行,且一旦验证失败,应返回一个通用的错误信息,而不是具体的“填充错误”。直接暴露填充正确与否的信息,可能为“Padding Oracle攻击”提供便利,攻击者可以利用这一点逐步推算出密钥或明文。生产环境应使用经过严格审计的加密库或采用认证加密模式(如GCM)。

4. 测试程序构建与关键功能实现

4.1 开发环境与界面布局

我使用Delphi 7进行开发,因其轻量、稳定,且生成的二进制文件兼容性极佳。测试程序的界面设计追求极简和功能清晰,主要包含以下几个区域:

  1. 密钥输入区:一个TEdit框,让用户输入文本形式的密钥,旁边有一个按钮可以生成随机密钥。底层程序会将其转换为16/24/32字节的二进制数组。为了直观,同时显示密钥的Hex字符串。
  2. 明文/密文输入区:两个TMemo控件,一个用于输入待加密的明文,一个用于显示加密后的密文(或输入待解密的密文)。密文默认以Hex格式显示,并提供Base64格式的切换选项。
  3. 操作按钮区加密解密清空按钮。点击加密,程序自动对明文进行PKCS#7填充、分块加密,并将结果Hex显示在密文框。点击解密则执行相反流程。
  4. 信息显示区:一个TStatusBarTMemo,用于显示操作日志,如“加密完成,共处理XX字节,分为YY块”、“解密成功,移除填充ZZ字节”等,这对调试非常有帮助。
  5. 模式选择(进阶):一个TRadioGroup,提供ECBCBC模式的选择。如果选择CBC,还需要一个输入框用于设置16字节的初始化向量。

4.2 核心按钮事件代码剖析

以“加密”按钮的OnClick事件为例,它串联起了之前的所有步骤:

procedure TFormMain.btnEncryptClick(Sender: TObject); var sKey, sPlainText: string; keyBytes, plainBytes, paddedBytes, cipherBytes: TBytes; iv: array[0..15] of Byte; // CBC模式用 i, blockCount: Integer; pBlock, pChain: PByteArray; begin // 1. 获取并处理密钥 sKey := edtKey.Text; if Length(sKey) < 16 then // 密钥不足时自动补零或报错,这里简单补零演示 sKey := sKey + StringOfChar(#0, 16 - Length(sKey)); SetLength(keyBytes, 16); Move(PAnsiChar(AnsiString(sKey))^, keyBytes[0], 16); aesKey(@keyBytes[0], 16); // 2. 获取明文并转换 sPlainText := memoPlainText.Text; plainBytes := TEncoding.UTF8.GetBytes(sPlainText); // 3. PKCS#7填充 paddedBytes := AddPKCS7Padding(plainBytes); // 4. 根据模式初始化 if rgMode.ItemIndex = 1 then // CBC模式 begin // 获取或生成IV。演示中从固定字符串生成。 FillChar(iv, 16, 0); StrPCopy(PAnsiChar(@iv[0]), 'MyInitVector1234'); aesEncInit; // 在基础单元中,aesEncInit可能内部使用了IV pChain := @iv; // 第一块的chainBlock是IV end else // ECB模式 begin aesEncInit; pChain := nil; end; // 5. 分配密文空间并分块处理 SetLength(cipherBytes, Length(paddedBytes)); blockCount := Length(paddedBytes) div 16; for i := 0 to blockCount - 1 do begin pBlock := @paddedBytes[i * 16]; // 如果是CBC模式,在加密前需要先将明文块与chainBlock异或 if rgMode.ItemIndex = 1 then begin XorBlock(pBlock, pChain, 16); // 自定义的异或函数 aesEncrypt(pBlock, nil); pChain := pBlock; // 当前密文块成为下一块的chainBlock end else // ECB模式 begin aesEncrypt(pBlock, nil); end; Move(pBlock^, cipherBytes[i * 16], 16); end; // 6. 显示结果 memoCipherText.Text := BytesToHex(cipherBytes); StatusBar1.Panels[0].Text := Format('加密完成。明文%d字节,密文%d字节。', [Length(plainBytes), Length(cipherBytes)]); end;

这个事件处理函数完美展示了如何将底层的、面向块的AES核心函数,与上层的、面向应用的数据处理(编码、填充、模式)结合起来。

4.3 扩展功能:CBC模式的实现要点

在基础ECB模式上增加CBC支持,主要修改在于chainBlock参数的使用和每一块处理前后的异或操作。

  1. 加密时
    • 第一块明文先与初始化向量IV异或,然后加密。
    • 加密后的结果(第一块密文)作为下一块明文的“链块”。
    • 后续每一块明文在加密前,都需要先与前一块的密文异或。
  2. 解密时
    • 流程相反。第一块密文先解密,解密后的结果再与IV异或,得到第一块明文。
    • 后续每一块密文解密后,需要与前一块密文(注意,是前一块密文,不是前一块明文)异或,才能得到当前块明文。

这就要求在加解密循环中,需要维护一个指向chainBlock的指针,并在每次处理后更新它。这解释了为什么我的函数接口中保留了chainBlock参数,即便在基础的ECB版本中它未被使用——这是为未来扩展预留的设计。

5. 调试心得与常见问题排查实录

在开发和调试这个AES单元及测试程序的过程中,我踩过不少坑,也总结出一些让算法正确跑起来的“秘籍”。

5.1 确保两端算法完全一致

这是嵌入式与PC联调中最常见也最头疼的问题。双方都报告“加解密成功”,但数据对不上。请按以下清单逐项核对:

  1. AES变体:双方使用的是AES-128, AES-192还是AES-256?密钥长度必须一致。
  2. 密钥本身:确保密钥的字节序列完全一致。一个常见的错误是字符串编码问题。PC端用UTF-8将字符串转字节,而嵌入式端可能用ASCII或UTF-16。最佳实践是双方都使用Hex或Base64编码的密钥字符串,解码为二进制数组后再使用。
  3. 加密模式:ECB、CBC、CTR?模式不同,结果天差地别。我一开始就固守ECB,因为简单且硬件支持。
  4. 初始化向量:如果使用CBC等模式,IV必须一致。通常IV不需要保密,但必须随机或按约定生成,并在通信开始时同步。
  5. 填充方式:PKCS#7、ANSIX.923、ZeroPadding?填充不一致,解密后移除填充时会失败。和密钥一样,约定一种并严格执行。
  6. 字节序:AES算法本身是面向字节的,通常不存在大小端问题。但如果你在传输或处理多字节数据(如整数)时涉及加解密,则需要统一字节序。

我的调试方法:我创建了一个“黄金向量”测试。在PC端,用一个固定的密钥(如全零)和固定的明文(如全零),进行加密,得到一组密文。然后,在嵌入式端的代码中,用同样的密钥和明文加密,通过调试器或串口打印出每轮加密后的中间状态(State矩阵),与PC端我单元计算的中间状态进行比对。一旦发现某一轮输出不一致,问题就锁定在那一轮的运算上(通常是SubBytes的S盒或MixColumns的矩阵乘法)。

5.2 Delphi实现中的特定陷阱

  • 数组边界与指针PByteArray和动态数组TBytes要小心转换。确保指针指向的内存范围有效。在循环中计算@paddedBytes[i * 16]时,要确保i * 16 + 15不会越界。
  • 内存覆盖aesEncrypt是原地操作。如果你需要保留原始明文,务必在加密前将数据拷贝到临时缓冲区。我的测试程序直接操作填充后的数组,是因为之后不再需要它。
  • 字符串与二进制数据TMemo中存放的是文本,而加密操作的是字节。使用TEncoding.UTF8.GetBytes/GetString进行转换是可靠的方式。显示时,用Hex或Base64,避免直接显示二进制数据(可能包含不可打印字符,导致显示乱码甚至截断)。
  • 密钥管理:测试程序中硬编码或简单输入密钥是极不安全的。这仅用于演示和调试。真实产品中,密钥需要通过安全的方式协商、存储和传输,例如使用非对称加密(RSA/ECC)来保护对称密钥(AES密钥)的交换。

5.3 性能考量与优化建议

这个纯Pascal实现用于偶尔的数据加密或调试绰绰有余,但如果需要加密大量数据(如文件流),可能会成为瓶颈。以下是一些优化思路:

  1. 内联关键函数:将SubBytesShiftRows等频繁调用的小函数声明为inline,减少调用开销。
  2. 使用查表法:AES的MixColumns等操作可以预先计算并存储在常量表中,用查表和异或代替复杂的有限域运算,能极大提升速度。许多优化的C语言实现都采用此法。
  3. 汇编优化:对于最核心的循环,可以使用内嵌汇编代码来优化,特别是异或、移位等操作。Delphi对此支持良好。
  4. 并行处理:在ECB模式下,各个数据块加密独立,理论上可以并行计算。但对于简单的测试程序,这点通常不需要考虑。

实操心得:在项目初期,不要过早优化。首先追求正确性和清晰度。当功能稳定,并且性能测试确实表明加密成为瓶颈时,再针对热点代码进行优化。我的这个单元,首要目标是“正确”和“与硬件匹配”,在满足这两点后,目前的性能对于配置指令、传输少量日志等场景已经完全足够。

这个Delphi AES测试程序,就像一把精心调校的螺丝刀,它不华丽,但能精准地解决特定问题。通过亲手实现和封装,你不仅获得了一个工具,更深入理解了数据如何在加密算法中流动、变换。当你的加密数据在串口、网络或无线信号中穿梭,被另一端的设备完美还原时,那种成就感,是直接调用一个第三方库所无法比拟的。希望这个详细的拆解和代码示例,能为你自己的安全通信项目铺平道路。如果在集成过程中遇到任何“对不上”的情况,回头仔细检查那份“一致性核对清单”,十有八九能找到答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询