Delphi TLbRijndael与PHP openssl_encrypt加密一致性适配问题
问题描述
我有一个同时被Delphi(XE 10.4或11)和PHP(8+)版本使用的数据库,其中部分加密数据(如密码)需要在两种语言的代码中被一致处理,即需编写功能完全相同的Delphi和PHP加密程序。
我选用了AES(其泛化版本为Rijndael),以下是我的PHP代码:
<?php /** * Applies PKCS7 padding to a given string. * * @param string $data The plaintext data. * @param int $blocksize The block size in bytes (default is 16). * @return string The padded data. */ function pkcs7_pad($data, $blocksize = 16) { $pad = $blocksize - (strlen($data) % $blocksize); return $data . str_repeat(chr($pad), $pad); } /** * Encrypts a plaintext string using AES-128-ECB. * * The key is hashed with SHA-256 and the first 16 bytes are used. * PKCS7 padding is applied to the plaintext before encryption. * * @param string $plaintext The plaintext to encrypt. * @param string $key The encryption key. * @return string The encrypted data in Base64 encoding. */ function encryptAES($plaintext, $key) { // Hash the key with SHA-256 and use the first 16 bytes (AES-128 key length) $key = substr(hash('sha256', $key, true), 0, 16); // Pad the plaintext using PKCS7 $padded = pkcs7_pad($plaintext); // Encrypt using AES-128-ECB mode $encryptedBytes = openssl_encrypt($padded, 'AES-128-ECB', $key, OPENSSL_RAW_DATA); // Return the encrypted data as a Base64 encoded string return base64_encode($encryptedBytes); } // Example usage with obfuscated test data $testKey = "ObfuscatedTestKey123"; // Replace with your actual key $testPlaintext = "ObfuscatedTextData"; // Replace with your actual plaintext $encryptedBase64 = encryptAES($testPlaintext, $testKey); echo "Encrypted (Base64): " . $encryptedBase64; ?>
以下是我使用Lockbox Rijndael编写的Delphi代码:
unit MainFormU; interface uses Winapi.Windows, System.SysUtils, System.Classes, Vcl.Forms, Vcl.StdCtrls, LbCipher, LbClass, System.Hash, System.NetEncoding; type TMainForm = class(TForm) btnEncrypt: TButton; edtPlainText: TEdit; lblEncrypted: TLabel; LbRijndael: TLbRijndael; // Place a TLbRijndael component on your form. procedure btnEncryptClick(Sender: TObject); private { Private declarations } function PKCS7Pad(const Data: string; BlockSize: Integer): string; function BytesToRawString(const Bytes: TBytes): RawByteString; public { Public declarations } end; var MainForm: TMainForm; implementation {$R *.dfm} const // Obfuscated test key. Replace with your actual key if needed. cKey = 'ObfuscatedTestKey'; { Applies PKCS7 padding to the input string so that its length is a multiple of BlockSize. } function TMainForm.PKCS7Pad(const Data: string; BlockSize: Integer): string; var PadSize: Integer; PadChar: Char; begin PadSize := BlockSize - (Length(Data) mod BlockSize); PadChar := Char(PadSize); Result := Data + StringOfChar(PadChar, PadSize); end; { Converts a TBytes array to a RawByteString without any encoding conversion. } function TMainForm.BytesToRawString(const Bytes: TBytes): RawByteString; begin SetLength(Result, Length(Bytes)); if Length(Bytes) > 0 then Move(Bytes[0], Result[1], Length(Bytes)); end; { Button click handler: derives a 16-byte key from a SHA-256 hash of cKey, applies PKCS7 padding to the plaintext, encrypts using AES-128-ECB (via TLbRijndael), and finally Base64-encodes the encrypted data. } procedure TMainForm.btnEncryptClick(Sender: TObject); var HashBytes, KeyBytes: TBytes; RawKey: RawByteString; Plaintext, PaddedPlaintext: string; EncryptedRaw: RawByteString; EncryptedBase64: string; begin // Derive a 16-byte key from the SHA-256 hash of cKey. HashBytes := THashSHA2.GetHashBytes(cKey, THashSHA2.TSHA2Version.SHA256); SetLength(KeyBytes, 16); Move(HashBytes[0], KeyBytes[0], 16); RawKey := BytesToRawString(KeyBytes); // Use the text from the edit control or fallback to an obfuscated test plaintext. if edtPlainText.Text = '' then Plaintext := 'ObfuscatedPlainText' else Plaintext := edtPlainText.Text; // Apply PKCS7 padding to the plaintext. PaddedPlaintext := PKCS7Pad(Plaintext, 16); // Configure TLbRijndael: set ECB mode and assign the derived binary key. LbRijndael.CipherMode := cmECB; LbRijndael.SetKey(RawKey); // Encrypt the padded plaintext. EncryptedRaw := LbRijndael.EncryptString(PaddedPlaintext); // Base64 encode the encrypted binary data. EncryptedBase64 := TNetEncoding.Base64.Encode(EncryptedRaw); // Display the Base64-encoded encrypted string. lblEncrypted.Caption := EncryptedBase64; end; end.
目前Delphi与PHP的加密结果不一致,主要疑问点:
- PHP的
openssl_encrypt期望密钥为字节数组,而Delphi的Rijndael组件期望密钥为字符串,不确定当前字节转字符串的方式是否能让两者行为一致 openssl_encrypt输出是字节数组,而Rijndael输出是字符串,是否需要将Rijndael输出转回字节后再Base64编码- 是否能修改Delphi代码实现与PHP完全(字节级、字符级)匹配?Lockbox 2.08与PHP的AES实现差异是否过大?Lockbox 3是否有大幅改动?是否需要更换其他Delphi组件?
问题分析与解决方案
核心差异点
- 字符串编码处理:PHP中字符串默认是字节序列,而Delphi的
string是UTF-16编码(UnicodeString),直接处理会导致字节长度不匹配,PKCS7 padding计算错误。 - 密钥传递方式:Lockbox 2的
SetKey方法会默认使用系统默认编码(如ANSI)解析字符串,而我们需要直接传递原始字节作为密钥。 - 加密输入输出的字节一致性:Lockbox的
EncryptString会先将UnicodeString转成字节流(默认编码可能不符合预期),输出的RawByteString也可能存在编码转换问题。
修改后的Delphi代码
unit MainFormU; interface uses Winapi.Windows, System.SysUtils, System.Classes, Vcl.Forms, Vcl.StdCtrls, LbCipher, LbClass, System.Hash, System.NetEncoding; type TMainForm = class(TForm) btnEncrypt: TButton; edtPlainText: TEdit; lblEncrypted: TLabel; LbRijndael: TLbRijndael; procedure btnEncryptClick(Sender: TObject); private function PKCS7Pad(const DataBytes: TBytes; BlockSize: Integer): TBytes; public end; var MainForm: TMainForm; implementation {$R *.dfm} const cKey = 'ObfuscatedTestKey123'; // 和PHP保持一致的测试密钥 { 对字节数组进行PKCS7填充,和PHP逻辑完全对齐 } function TMainForm.PKCS7Pad(const DataBytes: TBytes; BlockSize: Integer): TBytes; var PadSize: Integer; i: Integer; begin PadSize := BlockSize - (Length(DataBytes) mod BlockSize); SetLength(Result, Length(DataBytes) + PadSize); Move(DataBytes[0], Result[0], Length(DataBytes)); for i := 0 to PadSize - 1 do Result[Length(DataBytes) + i] := PadSize; end; procedure TMainForm.btnEncryptClick(Sender: TObject); var HashBytes, KeyBytes, PlaintextBytes, PaddedBytes, EncryptedBytes: TBytes; Plaintext: string; EncryptedBase64: string; begin // 1. 生成密钥:SHA256哈希后取前16字节,和PHP逻辑一致 HashBytes := THashSHA2.GetHashBytes(cKey, THashSHA2.TSHA2Version.SHA256); SetLength(KeyBytes, 16); Move(HashBytes[0], KeyBytes[0], 16); // 2. 获取明文并转成UTF-8字节(PHP默认字符串是UTF-8或ASCII,这里统一用UTF-8对齐) if edtPlainText.Text = '' then Plaintext := 'ObfuscatedTextData' // 和PHP测试明文一致 else Plaintext := edtPlainText.Text; PlaintextBytes := TEncoding.UTF8.GetBytes(Plaintext); // 3. PKCS7填充(直接操作字节数组,避免字符串编码问题) PaddedBytes := PKCS7Pad(PlaintextBytes, 16); // 4. 配置Lockbox Rijndael:ECB模式,直接设置字节密钥 LbRijndael.CipherMode := cmECB; LbRijndael.BlockSize := 16; // 明确指定AES-128的块大小 LbRijndael.SetKeyRaw(KeyBytes, Length(KeyBytes)); // 使用SetKeyRaw传递原始字节密钥 // 5. 加密字节数组,避免字符串编码转换 SetLength(EncryptedBytes, Length(PaddedBytes)); LbRijndael.EncryptBuffer(PaddedBytes[0], EncryptedBytes[0], Length(PaddedBytes)); // 6. Base64编码加密后的字节数组 EncryptedBase64 := TNetEncoding.Base64.EncodeBytesToString(EncryptedBytes); // 显示结果 lblEncrypted.Caption := EncryptedBase64; end; end.
关键修改说明
- PKCS7填充改为操作字节数组:避免Delphi UnicodeString的UTF-16编码导致的长度计算错误,完全对齐PHP的字节级填充逻辑。
- 使用
SetKeyRaw传递密钥:Lockbox 2的SetKeyRaw方法允许直接传入字节数组作为密钥,避免字符串编码转换带来的密钥字节不一致。 - 明文转UTF-8字节:PHP中字符串默认是UTF-8(或ASCII兼容字节),Delphi将UnicodeString转成UTF-8字节后再处理,确保明文的字节序列一致。
- 使用
EncryptBuffer加密字节数组:跳过EncryptString的字符串编码转换步骤,直接对原始字节进行加密,保证输入输出的字节一致性。 - 统一测试密钥和明文:确保两边测试用的密钥和明文完全一致,排除测试数据差异导致的结果不同。
关于Lockbox版本的补充说明
- Lockbox 2.08和Lockbox 3的API差异较大,Lockbox 3的设计更贴近现代加密标准,支持更直接的字节操作,但Lockbox 2只要正确使用
SetKeyRaw和EncryptBuffer方法,完全可以实现和PHP一致的AES-128-ECB加密。 - 如果后续遇到更多兼容性问题,也可以考虑使用Delphi内置的
System.NetEncoding和System.Hash结合第三方轻量AES库(如DCPCrypt),或者直接使用Windows CryptoAPI封装,实现更精准的字节级控制。
内容的提问来源于stack exchange,提问作者TomR
相关产品推荐
相关产品推荐

