1. 项目概述为什么在Delphi中实现AES如果你是一位Delphi开发者无论是维护着庞大的遗留系统还是开发新的桌面或服务端应用数据安全都是一个绕不开的话题。最近几年我接手和评审过不少项目发现很多涉及敏感信息比如用户配置、本地缓存、通信报文的处理还停留在简单的Base64编码甚至自定义XOR的阶段这无异于把家门钥匙放在脚垫下面。当客户或安全审计提出加密需求时选择一种可靠、标准、高效的对称加密算法就成了当务之急。AES高级加密标准无疑是这个场景下的“标准答案”。AES作为全球通用的加密标准其安全性和性能经过了最严苛的考验。在Delphi中实现它并不是为了学术研究而是解决非常实际的工程问题如何安全地存储本地数据库的连接字符串如何确保通过网络传输的配置文件不被篡改如何对用户的一些隐私设置进行加密存储这些场景都要求一个轻量级、无需依赖庞大第三方库的本地加密方案。虽然网上能找到一些现成的控件或单元但直接拿来用往往心里没底不知道里面有没有“后门”或者遇到一些冷门的模式如CFB、OFB就不支持了。自己动手实现一遍不仅是为了用更是为了懂。懂了原理你才能正确地选择密钥长度、工作模式、填充方式才能在出问题时快速定位是加密环节还是传输、存储环节的毛病。这个项目就是基于这样的背景抛开复杂的加密库从最底层的算法描述出发在Delphi中一步步实现AES-128/192/256加解密。我们会聚焦于最常用的ECB和CBC模式并实现PKCS7填充。最终得到一个纯净的、可独立使用的TAES类你可以直接把它扔进你的项目里用几行代码解决数据加密的需求。更重要的是通过这个过程你会彻底明白AES的SubBytes、ShiftRows、MixColumns和AddRoundKey都在干什么下次再遇到“AES加密结果为什么和在线工具不一样”这种问题时你就能从容应对了。2. AES算法核心原理与Delphi实现难点在动手写代码之前我们必须先搞清楚AES到底在干什么。很多开发者觉得加密算法高深莫测其实它的核心步骤是固定且清晰的关键在于理解其设计的精妙之处以及如何在Delphi中高效、正确地表达这些数学运算。2.1 AES算法流程速览AES是一种分组加密算法它把明文分成固定128位16字节的块然后在一个由密钥扩展出来的轮密钥数组辅助下对这个块进行多轮迭代的混淆和扩散操作。密钥长度可以是128、192或256位对应的加密轮数分别为10、12、14轮。每一轮操作除了最后一轮稍有不同都包含四个基本步骤SubBytes字节替换 这是一个非线性变换通过一个称为S盒的查找表将状态矩阵中的每一个字节替换成另一个字节。这是算法提供混淆性的主要来源。S盒的设计基于有限域上的乘法逆运算和仿射变换确保了其良好的非线性特性。在实现上我们就是准备一个256字节的常量数组直接查表完成。ShiftRows行移位 这是一个线性变换将状态矩阵的每一行进行循环左移。第0行不移位第1行左移1字节第2行左移2字节第3行左移3字节。这个操作增加了字节之间的扩散程度。MixColumns列混合 这是算法中最复杂的步骤也是提供强扩散性的关键。它将状态矩阵的每一列视为有限域GF(2^8)上的一个多项式与一个固定的多项式c(x)进行模x^41乘法。这个操作使得输入的每一个字节都会影响到该列的四个输出字节。计算可以通过查表预计算好的T盒来大幅加速。AddRoundKey轮密钥加 非常简单就是将当前的状态矩阵与当前轮的轮密钥进行逐字节的异或XOR操作。轮密钥是从原始密钥通过密钥扩展算法派生出来的。加密过程以一次初始的AddRoundKey开始然后进行Nr-1轮完整的四步操作最后一轮则省略MixColumns步骤。解密过程是加密的逆过程使用逆变换和逆轮密钥。2.2 Delphi实现中的三个关键挑战在理解了流程后用Delphi实现会遇到几个典型难点挑战一有限域运算MixColumns和其逆运算InvMixColumns的核心是有限域GF(2^8)上的乘法和加法。这里的加法和乘法与我们熟悉的整数运算完全不同。加法就是异或XOR而乘法则复杂得多需要模一个不可约多项式m(x) x^8 x^4 x^3 x 1对应十六进制0x11B。手动实现这个乘法效率很低。解决方案是预计算。我们可以预先计算好与固定系数{02},{03},{09},{0B},{0D},{0E}的乘法结果做成查找表。在代码中GMul2、GMul3等函数就是干这个的它们通过判断字节的最高位判断是否溢出和移位、异或操作来快速完成计算。挑战二状态矩阵的表示与操作AES操作的对象是一个4x4的字节矩阵状态。在C语言中用二维数组很自然。在Delphi中我们可以用array[0..3, 0..3] of Byte来表示。但更高效的做法是使用一维数组array[0..15] of Byte然后通过索引映射来模拟行列操作。例如状态矩阵中第r行第c列的元素在一维数组中的索引是r 4*c。这种表示法在内存中是连续的对于需要批量操作的步骤如整列处理有时更友好。我们的实现中会灵活运用这两种表示法。挑战三性能与可读性的平衡纯查表法使用巨大的T盒速度最快但代码体积大且可能掩盖算法逻辑。而完全动态计算每一步则速度慢。一个折中的方案是SubBytes和ShiftRows使用查表简单循环MixColumns使用预计算的有限域乘法函数。这样既保证了可接受的性能又让代码逻辑清晰便于调试和学习。对于绝大多数桌面应用这个性能已经绰绰有余。注意在实现SubBytes时S盒和逆S盒是固定的常量数组必须确保其数据完全正确。一个字节的错位都会导致加解密完全失败。建议直接从权威标准文档如FIPS PUB 197中复制这些常量数组而不是从网上随意拷贝。3. 核心模块设计与代码实现解析接下来我们进入实战环节一步步构建我们的TAES类。我会先给出核心接口设计然后深入关键函数的实现细节并解释为什么这么做。3.1 类结构与数据定义我们首先定义一个TAES类它封装了所有的加解密操作。为了支持不同的密钥长度和模式我们使用枚举来定义。type TAESKeySize (ks128, ks192, ks256); // 密钥长度枚举 TAESMode (mECB, mCBC); // 工作模式枚举 TAES class private FKey: TBytes; // 原始密钥 FExpandedKey: TBytes; // 扩展后的轮密钥 FKeySize: TAESKeySize; // 密钥长度 FMode: TAESMode; // 工作模式 FIV: TBytes; // CBC模式需要的初始化向量 FRounds: Integer; // 加密轮数根据密钥长度计算 // 核心内部函数 procedure KeyExpansion; // 密钥扩展 function SubBytes(state: TBytes; IsInv: Boolean): TBytes; // 字节替换 function ShiftRows(state: TBytes; IsInv: Boolean): TBytes; // 行移位 function MixColumns(state: TBytes; IsInv: Boolean): TBytes; // 列混合 function AddRoundKey(state, roundKey: TBytes): TBytes; // 轮密钥加 function EncryptBlock(const block: TBytes): TBytes; // 加密一个块 function DecryptBlock(const block: TBytes): TBytes; // 解密一个块 // 辅助函数 class function GMul2(a: Byte): Byte; static; // 有限域乘2 class function GMul3(a: Byte): Byte; static; // 有限域乘3 // ... 其他GMul函数 public constructor Create(const AKey: TBytes; AKeySize: TAESKeySize; AMode: TAESMode mECB; const AIV: TBytes nil); function Encrypt(const data: TBytes): TBytes; // 加密数据 function Decrypt(const data: TBytes): TBytes; // 解密数据 class function PKCS7Pad(const data: TBytes; blockSize: Integer): TBytes; static; // PKCS7填充 class function PKCS7Unpad(const data: TBytes): TBytes; static; // PKCS7去填充 end;关键点解析我们将FExpandedKey作为成员变量在构造时通过KeyExpansion一次性计算好。这样在加密多个数据块时避免了重复的密钥扩展开销。FIV用于CBC模式。如果使用ECB模式它可以为空。我们会在构造函数中检查如果模式是CBC但未提供IV则自动生成一个全零的IV在实际生产环境中强烈建议使用随机IV。FRounds由FKeySize决定在构造函数中计算方便后续循环使用。3.2 密钥扩展的实现密钥扩展是AES的第一步也是至关重要的一步。它的目标是将一个短的原始密钥16/24/32字节扩展成一个更长的轮密钥数组供每一轮的AddRoundKey使用。procedure TAES.KeyExpansion; var i, j, Nk, Nr: Integer; temp: array[0..3] of Byte; begin Nk : KeySizeInBytes div 4; // 密钥字数4字节为一个字 Nr : FRounds; SetLength(FExpandedKey, 4 * 4 * (Nr 1)); // 轮密钥总字节数 4字 * 4字节/字 * (轮数1) // 1. 将原始密钥拷贝到扩展密钥数组的前面 Move(FKey[0], FExpandedKey[0], Length(FKey)); i : Nk; while i 4 * (Nr 1) do begin // 2. 将前一个字临时存储 Move(FExpandedKey[4*(i-1)], temp[0], 4); if (i mod Nk 0) then begin // 3. 对temp进行RotWord循环左移一位、SubWordS盒替换和Rcon异或 // RotWord temp : [temp[1], temp[2], temp[3], temp[0]]; // SubWord for j : 0 to 3 do temp[j] : FSBox[temp[j]]; // FSBox是预定义的S盒数组 // 与轮常数Rcon异或 temp[0] : temp[0] xor Rcon[i div Nk]; // Rcon是预定义的轮常数数组 end else if (Nk 6) and (i mod Nk 4) then begin // 4. 对于256位密钥的特殊处理当i-4是Nk的倍数时对temp进行SubWord for j : 0 to 3 do temp[j] : FSBox[temp[j]]; end; // 5. 生成新的字W[i] W[i-Nk] xor temp for j : 0 to 3 do FExpandedKey[4*i j] : FExpandedKey[4*(i-Nk) j] xor temp[j]; Inc(i); end; end;实现心得轮常数Rcon是一个一维数组Rcon[i] [RC[i], 0x00, 0x00, 0x00]其中RC[1]0x01RC[i] 0x02 * RC[i-1]在有限域上计算。这部分需要预定义好。密钥扩展的逻辑对于128位、192位、256位密钥是统一的通过Nk密钥字数来区分处理逻辑。代码中的if (Nk 6) and (i mod Nk 4)就是专门为256位密钥Nk8增加的额外S盒变换步骤。确保你的FSBox加密S盒和FInvSBox解密S盒是正确的。解密时使用的扩展密钥顺序与加密不同但我们可以通过从扩展密钥中按逆序提取来获得逆轮密钥或者实现一个独立的InvKeyExpansion。为了简单我们可以在解密函数内部临时计算逆序的轮密钥。3.3 核心变换函数的实现我们以MixColumns及其逆运算为例看看如何实现有限域运算。这里我们采用动态计算的方式虽然比完全查表慢但代码清晰易懂。function TAES.MixColumns(state: TBytes; IsInv: Boolean): TBytes; var i, j: Integer; col: array[0..3] of Byte; resultCol: array[0..3] of Byte; begin SetLength(Result, 16); // 对每一列进行处理 for i : 0 to 3 do begin // 取出当前列 for j : 0 to 3 do col[j] : state[i * 4 j]; if not IsInv then begin // 加密时的列混合 // 新列中每个字节 2*col0 xor 3*col1 xor 1*col2 xor 1*col3 等在有限域上计算 resultCol[0] : GMul2(col[0]) xor GMul3(col[1]) xor col[2] xor col[3]; resultCol[1] : col[0] xor GMul2(col[1]) xor GMul3(col[2]) xor col[3]; resultCol[2] : col[0] xor col[1] xor GMul2(col[2]) xor GMul3(col[3]); resultCol[3] : GMul3(col[0]) xor col[1] xor col[2] xor GMul2(col[3]); end else begin // 解密时的逆列混合 resultCol[0] : GMul14(col[0]) xor GMul11(col[1]) xor GMul13(col[2]) xor GMul9(col[3]); resultCol[1] : GMul9(col[0]) xor GMul14(col[1]) xor GMul11(col[2]) xor GMul13(col[3]); resultCol[2] : GMul13(col[0]) xor GMul9(col[1]) xor GMul14(col[2]) xor GMul11(col[3]); resultCol[3] : GMul11(col[0]) xor GMul13(col[1]) xor GMul9(col[2]) xor GMul14(col[3]); end; // 放回结果状态矩阵 for j : 0 to 3 do Result[i * 4 j] : resultCol[j]; end; end; class function TAES.GMul2(a: Byte): Byte; begin // 有限域GF(2^8)上乘以2 // 如果a的最高位是1则左移后需要异或0x1B if (a and $80) 0 then Result : ((a shl 1) xor $1B) and $FF else Result : (a shl 1) and $FF; end;GMul3、GMul9等函数可以通过GMul2的组合来实现例如GMul3(a) GMul2(a) xor a。GMul14等可以通过多次调用GMul2和异或来计算。为了性能这些也可以预计算成256字节的查找表这就是T盒技术。在我们的实现中为了清晰先使用函数计算。3.4 块加密与工作模式整合有了基础变换函数加密一个单独的数据块就水到渠成了。function TAES.EncryptBlock(const block: TBytes): TBytes; var state: TBytes; round: Integer; begin if Length(block) 16 then raise Exception.Create(Block size must be 16 bytes.); state : Copy(block, 0, 16); // 初始轮密钥加 state : AddRoundKey(state, Copy(FExpandedKey, 0, 16)); // 进行前Nr-1轮完整操作 for round : 1 to FRounds - 1 do begin state : SubBytes(state, False); state : ShiftRows(state, False); state : MixColumns(state, False); state : AddRoundKey(state, Copy(FExpandedKey, round * 16, 16)); end; // 最后一轮省略MixColumns state : SubBytes(state, False); state : ShiftRows(state, False); state : AddRoundKey(state, Copy(FExpandedKey, FRounds * 16, 16)); Result : state; end;解密块DecryptBlock是加密的逆过程步骤顺序相反且使用逆变换InvSubBytes,InvShiftRows,InvMixColumns和逆序的轮密钥。最后我们需要在Encrypt和Decrypt方法中处理工作模式ECB/CBC和填充PKCS7。function TAES.Encrypt(const data: TBytes): TBytes; var paddedData: TBytes; blockCount, i: Integer; block, prevBlock: TBytes; begin // 1. PKCS7填充 paddedData : PKCS7Pad(data, 16); SetLength(Result, Length(paddedData)); prevBlock : FIV; // CBC模式下第一个块的前一个块是IV // 2. 分块处理 blockCount : Length(paddedData) div 16; for i : 0 to blockCount - 1 do begin block : Copy(paddedData, i * 16, 16); if FMode mCBC then begin // CBC模式先与前一密文块或IV异或再加密 for j : 0 to 15 do block[j] : block[j] xor prevBlock[j]; end; // ECB模式直接加密 block : EncryptBlock(block); // 加密核心块 Move(block[0], Result[i * 16], 16); prevBlock : block; // 更新prevBlock为当前密文块用于下一个CBC块 end; end;Decrypt函数与之对称在CBC模式下需要先解密再与前一个密文块注意是前一个密文块不是前一个解密后的明文块异或来得到明文。4. 实战应用、调试与性能优化代码写完了但离“能用”和“好用”还有一段距离。这部分我们来解决实际应用中会遇到的问题并分享一些调试技巧和优化思路。4.1 如何验证你的AES实现是正确的这是最关键的一步。自己实现的算法最怕的就是存在隐蔽的错误。这里提供一套验证组合拳使用标准测试向量NIST美国国家标准与技术研究院发布了完整的AES测试向量包括各种密钥长度和模式的加密解密测试。你可以找一套这样的测试数据通常是文本文件用你的程序加密已知的明文看结果是否与标准密文完全一致。这是最权威的验证方法。例如你可以测试一个128位密钥、ECB模式下的简单加密密钥2b7e151628aed2a6abf7158809cf4f3c明文3243f6a8885a308d313198a2e0370734密文3925841d02dc09fbdc118597196a0b32把你的密钥和明文转换成字节数组调用Encrypt将输出的字节数组转换成十六进制字符串看是否与标准密文匹配。与可靠工具交叉验证使用OpenSSL命令行工具、在线AES加密工具选择知名的、开源的进行对比。确保你选择的工具使用相同的参数AES-128-CBC、PKCS7填充、相同的IV。用你的代码加密一段文本再用工具加密比较Base64或Hex编码后的结果是否一致。注意很多在线工具的默认编码是UTF-8而Delphi的字符串可能是ANSI或Unicode确保你处理的是纯字节数据避免编码引入的干扰。自验环测试这是最直接的测试。随机生成一段数据长度不必是16的倍数用你的类加密然后立即解密比较解密后的数据是否与原始数据完全一致。这个测试能发现加解密流程中的逻辑错误。procedure TestAESSelf; var AES: TAES; Key, IV, PlainText, CipherText, DecryptedText: TBytes; begin // 生成随机密钥和IV仅测试用实际应用应使用安全的随机数生成器 Key : RandomBytes(16); // 128位密钥 IV : RandomBytes(16); PlainText : TEncoding.UTF8.GetBytes(Hello, this is a test message! 你好这是一个测试消息); AES : TAES.Create(Key, ks128, mCBC, IV); try CipherText : AES.Encrypt(PlainText); DecryptedText : AES.Decrypt(CipherText); if CompareMem(PlainText[0], DecryptedText[0], Length(PlainText)) then WriteLn(Self-test PASSED!) else WriteLn(Self-test FAILED!); finally AES.Free; end; end;4.2 常见问题与排查技巧实录在实际集成到项目时你几乎一定会遇到下面这些问题。我把它们和排查思路整理成了表格方便你快速对照。问题现象可能原因排查步骤与解决方案解密后得到乱码且长度不对PKCS7去填充失败。这是最常见的问题。解密函数末尾的PKCS7Unpad逻辑错误或者密文在传输/存储过程中被损坏导致填充字节数无效。1.打印解密后去填充前的数据在调用PKCS7Unpad前将数据用Hex打印出来看最后一个字节的值padLen是否在1-16之间。2.检查密文完整性确保你解密的密文就是之前加密输出的完整字节数组没有经过任何截断或编码转换如误将Base64字符串当Hex处理。3.手动验证填充计算最后padLen个字节的值是否都等于padLen。解密结果开头部分正确后面乱码CBC模式初始化向量不匹配。加密和解密时使用的IV不同。或者在CBC模式下密文块在解密前被篡改或错位。1.确保IV一致将加密时使用的IV保存下来解密时必须传入相同的IV。2.检查密文块顺序确保你传递给解密函数的数据是按16字节分块且顺序正确的完整密文。加密/解密结果与在线工具不一致参数没有完全对齐。包括密钥长度、工作模式、填充模式、IV、数据编码。1.参数六核对密钥(Key)、模式(Mode)、填充(Padding)、IV、输入数据、输出格式(Hex/Base64)。必须全部一致。2.隔离测试使用最简单的ECB模式、无IV、对纯英文短文本进行测试排除IV和编码问题。3.逐块调试对于长文本只加密第一个16字节块将中间状态每轮后的state打印出来与标准测试向量或另一个可靠实现的中间状态对比定位出错的具体变换步骤。性能慢加密大文件时卡顿使用了未优化的实现特别是MixColumns和SubBytes的纯函数计算以及频繁的字节数组拷贝。1.启用编译优化确保Delphi项目的编译选项开启了优化。2.引入查表法将SubBytes、ShiftRows、MixColumns合并的查表操作T盒实现这是性能提升最显著的一步。网上有现成的优化Delphi AES代码核心就是用了巨大的预计算表。3.减少内存分配与拷贝在内部循环中尽量使用原地操作避免频繁的SetLength和数组拷贝。例如可以修改EncryptBlock直接修改传入的state数组。在DLL或线程中使用时随机崩溃类内部使用了共享的静态变量如S盒、T盒但没有考虑线程安全或者内存管理有问题。1.检查静态数据确保S盒、Rcon等常量数组是const或只读的多个线程同时读取是安全的。2.线程隔离如果T盒是在初始化时动态计算的确保其初始化过程是线程安全的用TInterlocked或临界区保护。3.对象生命周期确保TAES实例的创建和释放在同一个线程内或者使用接口引用计数来管理。实操心得调试加密算法十六进制转储是你的最佳朋友。不要依赖眼睛看字符串一定要把关键的中间数据原始密钥、扩展密钥、IV、每轮加密前后的状态矩阵、填充前后的数据都以Hex形式输出到日志文件。对比这些Hex值你能精准定位到是密钥扩展错了还是某一轮的MixColumns结果不对。我曾经花了两天时间追踪一个Bug最后发现是ShiftRows函数里一个数组索引写成了[r, c]而不是[r, (c r) mod 4]只有通过对比每轮后的状态Hex才揪出来。4.3 进阶优化与生产环境建议当你通过了基本测试可以考虑以下优化让这个AES类更健壮、更高效T盒优化将SubBytes、ShiftRows、MixColumns合并的运算预先计算成4个1KB的T盒Te0, Te1, Te2, Te3加密时一轮操作可以简化为4次查表和4次异或。解密同理使用Td盒。这是工业级实现的标准做法性能可提升一个数量级。代码会变得复杂但核心逻辑不变。支持更多模式目前只实现了ECB和CBC。可以考虑增加CFB、OFB、CTR等模式。这些模式在实现上各有特点例如CTR模式可以实现并行加密非常适合流式数据。内存安全密钥和IV是高度敏感的数据。在类内部可以使用SecureZeroMemory类似的函数在对象销毁前清空存储这些数据的字节数组防止内存残留攻击。Delphi中可以用FillChar或ZeroMemory。错误处理目前的实现用Exception。在生产环境中可以考虑定义更细致的异常类如EAESKeyError,EAESDataError等方便上层调用者捕获和处理。与外部系统对接如果你的Delphi程序需要与其他系统如Java的Cipher类、C#的AesCryptoServiceProvider交互务必注意字节序问题。AES算法本身是面向字节的没有字节序问题。但如果你将密钥或IV从字符串特别是包含非ASCII字符时转换或者处理多字节整数就需要统一使用UTF-8编码并明确约定Hex或Base64的编码解码方式。最后虽然自己实现AES是一个极好的学习过程但对于关键的生产系统如果对性能和安全有极高要求优先考虑使用久经考验的库如通过Delphi调用OpenSSL的DLL或者使用成熟的第三方加密组件如LockBox。这些库经过了无数双眼睛的审查和优化。自己实现的这个TAES类更适合于对第三方依赖有严格限制、或需要深度定制、以及最重要的——用于学习和理解AES原理的场景。把它当作你的“瑞士军刀”在需要快速验证概念、内部工具开发或教育演示时它会非常称手。