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.