Delphi DEC库(Rijndael)加密

8
我将使用DEC 3.0库(Delphi Encryption Compedium Part I)在Delphi 7中加密数据,并通过POST发送给一个PHP脚本,在那里我将使用mcrypt(RIJNDAEL_256,ECB模式)进行解密。

Delphi部分:

uses Windows, DECUtil, Cipher, Cipher1;

function EncryptMsgData(MsgData, Key: string): string;
var RCipher: TCipher_Rijndael;
begin
  RCipher:= TCipher_Rijndael.Create(KeyStr, nil);
  RCipher.Mode:= cmECB;
  Result:= RCipher.CodeString(MsgData, paEncode, fmtMIME64);
  RCipher.Free;
end;

PHP部分:

function decryptMsgContent($msgContent, $sKey) {
    return mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $sKey, base64_decode($msgContent), MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND));
}

问题在于从PHP解密的结果无法使用,输出结果是乱码,与实际数据不同。
当然,Delphi的Key和PHP的$Key都是相同的24个字符字符串。
现在我知道DEC 3.0已经过时了,我也不是加密方面的专家,不能确定这个实现实际上是否为Rijndael 256。也许有人可以告诉我这个实现方式与PHP的mcrypt w/ RIJNDAEL_256有什么不同。也许密钥大小或块大小不同,但无法从代码中确定。这是Cipher1.pas的摘录:
const
{ don’t change this }
  Rijndael_Blocks =  4;
  Rijndael_Rounds = 14;

class procedure TCipher_Rijndael.GetContext(var ABufSize, AKeySize, AUserSize: Integer);
begin
  ABufSize := Rijndael_Blocks * 4;
  AKeySize := 32;
  AUserSize := (Rijndael_Rounds + 1) * Rijndael_Blocks * SizeOf(Integer) * 2;
end;

附带问题:

我知道ECB模式不被推荐,一旦我使ECB工作起来,我会使用CBC。问题是,我是否也需要将生成的IV传输到PHP脚本中?或者仅仅知道密钥就足够了,就像ECB一样?


@ldsandon:talereader正在使用ECB模式。没有IV。 - Henrick Hellström
希望 PHP 知道 - 不知道调用 mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND)) 会发生什么,也许它会被简单地忽略(我希望如此),也许它会触发一些不好的事情。如果它返回 False,可能会向 mcrypt_decrypt 传递错误的参数。 - user160694
@Isandon 想到了这一点,通过在加密和解密中生成IV并测试PHP的加密/解密,输出结果是正常的。因此看起来在ECB模式下,mcrypt会忽略传递的IV。 - binar
@RobS 如果我无法通过当前的实现(DEC)解决这个问题,我可能会转向另一个实现(DCPCrypt,Lockbox看起来不错,甚至是最新的DEC更新),但这仍然让我感到困惑。我已经使用这个DEC库很长时间了,如果我只在Delphi应用程序中使用它,就从来没有出过问题。 - binar
@RobS 不,这其实不是一个愚蠢的问题。这是基本的调试,直到现在我才没有想到在这个设置上进行测试,因为我之前在其他应用程序中使用过它。Delphi中的算法是有效的(加密然后解密)。Henrick Hellström指出了问题所在。 - binar
显示剩余2条评论
3个回答

6

您正在调用TCipher.Create(const Password:String; AProtection:TProtection)构造函数,该构造函数将计算密码的哈希值,然后将其传递给执行所实现算法的标准密钥计划的Init方法。要覆盖此密钥派生,请使用:

function EncryptMsgData(MsgData, Key: string): string;
var RCipher: TCipher_Rijndael;
begin
  RCipher:= TCipher_Rijndael.Create('', nil);
  RCipher.Init(Pointer(Key)^,Length(Key),nil);
  RCipher.Mode:= cmECB;
  Result:= RCipher.CodeString(MsgData, paEncode, fmtMIME64);
  RCipher.Free;

结束;


