2

Using: Delphi 7, DEC v5.2

Please refer to this question: Delphi 7 - DCPCrypt - TDCP_rijndael - DecryptString - How to make it work?

From @AmigoJack's excellent answer, I have the Delphi Decrypt function working fine. Based on that, I am now trying to implement the Encrypt function but have been unsuccessful so far. What is happening is that the encryption is done in Delphi, and the string when decrypted in PHP is producing a different string than what was encrypted, implying that something is wrong in the Delphi code.

This is the code:

uses SysUtils, Windows, Classes, DECCipher, DECFmt, DecUtil;

function Encrypt(AStr: string): string;
function Decrypt(AStr: string): string;

implementation

const
  GLUE = '::';
  cPASSWORD = 'myownpassword';

function Encrypt(AStr: string): string;
var
  c: TDecCipher;  
  sKey, 
  sIv,  
  sEncrypted,  
  sPlain: AnsiString;  
  iPosGlue,  
  iLength: Integer;  
begin
  
  sKey := cPASSWORD;
  iLength := 16;
  SetLength(sIv, iLength);

  // Expect DEC 5.2 to only deal with AES-128-CBC, not 256.
  c := ValidCipher(DecCipher.TCipher_Rijndael).Create;
  try
    c.Mode := cmCBCx;
    c.Init(sKey, sIv); // Provide binary key and binary IV

    sPlain := AStr;
    iLength := Length(sPlain);
    SetLength(sEncrypted, iLength); // By now the output length must match the input's
    c.Encode(sPlain[1], sEncrypted[1], iLength);

  finally
    c.Free;
  end;

  Result := TFormat_MIME64.Encode(sEncrypted) + GLUE + TFormat_MIME64.Encode(sIv) + GLUE + IntToStr(iLength);
end;

I am sure that there is something missing with initialization of the variable Iv, and would really be glad if someone could point out the mistake.

UPDATE: As a first step, I've completed the implementation for Encrypt and have it working in Delphi (see my answer below). With that, I seem to have found a completely different, unrelated bug in the code, that I will post in a separate question.

