ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DCPcrypt2在Delphi12.3下的AES文件加密与哈希校验实践

DCPcrypt2在Delphi12.3下的AES文件加密与哈希校验实践 简介面向 Delphi 12.3 开发者的加密扩展库 DCPcrypt2-XE12属于 DCPcrypt2 加密套件针对新版 IDE 的适配版本可直接在 Object Pascal 工程中提供对称加密与散列算法免去从零实现底层密码逻辑的繁琐。压缩包共 74 个文件以 pas 源码、inc 包含文件、dpk/dproj 工程文件为主辅以 html 文档、txt 说明及 dcr/dfm 资源整体仅 122KB结构紧凑适合作为依赖库嵌入项目。目前已有 160 人学习/下载。除 AES、DES、Blowfish、CAST、RC 系列、IDEA 等常见算法外包内还包含 DCPcrypt2.pas、DCPreg.pas 等封装注册单元并提供 demos 下的 FileEncrypt、FileHash 示例便于对照集成调用同时附有 Athens、Sydney、Alexandria 等版本的 dpk/dproj 工程可明显减少跨 Delphi 版本的兼容性调整成本。对需要为桌面或跨平台应用补充加密能力的开发者这是一份可直接参考的算法组件资源。1. DCPcrypt2 是那个压缩包里最能直接落地的加密库先别急着装组件如果你在 Delphi 12.3 下做一个内网文件加密工具第一个卡住的往往是用什么库不折腾Indy 自带的哈希能用但对称加密还得自己拼网上搜到的加密控件要么版本停在十年前要么在 64 位编译器下翻车。而这个DCPcrypt2-XE12.7z解压后核心就是一套很经典的 Delphi 加密控件库 DCPcrypt2包含 RijndaelAES、Blowfish、Twofish、SHA-1、SHA-256、MD5 等算法支持流式读写适合做文件加密、接口签名、配置项防篡改这类场景。适合谁做 Win32/Win64 桌面工具、不想引入巨型第三方框架、又需要把加密逻辑控制在几十行代码内的 Delphi 开发者。它不是官方组件却是在 Delphi 圈子里沉淀了很多年的那类“老而可靠”的资源值得拆开看看。2. 先看 DCPcrypt2 的底子Cipher、Hash 和流式设计的六个关键点2.1 组件面板上出现的东西基础类与算法类怎么分层DCPcrypt2 的设计和很多控件库不一样它把“算法”拆成了两层。第一层是TDCP_cipher和TDCP_hash这样的抽象基类负责公共状态管理、流处理流程第二层是具体的算法类比如TDCP_rijndael、TDCP_sha1、TDCP_sha256、TDCP_md5、TDCP_blowfish。这种分层的好处是你在代码里操作的是具体的TDCP_rijndael但这个类从基类继承了InitStr、EncryptStream、DecryptStream、HashFile等一致接口记一套用法就能套用到绝大多数算法上。很多新手误以为装完控件就能拖一个“加密组件”到窗体上。实际上DCPcrypt2 的组件注册后面板上出现的是算法对象和一个基础 cipher 对象它们是非可视组件和 TTimer 一样拖到窗体上就行运行时不显示界面。关键区别在于如果你是纯代码开发完全可以不往窗体上放任何东西直接TDCP_rijndael.Create(nil)就可以用。这也是这个库灵活的地方不用被 Form 设计器绑住。个值得注意的点是版本命名。压缩包写着 XE12对应的是 Delphi 12.3但 DCPcrypt2 的源码里通常按 Delphi 编译器版本号做条件编译比如{$IFDEF VER340}。Delphi 12 的编译器版本更高原版源码里的这些宏不一定覆盖得到。你拿到这个压缩包后如果发现编译时某些单元报“未定义的标识符”或者条件编译分支没生效大概率是老版本号没匹配上需要手动加一行版本宏。这个后面安装章节再展开。2.2 流式加密为什么适合大文件内部状态与 EncryptStream 的工作方式DCPcrypt2 处理数据的核心不是“一次性把整个文件读进内存”而是流式。以TDCP_rijndael为例基类里维护了密钥调度表、当前块状态、初始化向量等内部字段。调用EncryptStream(Src, Dst)时它内部循环从源流读固定大小的块加密后写入目标流直到源流读完。这意味着你可以加密一个 2GB 的文件而内存占用只有几十 KB这个特性在处理日志文件、备份文件时非常实用。这里有个使用习惯问题EncryptStream和DecryptStream是流式接口但它们假设你传入的流是从当前位置开始、到末尾结束。如果你刚写完文件没重设 Position 就加密会把空数据源当成输入产出空密文。我一般会在调用前强制把源流Position : 0目标流Position : 0避免这种低级错误。填充策略也是流式加密的一部分。块加密算法按固定块大小处理数据AES 是 16 字节一块文件最后一块不足 16 字节时要做 Padding。DCPcrypt2 在基类里有Padding属性常见取值包括cmPaddingNone、cmPaddingZero、cmPKCS7。cmPKCS7是通用做法加密方和解密方必须一致否则解密出来尾部会有乱码或者直接抛错。这一点在避坑章节我会重点讲。2.3 与 Indy 自带单元的分工什么时候用它而不是自己拼Delphi 自带的 Indy 库其实也有哈希和 Base64 支持比如IdHashSHA1、IdHashMessageDigest很多人会问那我为什么还要用 DCPcrypt2关键在于 Indy 的强项是网络协议它提供的哈希组件是独立对象但没有形成一套统一的加密流接口。你要做 AES 加密Indy 本身不直接提供成熟的对称加密组件最后还是得找第三方库或者调用 Windows API。DCPcrypt2 的优势在于“一次引入对称加密、哈希、Base64、随机数都齐了”。它还能直接和流对象配合这在写文件加密工具时省很多事。换句话说如果你的项目只是偶尔算个 MD5 校验值用 Indy 足够但如果要加密文件、加密接口报文、在多个算法之间切换DCPcrypt2 更合适。兼容性上要注意一点DCPcrypt2 是纯 Delphi 实现的加密库不依赖 DLL早期版本主要针对 VCL/Win32 设计。你在 Delphi 12.3 下使用建议优先做 Win32/Win64 的 VCL 项目如果是 FMX 跨平台项目需要确认条件编译里是否包含对应平台的分支否则可能编译不过。这一点体现了它和“官方支持所有平台”的库之间的边界。3. 把压缩包装进 Delphi 12.3解压、Library Path 与编译自检3.1 解压 7z 后先确认这些目录少走弯路压缩包是 7z 格式第一步肯定是用 7-Zip 解压。解压后你会看到类似Source、Packages、Dcu这样的目录。Source 里面装的是.pas源文件这是最重要的DCPcrypt.pas、DCPrijndael.pas、DCPsha1.pas、DCPbase64.pas都在这里。Packages 目录下通常是.dpk包文件用来编译和安装设计期组件。Dcu 目录一般放预编译产物但这个目录经常是空的或者不是针对 Delphi 12.3 编译的我建议你忽略它自己编译一遍。第一次打开包之前先把 Source 目录加进 IDE 的库路径。打开Tools - Options - Environment Options - Delphi Options - Library在Library path里加上 Source 的完整路径。这一步不做后面打开包文件时会报找不到.dcu或.pas因为编译器默认不在解压目录找源文件。3.2 方式A只做运行时引用适合写无面板代码如果你不需要把组件拖到窗体上只是想用代码创建对象那只加 Library Path 就够了。具体操作如下解压 7z记住 Source 目录的绝对路径。在 Delphi 12.3 中进入Tools - Options - Environment Options - Delphi Options - Library。点击Library path右侧的按钮把 Source 目录添加进去。点OK保存然后新建一个 VCL 应用在 uses 里写DCPcrypt, DCPrijndael, DCPsha1。这时直接编译一个空窗体引用这些单元如果 IDE 能找到.pas说明路径生效。找不到.dcu时会弹错误那就要检查路径是否填错或者机器上是否开了“只使用已编译 .dcu”的选项。这种方式的好处是快不碰包文件不污染 IDE 的组件面板。缺点是设计期没法拖组件但绝大多数场景其实也不需要拖。我自己的习惯是纯代码方式因为加密逻辑写在业务类里比放在 Form 上更容易维护。3.3 方式B打开 DPK 安装设计期组件如果你想在组件面板里看到 DCP 页那就得编译并安装设计期包。通常在 Packages 目录下会有运行时包和设计期包两个.dpk文件名可能类似dcpcrypt2_r.dpk和dcpcrypt2_d.dpk。操作步骤是双击运行时包.dpk在项目管理器里右键选择Compile。编译成功后右键包节点选择Install如果提示没有设计期类说明你打开的是运行时包需要关掉再打开设计期包。打开设计期包.dpk同样先Compile再Install。安装完成后组件面板会出现一个新的分类页里面能看到TDCP_rijndael、TDCP_sha1、TDCP_md5等。页签名称可能是 DCP也可能是 DCPCrypt看包里注册单元怎么写的。编译包的时候如果报E1026 File not found: DCPcrypt.dcu基本可以断定 Library Path 没配置好或者包的requires引用路径指向了不存在的目录。这时候先回到 3.2 把 Source 加进路径再重新编译包。3.4 编译成功的度量用 SHA1 自检代替“没有报错”编译通过不等于代码能跑出正确结果。DCPcrypt2 这种老库最常见的坑是某些单元在 64 位下存在指针隐式转换编译器可能只给 warning 不给 error跑起来结果却是错的。所以装完库之后我建议先做一次最小自检计算字符串hello的 SHA1和已知值比对。uses DCPsha1, DCPcrypt; function SHA1HexOfString(const AValue: string): string; var Hash: TDCP_sha1; Digest: array[0..19] of Byte; Hex: string; B: Byte; begin Hash : TDCP_sha1.Create(nil); try Hash.HashString(AValue, Digest); Hex : ; for B in Digest do Hex : Hex IntToHex(B, 2); Result : LowerCase(Hex); finally Hash.Free; end; end;这段代码做了三件事创建TDCP_sha1对象调用HashString计算摘要把 20 字节的摘要转成十六进制字符串。Digest声明为array[0..19] of Byte因为 SHA1 输出 160 位即 20 字节如果声明成别的长度Final写入时会越界。调用IntToHex(B, 2)时第二个参数 2 保证了单字节前面补零比如0A而不是A。如果你SHA1HexOfString(hello)得到aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d那说明编译产物基本可信。如果结果对不上先确认输入字符串编码是不是 UTF-8DCPcrypt2 的HashString处理的是 Delphi 字符串的内存字节Delphi 12 的string默认是 UTF-16和在线工具算 UTF-8 的结果会不一样。想和在线工具对齐需要先把字符串转成 UTF-8 字节数组再调用Update而不是直接用HashString。4. 实战封装写一个带 IV 文件头的 AES-256 文件加密工具类4.1 工具类的整体设计文件头结构、密钥生成与 Padding 约定文件加密的基本要求是同一个密码加密不同文件时密文不能相同否则文件头有规律很容易被看出来内容相似。解决方案是每次加密生成一个随机的 128 位 IV把它写进密文文件头。文件头结构我设计如下字段长度说明魔数4 字节固定为DCP1用来识别文件格式IV 长度4 字节写入整数 16方便以后扩展IV16 字节随机生成的初始化向量密文剩余字节AES-256-CBC 加密结果密钥由字符串密码生成。DCPcrypt2 的InitStr方法支持传入一个哈希类比如TDCP_sha256它会把密码字符串先用 SHA-256 散列再截取或扩展成密钥。这样你不需要自己管密码到字节数组的转换。填充方式统一用cmPKCS7。这个填充策略在 CBC 模式下是标配加密时会在文件末尾补齐到 16 字节倍数解密时由库自动去掉。保证加密解密两端设置一致这是文件头里不写填充方式也没问题的前提。4.2 加密实现读取 IV、InitStr 与 EncryptStream下面是我封装后的加密方法完整代码可以直接放到一个单元里用。注意演示代码里的 IV 是用Random生成的生产环境请替换成系统级随机源。uses System.SysUtils, System.Classes, DCPcrypt, DCPrijndael, DCPsha256; type TFileEncryptor class private FCipher: TDCP_rijndael; public constructor Create; destructor Destroy; override; procedure EncryptFile(const AInputFile, AOutputFile, APassword: string); procedure DecryptFile(const AInputFile, AOutputFile, APassword: string); end; constructor TFileEncryptor.Create; begin inherited Create; FCipher : TDCP_rijndael.Create(nil); FCipher.Burn; // 清除上次遗留的密钥状态 end; destructor TFileEncryptor.Destroy; begin FCipher.Free; inherited Destroy; end; procedure TFileEncryptor.EncryptFile(const AInputFile, AOutputFile, APassword: string); var Src, Dst: TFileStream; IV: array[0..15] of Byte; Magic: array[0..3] of Byte; IVLen: Integer; I: Integer; begin Src : TFileStream.Create(AInputFile, fmOpenRead or fmShareDenyNone); Dst : TFileStream.Create(AOutputFile, fmCreate); try // 文件头魔数 IV 长度 IV Magic : DCP1; Dst.WriteBuffer(Magic, SizeOf(Magic)); IVLen : SizeOf(IV); Dst.WriteBuffer(IVLen, SizeOf(IVLen)); // 演示用随机源生产环境建议使用系统加密随机数 for I : 0 to 15 do IV[I] : Random(256); Dst.WriteBuffer(IV, SizeOf(IV)); // 用密码和 SHA-256 生成密钥并设置 IV 与填充 FCipher.InitStr(APassword, TDCP_sha256); FCipher.IV : IV; FCipher.Padding : cmPKCS7; // 流式加密从源流当前位置读到末尾并加密写入目标流 Src.Position : 0; FCipher.EncryptStream(Src, Dst); finally Dst.Free; Src.Free; end; end;这段代码的重点在三个地方。第一个是文件头写入顺序先写魔数再写 IV 长度和 IV解密时按同样顺序读第二个是InitStr(APassword, TDCP_sha256)第一个参数是用户输入的密码字符串第二个参数是哈希类库内部用 SHA-256 把任意长度密码转成固定长度密钥第三个是FCipher.IV : IV把随机 IV 设置给加密对象Padding : cmPKCS7指定填充模式这两项在解密时必须和加密时完全一致。调用EncryptStream之前把Src.Position : 0是很重要的一步。如果你传入的源流是上一个操作留下的Position 可能停留在中间位置导致只加密了后半段文件解密时得到残缺数据。EncryptStream会一直处理到流末尾所以源流的 Position 就是起始点。4.3 解密实现还原 IV、重新初始化和流复位解密方法读取文件头还原 IV然后用同一个密码和同样的填充策略初始化流再调用DecryptStream。procedure TFileEncryptor.DecryptFile(const AInputFile, AOutputFile, APassword: string); var Src, Dst: TFileStream; IV: array[0..15] of Byte; Magic: array[0..3] of Byte; IVLen: Integer; begin Src : TFileStream.Create(AInputFile, fmOpenRead or fmShareDenyNone); Dst : TFileStream.Create(AOutputFile, fmCreate); try // 读文件头校验魔数 Src.ReadBuffer(Magic, SizeOf(Magic)); if not SameValue(Ord(Magic[0]), Ord(D)) then raise Exception.Create(不是有效的加密文件格式); Src.ReadBuffer(IVLen, SizeOf(IVLen)); if IVLen SizeOf(IV) then raise Exception.Create(不支持的 IV 长度); Src.ReadBuffer(IV, SizeOf(IV)); // 重新初始化密钥、IV 和填充 FCipher.InitStr(APassword, TDCP_sha256); FCipher.IV : IV; FCipher.Padding : cmPKCS7; // 流式解密 Src.Position : Src.Position; FCipher.DecryptStream(Src, Dst); finally Dst.Free; Src.Free; end; end;解密时有个非常容易忽略的细节DecryptStream处理完流之后会把 Cipher 的内部状态留在“上一条消息处理完”的位置。如果你复用同一个TFileEncryptor实例连续解密两个文件第二个文件解密前必须重新调用InitStr否则会沿用上一个文件的密钥和 IV。我在构造函数里调了一次Burn但每个加密/解密操作内部还是建议重新InitStr这是最稳妥的写法。Src.Position : Src.Position这一行表面上没做任何事实际上是在提醒自己文件头读取完毕后流的当前位置已经在密文起始处不需要额外 seek。如果这里写成Src.Position : 0会把文件头当密文解密结果完全乱掉。4.4 调用方式与参数说明实际调用时业务代码里只需要几行var Encryptor: TFileEncryptor; begin Encryptor : TFileEncryptor.Create; try Encryptor.EncryptFile(C:\Data\report.db, C:\Data\report.enc, Pssw0rd); Encryptor.DecryptFile(C:\Data\report.enc, C:\Data\report_dec.db, Pssw0rd); finally Encryptor.Free; end; end;文件加密工具的参数通常有几个是必须确认的AInputFile必须是存在且可读取的源文件AOutputFile不能和AInputFile是同一个路径否则流在写入时会互相干扰APassword建议至少 12 位因为最终密钥是 SHA-256 散列后的结果密码复杂度直接影响暴力破解成本。如果业务上要求多次加密同一文件得到不同密文上面的代码已经满足因为每次加密都会重新生成随机 IV。这套封装的边界也很清楚它只做 AES-256-CBC 算法的文件加解密不包含压缩、分块、增量更新。如果文件超过 4GBTFileStream用的是 64 位文件指针没问题但要注意磁盘剩余空间如果要在断点续传那需要把 IV 和块索引都写进文件头复杂度会明显上升。5. 避坑记录DCPcrypt2 在 Delphi 12.3 下最容易翻车的五个点5.1 组件面板没出现 DCP 页现象按 3.3 的步骤编译 dcp 包并 Install提示安装成功但组件面板里找不到任何新分类或者有分类但里面是空的。原因DCPcrypt2 的很多流通版本里运行时包和设计期包没有严格区分安装的是运行时包里面没有注册组件的过程。解决右键项目管理器里的包节点看Contains列表里有没有Register相关单元。如果没有找另一个文件名带design或d的 dcp 文件重新打开安装。实在找不到设计期包就直接用 3.2 的纯代码方式不影响任何业务功能。5.2 64 位下编译大量指针隐式转换警告现象Win32 编译通过切到 Win64 后DCPcrypt 相关单元报一堆W1000 Symbol is deprecated或者W1024某些地方甚至直接提示Illegal type in OLE section。原因老库的源码里大量使用Pointer和Integer混用32 位下两者长度一致64 位下Integer是 4 字节而Pointer是 8 字节编译器只能给警告严重时产生截断。解决优先用 Win32 编译发布如果必须出 Win64打开 DCPcrypt 相关源码搜索integer(ptr这类强制转换把Integer改成NativeInt。这个过程需要一点耐心改动后建议用 4.2 里的 AES 加解密自检跑一遍确认结果与 32 位版本一致。5.3 解密结果文件比原文件多出几 KB 或尾部乱码现象加密后再解密文件内容能打开但大小对不上或者尾部多了一段不可读字符。原因加密时用了cmPKCS7解密时用的是cmPaddingNone库不会主动去掉填充所以解密流里会保留填充字节。解决加密和解密两端统一使用cmPKCS7。如果你需要和外部系统对接外部系统通常用 PKCS7这是默认选择。尽量不要用cmPaddingNone除非你能保证原始文件长度正好是 16 的倍数。5.4 同一个密码每次加密结果一样文件头有明显的重复段现象用同一个密码加密多个文件密文开头的 16 或 32 字节完全相同。原因加密时没有生成随机 IVInitStr之后库内部把 IV 全部清零导致 CBC 模式下每个文件第一个明文块用同一个密钥向量加密。解决参考第 4 章的代码在InitStr之后手动设置随机 IV并在解密时从文件头读回同一个 IV。这一步是加密安全性的底线不能省。5.5 HashString 结果和百度/在线工具不一致现象TDCP_sha1.HashString(hello, Digest)转成十六进制后和在线 SHA1 计算器结果完全不同。原因Delphi 12 的string是 UTF-16 编码HashString对字符串内存字节做哈希等价于哈希 UTF-16 编码的内容在线工具默认哈希 UTF-8 编码的内容。编码不同摘要自然不同。解决如果要对齐外部系统先把字符串转成 UTF-8 字节再交给哈希对象。用TEncoding.UTF8.GetBytes得到字节数组然后调用Hash.Update(PByte(Bytes), Length(Bytes))最后Hash.Final(Digest)。不要用HashString除非你明确知道对端按 Delphi 字符串内存字节处理。6. 进阶用 TDCP_sha1 分块计算大文件指纹并和系统命令对拍6.1 分块 Update 的读法很多哈希组件提供HashFile方法可以直接算整个文件。但HashFile在部分版本里是一次性读入文件内容大文件会浪费内存。更好的做法是手动分块调用Update把内存占用压到固定大小procedure GetFileSHA1(const AFileName: string; var ADigest: array of Byte); var FS: TFileStream; Hash: TDCP_sha1; Buf: array[0..1023] of Byte; ReadCount: Integer; begin FS : TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite); Hash : TDCP_sha1.Create(nil); try Hash.Init; repeat ReadCount : FS.Read(Buf, SizeOf(Buf)); if ReadCount 0 then Break; Hash.Update(Buf[0], ReadCount); until ReadCount SizeOf(Buf); Hash.Final(ADigest); finally Hash.Free; FS.Free; end; end;这段代码用 1024 字节的缓冲区循环读取文件。Update只接收实际读到的字节数ReadCount不能传SizeOf(Buf)否则最后一块会读越界把缓冲区里残留的旧数据也哈希进去。Final(ADigest)输出 20 字节摘要调用前要确认ADigest的长度是 20。缓冲区大小可以调到 64KB 或更大但 1KB 在绝大多数场景下已经足够快瓶颈在磁盘 IO 而不是哈希计算。6.2 用 certutil 对拍验证在 Windows 上验证 SHA1 结果系统自带certutil命令certutil -hashfile D:\Data\large.bin SHA1输出是一行十六进制摘要和上面 Delphi 代码算出来的结果做字符串比对。一致性没问题说明哈希链路是对的。如果是 SHA-256把算法参数换成SHA256Delphi 侧用TDCP_sha256摘要数组长度改成 32。这个对拍做法我每次拿到新加密库都会跑一遍避免算法实现偏差导致后续业务数据对不上。从那以后我每次改完 DCPcrypt2 相关代码都会强制走一遍这套流程先算hello的 SHA1 验证基础链路再加密一个已知文件并解密回来对比内容最后用certutil对拍大文件哈希。三关全过才敢把代码合进业务项目。希望帮到你。本文还有配套的精品资源点击获取
返回列表