谢谢,这让我找对了方向。实际上我在使用自己的密码转换方式(从发布的代码中省略),它只取密码密钥和时间戳的sha1的前24个字符(这些数据一起在POST数据中)。我不得不这样做是因为在PHP中出现了一个错误,即mcrypt只接受最多24个字符的密钥。 - binar
问题的另一部分是DEC实现实际上是128位的Rijndael。通过将PHP解密函数更改为使用RIJNDAEL_128找到了这个问题。现在,PHP解密函数的输出具有原始数据+一些无意义的内容。根据我之前在stackoverflow上阅读的其他帖子,似乎存在填充问题,我需要找出来。 - binar
1
正确的,DEC在ECB模式下不会填充您的明文,这意味着您传递给函数的明文长度必须是密码块大小的倍数(Rijndael / AES为16)。PHP默认使用零填充,因此,假设您使用Delphi的非unicode版本,应该使用StringOfChar(#0,16-(Length(MsgData)mod 16))填充MsgData。 - Henrick Hellström
1
DEC使用128位块大小和128、192和256位密钥实现Rijndael。如果您传递给Init方法的密钥长度<=16,则将使用AES-128,16 < length <= 24则将使用AES-192,否则将使用AES-256。但是,为了保证与mcrypt(或任何其他实现)的互操作性,如果您想要AES-256,则应确保密钥的长度恰好为32。 - Henrick Hellström
1
根据可用文档,MCRYPT_RIJNDAEL_128 对应于 AES(具有 128 位块大小的 Rijndael),无论密钥大小如何。MCRYPT_RIJNDAEL_256 不是 AES,也不受 DEC 支持。 - Henrick Hellström
显示剩余2条评论

2

好的,总结一下,我的代码有3个问题:

  1. 由于我对mcrypt和密码技术的理解不够深入,MCRYPT_RIJNDAEL_256指的是128位,而不是密钥大小。我的正确选择应该是MCRYPT_RIJNDAEL_128,这是AES标准,也被DEC 3.0支持。

  2. DEC有自己的默认密钥派生方式,因此我需要绕过它,这样我就不必在PHP中实现它。实际上,我正在使用自己的密钥派生算法,在PHP中易于复制(sha1(key)的前32个字符)。

  3. DEC不会将明文填充到密码的块大小的倍数,如mcrypt所期望的那样,因此我必须手动进行填充。

以下是可行的代码:

Delphi:

uses Windows, DECUtil, Cipher, Cipher1, CryptoAPI;

function EncryptMsgData(MsgData, Key: string): string;
var RCipher: TCipher_Rijndael;
    KeyStr: string;
begin
  Result:= '';
  try
    // key derivation; just making sure to feed the cipher a 24 chars key
    HashStr(HASH_SHA1, Key, KeyStr);
    KeyStr:= Copy(KeyStr, 1, 24);
    RCipher:= TCipher_Rijndael.Create('', nil);
    RCipher.Init(Pointer(KeyStr)^, Length(KeyStr), nil);
    RCipher.Mode:= cmECB;
    Result:= RCipher.CodeString(MsgData + StringOfChar(#0,16-(Length(MsgData) mod 16)), paEncode, fmtMIME64);
    RCipher.Free;
  except
  end;
end;

PHP:

function decryptMsgContent($msgContent, $sKey) {
    $sKey = substr(sha1(sKey), 0, 24);
    return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $sKey, base64_decode($msgContent), MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB), MCRYPT_RAND)));
}

嗯。SHA-1只会输出20个字节。最简单的解决方案可能是将密钥大小减小到128位/16字节。另一种选择是使用SHA-256并切换到支持SHA-256的另一个库,例如<不要介意我打广告>OpenStrSecII或StreamSec Tools</不要介意我打广告>或DCP Crypt。第三种选择是使用带有密钥拉伸的SHA-1,但在PHP中实现可能很复杂,并且DEC 3.0没有实现任何PKCS#5 v2.0函数。 - Henrick Hellström
我说上面的代码实际上使用的是AES-128,而不是192,我的理解正确吗?我应该将密钥长度减少到16吗?因为这只是一个小项目,我会继续使用AES-128。另外,我切换到了CBC模式,让DEC内部生成IV,然后我只需将IV base64编码与数据一起传递即可。 - binar
它使用AES-192,但密钥中最多只有160位的熵(受SHA-1输出长度的限制),并且密钥的最后4个字节(#21至#24)未定义(尽管可能为0,因为您已经使其工作)。 - Henrick Hellström

0

我发现的256位密钥是32个字符或32个字节,而不是24个。这可能是问题所在。

[编辑]

我将大家的想法(ansistring等)合并为一个单一的想法,并进行了修复。

另外,您正在使用codestring( -- 应该是Encodestring(

我在下面粘贴了可工作的加密和解密源代码:


function EncryptMsgData(MsgData, Key: AnsiString): AnsiString;
var RCipher: TCipher_Rijndael;
begin
  RCipher:= TCipher_Rijndael.Create('', nil);
  RCipher.Init(Pointer(Key)^,Length(Key),nil);
  RCipher.Mode:= cmCBC;
  Result:= RCipher.EncodeString(MsgData);
  RCipher.Free;
end;

function DecryptMsgData(MsgData, Key: AnsiString): AnsiString;
var RCipher: TCipher_Rijndael;
begin
  RCipher:= TCipher_Rijndael.Create('',nil);
  RCipher.Init(Pointer(Key)^,Length(Key),nil);
  RCipher.Mode:= cmCBC;
  Result:= RCipher.DecodeString(MsgData);
  RCipher.Free;
end;

使用32个字符的密钥进行加密和解密,可以获得正确的加密和解密结果。

为了将加密数据存储和使用为字符串,您可能需要使用Base64Encode(

但在解密之前不要忘记进行Base64Decode。

这也是使用Blowfish所需的相同技术。有时候字符实际上就像一个退格键,执行功能而不是显示在屏幕上。Base64Encode基本上将字符转换为可以在文本中显示的内容。

在通过互联网或传输到同一语言或其他语言的另一个应用程序之前,您必须进行base64encode和decode以防止数据丢失。在PHP中也不要忘记这一点!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接