You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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组件?

问题分析与解决方案

核心差异点

  1. 字符串编码处理:PHP中字符串默认是字节序列,而Delphi的string是UTF-16编码(UnicodeString),直接处理会导致字节长度不匹配,PKCS7 padding计算错误。
  2. 密钥传递方式:Lockbox 2的SetKey方法会默认使用系统默认编码(如ANSI)解析字符串,而我们需要直接传递原始字节作为密钥。
  3. 加密输入输出的字节一致性: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:47:08