Steve F
  • 1,527
  • 1
  • 29
  • 55
  • 1
    In your code snippet I do not see that you provide any (randomly generated) IV to the function? – Michael Fehr Feb 14 '21 at 08:29
  • Isn't the key supposed to be an array of 16 bytes? – Olivier Feb 14 '21 at 08:43
  • The PHP [`openssl_encrypt()`](https://www.php.net/manual/en/function.openssl-encrypt.php) function pads the key with NUL bytes when it is too short. You should do the same in Delphi. Your key has only 13 bytes here, you should add 3 NUL bytes. – Olivier Feb 14 '21 at 09:10
  • I've now added: `SetLength(sIv, iLength); RandomBuffer(sIv[1], iLength);` Following this change, I can now decrypt the string in Delphi correctly (which means that the Encrypt function works), but am still unable to decrypt the string in PHP. That means the decrypt function in PHP has something wrong. I'm not sure! – Steve F Feb 14 '21 at 09:19
  • @Olivier I've added `sKey := sKey + StringOfChar(#0, iLength - Length(sKey));` but it still does not work. – Steve F Feb 14 '21 at 09:28

2 Answers2

2

What's a block?

A block cipher (such as AES/Rijndael) only works with the same fixed length (in this case 16 byte = 128 bit). Providing data that doesn't match these 16 bytes is not possible. The data you want to encrypt or decrypt must always be 16 byte = 128 bit in length. Or multiples of that length.

Where does padding apply?

In the real world your data rarily is a multiple of the needed block size. The key, the initialization vector and the data all need to match the block size. If not, they must be padded (or cut) to that size:

  • The key sizes in my example already 16 bytes. Most likely it should be a hash of a textual password (instead of ASCII disguised as binary, as in my example), and there are enough available that output 128 bit = 16 byte - historically MD5 was chosen often. OpenSSH will automatically pad a key with #0 when it's too short and so does DEC5.2 - that means you can use a shorter key both here and in PHP and the output should be the same.
  • The IV needs no further explanation: it's the most random part in all this, so there should be no problem in making it 16 bytes right away.
  • The data can be padded in various ways and OpenSSH by default uses the amount of bytes to be padded as byte value: if 6 bytes of padding are needed, then #6#6#6#6#6#6 is appended; if 2 bytes are needed, then #2#2 is appended.

Why does OpenSSH pad it this way?

  • Only the last of all blocks might be shorter than the needed block size.
  • When decrypting you most likely want to cut off that padding instead of seeing it as part of your input.
  • You look at the last byte and realize it's #15 or lower - now you look at the 14 other preceeding bytes and if they also all are #15 then it's most likely only padding that can be cut off. If the last byte is a #1 then it's not so clear: is this part of the input data or is it padding? To decide/know that it is up to you (i.e. if your input data was text then bytes with such values might never occur). Given the perspective you look at it it might be better than only padding #0 bytes.

PKCS #7 as specified in RFC 2315 §10.3 explains the padding; a comment in PHP's manual to openssl_encrypt() also mentions this.

Encrypting in D7 using DEC5.2

const  // The same glue for concatenating all 3 parts
  GLUE= '::';
var
  c: TDecCipher;  // Successfully tested with DEC 5.2 on Delphi 7
  sKey,  // The binary key we have to provide
  sIv,  // Initialization vector, should be random in the real world
  sEncrypted,  // Output of the encryption
  sPlain: AnsiString;  // What we want to encrypt, in binary
  iPlus,  // Input data padding
  iLength: Integer;  // Plaintext length source, in bytes
begin
  // Keep in mind: Plain, Key and IV are all binary, not text!
  sPlain:= 'AbCdEfGhIjKlMnOpQrStUvWxYz';
  sKey:= '1234567890123456';
  sIv:= #$9e#$8e#$5d#$5a#$b9#$09#$d9#$3c#$99#$1f#$d6#$04#$b9#$8f#$4f#$50;
  iLength:= Length( sPlain );

  // The cipher/algorithm depends on fixed block sizes, so it is automatically
  // padded to the next full length. OpenSSL's padding byte is equal to the amount
  // of bytes to be added: if 6 bytes need to be added, then 6 times #6 is added.
  iPlus:= 16- (iLength mod 16);
  sPlain:= sPlain+ StringOfChar( Chr( iPlus ), iPlus );

  // Expect DEC 5.2 to only deal with AES-128-CBC, not 256.
  c:= ValidCipher( DecCipher.TCipher_Rijndael ).Create;
  try
    c.Mode:= cmCBCx;
    c.Init( sKey, sIv );  // Provide binary key and binary IV
    SetLength( sEncrypted, Length( sPlain ) );  // Both are multiples of 16
    c.Encode( sPlain[1], sEncrypted[1], Length( sPlain ) );

    // Glue it together, should be...
    Writeln
    ( TFormat_MIME64.Encode( sEncrypted )  // '9NC0HhAxFZLuF/omOcidfDQnczlczTS1nIZkNPOlQZk='
    + GLUE+ TFormat_MIME64.Encode( sIv )   // '::no5dWrkJ2TyZH9YEuY9PUA=='
    + GLUE+ IntToStr( iLength )            // '::26'
    );
  finally
    c.Free;
  end;
end;

The padding needs to be the same on both ends: either you instruct OpenSSL to use #0 padding, or you have to mimic the PKCS #7 padding on your side (because DEC is not OpenSSL). Of course: you could also use OpenSSL in Delphi right away instead of relying on DEC, but then none of the details would have surfaced - I rather want to know them to be able to know which parts may break instead of keeping all the "magic" under the hood and only calling one function which does all the work. In the end one has to understand sooner or later how cryption works - you're walking on thin ice if you never tried to use one tool to encrypt and a different one to decrypt.

Community
  • 1
  • 1
AmigoJack
  • 5,234
  • 1
  • 15
  • 31
  • Thanks for the answer. I added the padding code `iPlus:= 16- (iLength mod 16); sPlain:= sPlain+ StringOfChar( Chr( iPlus ), iPlus );` and the decryption is now working correctly in PHP. – Steve F Feb 14 '21 at 14:55
1

Based on comment from Michael & Olivier above, I'm made corrections to the code, and have a working implementation of Encrypt working in Delphi, with the code listed below.

Code:

uses SysUtils, Windows, Classes, DECCipher, DECFmt, DecUtil;

function Encrypt(AStr: string): string;

implementation

const
  GLUE = '::';
  cPASSWORD = 'myownpassword';

function Encrypt(AStr: string): string;
var
  c: TDecCipher; // Successfully tested with DEC 5.2 on Delphi 7
  sKey, // The binary key we have to provide
  sIv, // Initialization vector, random bytes
  sEncrypted, // Encrypted binary we want to get
  sPlain: AnsiString; // Actual data to encrypt
  iLength: Integer; // Plaintext length target, in bytes
begin

  sKey := cPASSWORD;
  iLength := 16;
  // ** sKey := sKey + StringOfChar(#0, iLength - Length(sKey));
  SetLength(sIv, iLength);
  RandomBuffer(sIv[1], iLength);

  // Expect DEC 5.2 to only deal with AES-128-CBC, not 256.
  c := ValidCipher(DecCipher.TCipher_Rijndael).Create;
  try
    c.Mode := cmCBCx;
    c.Init(sKey, sIv); // Provide binary key and binary IV

    sPlain := AStr;
    iLength := Length(sPlain);
    SetLength(sEncrypted, iLength); // Set the output byte array length 
    c.Encode(sPlain[1], sEncrypted[1], iLength);

  finally
    c.Free;
  end;

  Result := TFormat_MIME64.Encode(sEncrypted) + GLUE + TFormat_MIME64.Encode(sIv) + GLUE + IntToStr(iLength);
end;

function Decrypt(AStr: string): string;
var
  c: TDecCipher; // Successfully tested with DEC 5.2 on Delphi 7
  sKey, // The binary key we have to provide
  sIv, // Initialization vector, decoded from AStr
  sEncrypted, // Actual data to decrypt, decoded from AStr
  sPlain: AnsiString; // Decrypted binary we want to get
  iPosGlue, // Next found glue token to cut one part off
  iLength: Integer; // Plaintext length target, in bytes
begin
  iPosGlue := Pos(GLUE, AStr);
  sEncrypted := Copy(AStr, 1, iPosGlue - 1); // Still Base64
  Delete(AStr, 1, iPosGlue - 1 + Length(GLUE));

  iPosGlue := Pos(GLUE, AStr);
  sIv := Copy(AStr, 1, iPosGlue - 1);
  Delete(AStr, 1, iPosGlue - 1 + Length(GLUE));

  iLength := StrToInt(AStr);

  sKey := cPASSWORD;

  // Decode Base64 back into binary
  sEncrypted := TFormat_MIME64.Decode(sEncrypted);
  sIv := TFormat_MIME64.Decode(sIv);

  // Expect DEC 5.2 to only deal with AES-128-CBC, not 256.
  c := ValidCipher(DecCipher.TCipher_Rijndael).Create;
  try
    c.Mode := cmCBCx;
    c.Init(sKey, sIv); // Provide binary key and binary IV
    SetLength(sPlain, Length(sEncrypted)); // By now the output length must match the input's
    c.Decode(sEncrypted[1], sPlain[1], Length(sEncrypted));
    SetLength(sPlain, iLength); // Now cut it to the actual expected length
  finally
    c.Free;
  end;

  Result := sPlain;
end;

// Sample implementation: (Delphi 7)
// var
//   s: string;
// begin
//   s := 'The quick brown fox jumps over the lazy rabbit..';
//   WriteLn(s);
//   s := Encrypt(s);
//   WriteLn(s);
//   s := Decrypt(s);
//   WriteLn(s);
// 
// end;
Steve F
  • 1,527
  • 1
  • 29
  • 55
  • This can't be "working" (decrypted) unless your _PHP_ code looks different from my example, since _OpenSSL_ doesn't expect `#0` padding. Wasn't the whole point of your question to use it on both ends? – AmigoJack Feb 14 '21 at 13:34
  • @AmigoJack I have not yet been able to test the decryption with PHP, because an intermediary step in the process has a fault (bug). I have posted another question regarding this here: https://stackoverflow.com/questions/66196003/delphi-7-encrypt-with-dec-and-decrypt-with-php-openssl-part-ii – Steve F Feb 14 '21 at 13:46
  • @AmigoJack I have added and tested the code you provided in your answer above, and find that decryption in PHP is working correctly. – Steve F Feb 14 '21 at 14:57