4

I'm encrypting text in PHP (openssl_encrypt / 'aes-256-cbc') and then trying to decrypt it in Delphi 7 (DCPCrypt / TDCP_rijndael).

The PHP script file is saved with ANSI encoding, in the hope that the string transmitted (its a REST API web service) is compatible with Delphi.

However the Delphi decryption is producing the wrong result, I am guessing that something is wrong in the code. I would be grateful if you could have a look, and spot my error on the Delphi side:

PHP Code:

function encrypt($key, $payload) {
    $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
    $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $key, 0, $iv);
    return base64_encode($encrypted . '::' . $iv);
}

function decrypt($key, $garble) {
        list($encrypted_data, $iv) = explode('::', base64_decode($garble), 2);
        return openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, 0, $iv);
}

Delphi code:

var
  DCP_rijndael: TDCP_rijndael;

const
  cPASSWORD = 'myownpassword';

function Decrypt(AStr: string): string;
var
  d, s, iv: String;
  p: Integer;
begin
  d := Base64DecodeStr(AStr);
  p := Pos('::', d);
  s := Copy(d, 1, p - 1);
  iv := Copy(d, p + 2, Length(s));

  DCP_rijndael.SetIV(iv);
  Result := DCP_rijndael.DecryptString(s);
end;

initialization
  DCP_rijndael := TDCP_rijndael.Create(nil);
  DCP_rijndael.Algorithm := 'Rijndael';
  DCP_rijndael.CipherMode := cmCBC;

  //DCP_rijndael.BlockSize := 128; {tried various values with no luck!}
  //DCP_rijndael.MaxKeySize := 256;{tried various values with no luck!}

  DCP_rijndael.Init(cPASSWORD, 256, nil);

finalization
  DCP_rijndael.Free;

..have a tight project deadline, and am stuck on this ..would really appreciate assistance in resolving the issue. TIA!

Steve F
  • 1,527
  • 1
  • 29
  • 55
  • Have you checked that the first stage (Base64 decoding) gives the same result? – fpiette Jan 20 '21 at 13:39
  • @fpiette I'm not sure how to check this, Its all binary characters on PHP side when output to browser. – Steve F Jan 20 '21 at 14:03
  • @SteveF isn't it obvious? Don't output binary data - store it in files, which can be compared even easier. Also: why using OpenSSL only in PHP but not in Delphi? – AmigoJack Jan 20 '21 at 14:25
  • @AmigoJack The PHP side is a web service (REST API), and it returns a data string encrypted. This web API is accessed from Delphi, and I need to decrypt the transmitted data. If DCPCrypt library does not work then I will try an OpenSSL implementation. – Steve F Jan 20 '21 at 14:32
  • The PHP implementation is flawed, as any of both binary sides could have `'::'` by incidence already. The text encoding of a PHP files is irrelevant to what `$payload` may contain - likewise it's possible to store your code in UTF-8 but deal with strings in Shift-JIS. All the code ignores so many details... – AmigoJack Jan 20 '21 at 15:12

1 Answers1

9

Basics that need to be understood

  • Never deal with "text", as encryption is not aware of text encodings.
  • Never deal with "Strings", as those differ drastically between programming languages, platforms (x86 vs. x64) and types (Ansi vs. Wide).
  • Different ciphers have different block sizes - this implies that the provided data to encrypt must match a length that can be divided by a given divisor (i.e. 8 or 16). Otherwise padding applies, and you might need to take care of that.
  • OpenSSL's primary target audience is MIME/e-mail, hence it already operates on Base64. Do not re-encode its output into Base64 again - that's just missing the point.
  • A key is always binary. Deal with it as raw bytes. It's only coincidence that ASCII works, too. But as soon as you're beyond that, reconsider what you're about to do.

Why dealing with Base64?

It's just a way to store binary data in a rock solid format. It's bigger in size, but even safe to be sent in emails. If you don't have that need, because you'd store your data into files anyway, then (of course) don't use it.

Encrypting and decrypting in PHP using OpenSSL

The PHP file's text encoding is irrelevant in terms of encoding and decoding: the parameters for those functions are still treated as binary.

<?php

// This file's output should not be interpreted as HTML
header( 'Content-type: text/plain' );

// Do not use the same literals again and again
define( 'CIPHER', 'aes-128-cbc' );  // Which algorithm is used
define( 'GLUE', '::' );  // How to concatenate data and IV


function encrypt( $key, $plain ) {
    // Initialization vector comes in binary. If we want to carry that
    // thru text-like worlds then we should convert it to Base64 later.
    $iv= openssl_random_pseudo_bytes( openssl_cipher_iv_length( CIPHER ) );
    echo "\n iv=\t\t(binary as hex)\t". bin2hex( $iv ). "\tlength=". strlen( $iv );

    // By default OpenSSL already returns Base64, but it could be changed 
    // to binary with the 4th parameter, if we want.
    $encryptedData= openssl_encrypt( $plain, CIPHER, $key, 0, $iv );
    echo "\n encrypted=\t(Base64)\t". $encryptedData;

    // The encrypted data already came in Base64 - no need to encode it
    // again in Base64. Just concatenate it with the initialization
    // vector, which is the only part that should also be encoded to
    // Base64. And now we have a 7bit-safe ASCII text, which could be
    // easily inserted into emails.
    return $encryptedData. GLUE. base64_encode( $iv ). GLUE. strlen( $plain );
}

function decrypt( $key, $allinone ) {
    // The "glue" must be a sequence that would never occur in Base64, so
    // we chose "::" for it. If everything works as expected we get an
    // array with exactly 3 elements: first is data, second is IV, third
    // is size.
    $aParts= explode( GLUE, $allinone, 3 );

    // OpenSSL expects Base64 by default as input - don't decode it!
    $data= $aParts[0];
    echo "\n data=\t\t(Base64)\t". $data;

    // The initialization vector was encoded in Base64 by us earlier and
    // now needs to be decoded to its binary form. Should size 16 bytes.
    $iv= base64_decode( $aParts[1] );
    echo "\n iv=\t\t(binary as hex)\t". bin2hex( $iv ). "\tlength=". strlen( $iv );

    return openssl_decrypt( $data, CIPHER, $key, 0, $iv );
}

// Keep in mind that you DON'T encrypt and decrypt "TEXT" - you
// operate on binary data. Likewise make sure you fully understood
// this by choosing only ASCII before advancing into the world of
// different text encodings. Never mix encryption with "Strings" -
// only operate on it as if it would be naked bytes that make no sense!
$plain= 'AbCdEfGhIjKlMnOpQrStUvWxYz';
$key= '1234567890123456';

echo "Parameters:
 plain=\t\t(binary)\t$plain\tlength=". strlen( $plain ). "
 key=\t\t(binary)\t$key\tlength=". strlen( $key ). "
";

echo "\nEncryption:";
$en= encrypt( $key, $plain );
echo "\n allinone=\t(ASCII)\t\t". $en. "\n";

echo "\nDecryption:";
$de= decrypt( $key, $en );
echo "\n decrypted=\t(binary)\t". $de;

If an initialization vector of 9e8e5d5ab909d93c991fd604b98f4f50 (hexadecimal representation of its 16 byte length) is chosen then the encryption should produce an all-in-one text of 9NC0HhAxFZLuF/omOcidfDQnczlczTS1nIZkNPOlQZk=::no5dWrkJ2TyZH9YEuY9PUA==::26 where the first part is the encrypted data in Base64, the second part is the initialization vector in Base64, and the third part ensures the length of our plain(text) input. Use that long text and you should be able to decode it back to the plain(text) of AbCdEfGhIjKlMnOpQrStUvWxYz (length 26 in bytes).

Decrypting in D7 using DEC5.2

I'm not entirely sure but Delphi Encryption Compendium 5.2, Part I doesn't seem to support different key sizes for AES, that's why I stick to 128. Keep in mind that Delphi 7's String must always be treated as AnsiString in other versions, as otherwise you end up with something that isn't byte-safe.

uses
  DecCipher, DecFmt;

const  // The same glue for concatenating all 3 parts
  GLUE= '::';

var
  c: TDecCipher;  // Successfully tested with DEC 5.2 on Delphi 7
  sAllInOne,  // All 3 parts in a 7bit-safe ASCII text
  sKey,  // The binary key we have to provide
  sIv,  // Initialization vector, decoded from sAllInOne
  sEncrypted,  // Actual data to decrypt, decoded from sAllInOne
  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
  // What was output by the PHP script
  sAllInOne:= '9NC0HhAxFZLuF/omOcidfDQnczlczTS1nIZkNPOlQZk=::no5dWrkJ2TyZH9YEuY9PUA==::26';

  // Find next delimiter; Base64 will never have a '::' sequence
  iPosGlue:= Pos( GLUE, sAllInOne );
  sEncrypted:= Copy( sAllInOne, 1, iPosGlue- 1 );  // Still Base64
  Delete( sAllInOne, 1, iPosGlue- 1+ Length( GLUE ) );

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

  // What remains is the length of the original text, once decrypted. Why do we need it?
  // Because the cipher/algorithm depends on fixed block sizes, so it is automatically
  // padded to the next full length. Otherwise we end up with decryptions that will
  // always have a few odd bytes at the end, if they aren't multiples of 16.
  iLength:= StrToInt( sAllInOne );

  // Keep in mind: this is treated as binary, not text! 16 full bytes.
  sKey:= '1234567890123456';

  // 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

    // We're done: sPlain should be 'AbCdEfGhIjKlMnOpQrStUvWxYz'
    Writeln( sPlain );
  finally
    c.Free;
  end;
end;

Since OpenSSL is not used we need to treat the block size padding ourselves - if you omit the last length assignment you'll see there are more bytes to round up to a size of 32 bytes.

And the rest?

Should be obvious. Encryption in Delphi is very similar. Using texts beyond ASCII both as payload and/or keys is entirely possible, but will most likely not be magically done behind the scenes - make sure you actually have i.e. UTF-8 or ISO-8859-1 everywhere by stepping thru all code lines and trace if the memory really holds the bytes you expect. If you're not into text encodings, then leave it to others. If you're not into encryption, then leave dealing with text to others.

Using a different library/component (i.e. one that supports AES-256) in Delphi should be easily exchangeable with my example if you mind all the steps. If you grab a wild Base64 en-/decoder from the internet then be aware that there are also mildly different versions.

AmigoJack
  • 5,234
  • 1
  • 15
  • 31