AES encryption differences between php mcrypt and a Delphi component - php

I am using a Delphi component from chillkat which does AES encryption for me. It works like a charm and the server accepts my encrypted requests. So I tried to create a php pendant by using mcrypt. But the PHP mcypt result is different in comparison with the Delphi Chillcat result - even if all the parameters are the same. Therefore the server rejects the php requests.
All the encryption settings are the same:
Cipher Name: AES 128
Cipher mode: ECB
Padding scheme: Pad with NULL
Key length: 128
Key: 1234567890ABE1234567890ABE1234DB
String to encrypt: This is a really cool teststring
This is the little php script:
<?php
$key = '1234567890ABE1234567890ABE1234DB';
function string_encrypt($string, $key) {
$crypted_text = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_ECB);
return $crypted_text;
}
function string_decrypt($encrypted_string, $key) {
$decrypted_text = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encrypted_string, MCRYPT_MODE_ECB);
return trim($decrypted_text);
}
echo $test_str = 'This is a really cool teststring'; echo '<br />';
$enc_str = string_encrypt($test_str, $key);
echo bin2hex($enc_str); echo '<br />';
echo string_decrypt($enc_str, $key); echo '<br />';
?>
The php output is:
e355fbcd91ada4b835e1b030cc9741759219f59fe441ba62e628eca2e8289eb3
This is the Delphi Code:
function encrypt(s:PWideChar;mode,padding:integer;algo,cipher,keylength:string):string;
var
crypt: HCkCrypt2;
success: Boolean;
ivHex: PWideChar;
keyHex: PWideChar;
encStr: PWideChar;
decStr: PWideChar;
begin
crypt := CkCrypt2_Create();
// AES is also known as Rijndael.
CkCrypt2_putCryptAlgorithm('aes');
// "pki", "aes", "blowfish", "blowfish2", "des", "3des", "rc2", "arc4", "twofish", "pbes1" and "pbes2"
// CipherMode may be "ecb" or "cbc"
CkCrypt2_putCipherMode(crypt,'ecb');
// KeyLength may be 128, 192, 256
try
CkCrypt2_putKeyLength(crypt,128);
Except
showmessage('The encryption key you have used seems to be invalid');
end;
// The padding scheme determines the contents of the bytes
// that are added to pad the result to a multiple of the
// encryption algorithm's block size. AES has a block
// size of 16 bytes, so encrypted output is always
// a multiple of 16.
{
Possible values are:
0 = RFC 1423 padding scheme: Each padding byte is set to the number of padding bytes.
If the data is already a multiple of algorithm's block size bytes, an extra block is
appended each having a value equal to the block size. (for example, if the algorithm's
block size is 16, then 16 bytes having the value 0x10 are added.). (This is also known as
PKCS5 padding: PKCS #5 padding string consists of a sequence of bytes, each of which
is equal to the total number of padding bytes added. )
1 = FIPS81 (Federal Information Processing Standards 81) where the last byte contains
the number of padding bytes, including itself, and the other padding bytes are set to random values.
2 = Each padding byte is set to a random value. The decryptor must know
how many bytes are in the original unencrypted data.
3 = Pad with NULLs. (If already a multiple of the algorithm's block size,
no padding is added).
4 = Pad with SPACE chars(0x20). (If already a multiple of algorithm's block size, no padding is added).
}
CkCrypt2_putPaddingScheme(crypt,3);
// EncodingMode specifies the encoding of the output for
// encryption, and the input for decryption.
// It may be "hex", "url", "base64", or "quoted-printable".
CkCrypt2_putEncodingMode(crypt,'hex');
// An initialization vector is required if using CBC mode.
// ECB mode does not use an IV.
// The length of the IV is equal to the algorithm's block size.
// It is NOT equal to the length of the key.
ivHex := '';
CkCrypt2_SetEncodedIV(crypt,ivHex,'hex');
// The secret key must equal the size of the key. For
// 256-bit encryption, the binary secret key is 32 bytes.
// For 128-bit encryption, the binary secret key is 16 bytes.
keyHex := '1234567890ABE1234567890ABE1234DB';
CkCrypt2_SetEncodedKey(crypt,keyHex,'hex');
// Encrypt a string...
// The input string is 44 ANSI characters (i.e. 44 bytes), so
// the output should be 48 bytes (a multiple of 16).
// Because the output is a hex string, it should
// be 96 characters long (2 chars per byte).
//encryption
if mode = 0 then
begin
encStr := CkCrypt2__encryptStringENC(crypt,s);
result := encStr;
end
else
begin
result := CkCrypt2__decryptStringENC(crypt,s);
end;
CkCrypt2_Dispose(crypt);
End;
The chillkat Delphi component's output is:
780F849AB30690433409D4FB7B3357735296A6E76D3AA6B6D6C769BE99F32041
I thought both outputs should produce the same value, as all the input parameters are equal, right ?

MCrypt expects the key to be a binary string, but you pass a hex encoded string into it.
Use
$key = hex2bin($key);
or
$key = pack('H*', $key);
depending on PHP support. This must be done before calling mcrypt_encrypt().
Security:
Don't ever use ECB mode. It's not semantically secure. There is a lot that an attacker can get from ciphertexts without decrypting them. You need to use at the very least CBC mode with a random IV. You should also have an authentication tag of your ciphertexts.
This answer gives you every thing you need to know about this. One thing left, don't use MCrypt. It's an old unmaintained library.
This answer is also very helpful in that regard with a different way of achieving the same thing.

thanks to your your nice inputs, I was able to receive a positive answer from the server with the following 4 lines of code:
$key = pack('H*', "*5DB");
$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key,$mytxt, MCRYPT_MODE_ECB);
$hex2 = bin2hex($ciphertext);
echo strtoupper($hex2);
However, I have learned that
ECB is unsecure
mcrypt is old and unmaintained
I will check alternatives and update my code accordingly. Thanks again !

Related

openssl_decrypt fails. cipher expects an IV of precisely 16 bytes

I have a decryption function that just started failing for unknown reasons. The error is "openssl_decrypt(): IV passed is only 6 bytes long, cipher expects an IV of precisely 16 bytes". I have found multiple suggestions to "add base64 encoding", which I already had.
My function is
function decryptThis($data) {
$encryptionKey = "encryptionKey";
$encryption_key = base64_decode($encryptionKey);
list($encrypted_data, $iv) = array_pad(explode('::', base64_decode($data), 2),2,null);
return openssl_decrypt($encrypted_data, 'AES-128-CTR', $encryption_key, 0, $iv);
}
Previously a lot of the mcrypt and OpenSSL functions simply right padded the IV with zeros. However nowadays we try and keep to the specifications for the cipher / mode of operation. If you want to return to the previous situation then you'll have to right pad yourself.
It seems that the fix was a bit too generic or that the author was a bit overzealous as CTR really doesn't take an IV but a nonce. In that sense it would be perfectly reasonable to right-pad a nonce with zeros, as long as the key / nonce combination remains unique.
However, many CTR (counter mode) implementations assume that you provide an entire block consisting of the nonce + starting counter value.
Note that OpenSSL is a C library, so just providing an IV value of 6 bytes would result in a buffer overrun, possibly leading to broken - or worse: irreversible - encryption.

PHP OpenSSL encrypt with DES-ECB algorithm unexpected output

I've tried to encrypt the value with DES-ECB via PHP 7.4 (Laravel) and OpenSSL, and I try the online tool https://emvlab.org/descalc/ to encrypt value, the result was the expected.
key: 96187BBAB2BD19ACF899B74FB4E37972
Input Data: F01DCCE40F8C365ADE0A7DC03BC11DDE
The encrypted data is 355A627E977D8ECF4953C98D801E472F on online tool.
https://emvlab.org/descalc/?key=96187BBAB2BD19ACF899B74FB4E37972&iv=0000000000000000&input=f01dcce40f8c365ade0a7dc03bc11dde&mode=ecb&action=Encrypt&output=D5131A2B35766D371420F679F15969FF
When the next code written in PHP I can't replicate the result:
$data = "F01DCCE40F8C365ADE0A7DC03BC11DDE";
$key = "96187BBAB2BD19ACF899B74FB4E37972";
$encrypted = openssl_encrypt($data, 'DES-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
echo base64_encode($encrypted); // KV8Vie63pG2ZyjW7U5NRJUwOrbsEt+lnG1hNyS331Yk=
Is there a way to get the same string that the online tool?
Thank you!
What must be considered for the website?
The website allows encryption with DES and TripleDES in 2TDEA variant (double length keys). The algorithm is determined by the key size: For an 8 bytes key DES is used, for a 16 bytes key TripleDES/2TDEA. The supported modes are ECB and CBC.
Key, IV (in case of CBC mode), plaintext and ciphertext have to be entered hex encoded on the website. Zero padding is applied (the variant that does not pad if the plaintext length is already an integer multiple of the DES/TripleDES block size of 8 bytes).
What does this mean for the posted data?
Since the posted hex encoded key 96187BBAB2BD19ACF899B74FB4E37972 has a size of 16 bytes, TripleDES/2TDEA is used (in ECB mode).
The hex encoded plaintext F01DCCE40F8C365ADE0A7DC03BC11DDE has a size of 16 bytes and is therefore not padded. The resulting hex encoded ciphertext 355A627E977D8ECF4953C98D801E472F thus has the same length.
How can the PHP code be fixed?
The posted PHP code gives a different result because DES is used as algorithm (in ECB mode) and also the hex encoding/decoding is missing. Thus to produce the result of the website, the encoding/decoding has to be fixed and DES-EDE-ECB has to be applied as algorithm (not to be confused with DES-EDE3-ECB for 3TDEA/triple length keys):
$data = hex2bin("F01DCCE40F8C365ADE0A7DC03BC11DDE"); // hex decode the 16 bytes data
$key = hex2bin("96187BBAB2BD19ACF899B74FB4E37972"); // hex decode the 16 bytes key key
$encrypted = openssl_encrypt($data, 'DES-EDE-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); // apply DES-EDE-ECB; use OPENSSL_ZERO_PADDING
echo bin2hex($encrypted); // 355a627e977d8ecf4953c98d801e472f // hex encode the 16 bytes ciphertext
with the output:
355a627e977d8ecf4953c98d801e472f
that is equal to the result of the web site, s. here
How can a consistent result be achieved for DES?
The current PHP code applies DES and therefore implicitly truncates the key to 8 bytes by using only the first 8 bytes. This can be easily verified by removing the last 8 bytes of the key. The result does not change.
If the shortened key is applied to the website, the results match again.
$data = hex2bin("F01DCCE40F8C365ADE0A7DC03BC11DDE"); // hex decode the 16 bytes data
$key = hex2bin("96187BBAB2BD19AC"); // hex decode the 8 bytes key
$encrypted = openssl_encrypt($data, 'DES-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING); // apply DES-ECB; use OPENSSL_ZERO_PADDING
echo bin2hex($encrypted); // 692370230b8176f011760ddc07392a4f // hex encode the 16 bytes ciphertext
with the output:
692370230b8176f011760ddc07392a4f
that is equal to the result of the web site, s. here.

PHP Encryption Method Comparison

I was recently asked to create a method for encryption / decryption of a URL string. I quickly produced a 1 liner and shot it out.
I was then provided with the code from another developer and asked my opinion. I looked at his to find a much more complex function.
My questions:
What are the specific differences here?
Are there shortfalls found in the short solution?
We are encrypting a json encoded array and passing it via query string URL.
Long solution:
public function Encrypt($message, $key = 'defaultkey') {
//Create an instance of the mcrypt resource
$td = mcrypt_module_open('tripledes', '', 'ecb', '');
//Create a random intialization vector and initialize
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
mcrypt_generic_init($td, $key, $iv);
// Create a Timestamp and add it.
$T = new \DateTime('NOW');
$message = $T->format("YmdHis") . $message;
// PKCS7 Padding
//get the block size of the cipher
$b = mcrypt_get_block_size('tripledes', 'ecb');
//What is the purpose?
$dataPad = $b-(strlen($message)%$b);
$message .= str_repeat(chr($dataPad), $dataPad);
//convert to hexidec string
$encrypted_data = bin2hex(mcrypt_generic($td, $message));
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $encrypted_data;
}
Short Solution:
public function Encrypt($message, $key = 'defaultkey') {
$T = new \DateTime('NOW');
return bin2hex(mcrypt_encrypt(MCRYPT_3DES, $key, $T->format("YmdHis").$message, 'ecb'));
}
The only real difference is the padding. Triple DES is a symmetric block cipher and such only operates on a single full block (8 byte). A mode of operation like ECB enables it to encrypt many full blocks. When your data is not a multiple of the block size, it has to be padded for it to be encrypted.
MCrypt uses zero padding by default. It will fill up the plaintext with 0x00 bytes until a multiple of the block size is reached. Those additional padding bytes have to be removed during decryption (usually done with rtrim()). This means that if the plaintext ends with 0x00 bytes, those will also be removed which might break your plaintext.
PKCS#5/PKCS#7 padding on the other hand pads with a byte that represents the number of padding bytes. If the plaintext is already a multiple of the block size, it will add a full block of padding. Doing it this way enables it to only remove the padding and not additional plaintext bytes during decryption.
Whether mcrypt_generic_init() or mcrypt_encrypt() was used doesn't really make a difference.
You should never use ECB mode. It is not semantically secure. It means that the same plaintext block will always result in the same ciphertext block. Since you're encrypting URLs the first couple of blocks will stay the same for similar URLs after observing many ciphertexts. An attacker might get additional information out of this.
Use at least CBC mode with a random IV. The IV doesn't need to be hidden, so it can be easily prepended to the ciphertext and sliced off during decryption.
It is also best to have the ciphertext authenticated to detect manipulation. You can use a message authentication code such as HMAC-SHA256 with a different key. A better way would be to simply use an authenticated mode such as GCM or EAX.

AES encryption inconsistency in PHP using the Open SSL library and coding CBC mode from scratch

I suppose I should deal with my first statement first. Is there an inconsistency in the way PHP does AES-128-CBC encryption when you use the Open SSL library?
I am asking this because if you look through RFC3602 for AES then you'll find some test vectors in section 4. The first one I tried was:
Case #1: Encrypting 16 bytes (1 block) using AES-CBC with 128-bit key
Key : 0x06a9214036b8a15b512e03d534120006
IV : 0x3dafba429d9eb430b422da802c9fac41
Paintext : "Single block msg"
Cphertext: 0xe353779c1079aeb82708942dbe77181a
Using the open SSL library with the following code:
echo bin2hex(openssl_encrypt($str, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $iv));
You actually get back:
e353779c1079aeb82708942dbe77181ab97c825e1c785146542d396941bce55d
Now this is all well and good and decrypts back just fine, and the first 32 bytes are the expected cipher text "e353779c1079aeb82708942dbe77181a". However, there appears to be another 32 bytes at the end of the cipher text returned from the Open SSL library. Why is this? This question becomes relevant next.
As part of something I am doing for fun I am trying to implement AES CBC mode encryption sort of from scratch. The code I currently have for doing this is (based on the RFC previously referenced and on CBC diagrams at the "Block cipher mode of operation" Wikipedia page):
public function cbcEncrypt($str, $iv, $key) {
$bl = 16;
$bs = str_split($str,$bl);
$pv = null;
$r = '';
foreach($bs as $b) {
if($pv === null) $pv = $iv;
if(strlen($b)<$bl) $b = $this->pkcs7Pad($b, $bl);
$pv = openssl_encrypt($b^$pv, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);
$r .= $pv;
}
return $r;
}
At the moment I'm pretty sure this code wont work on strings longer than the current test 16 byte string anyway, however, I ran this code side by side with the Open SSL implementation to compare. The return value from by CBC implementation is:
e353779c1079aeb82708942dbe77181a8b1ccc6f8cd525ffe22d6327d891a063
When called via:
$crypto = new CryptoTools();
$enc = $crypto->cbcEncrypt('Single block msg',hex2bin("3dafba429d9eb430b422da802c9fac41"),hex2bin("06a9214036b8a15b512e03d534120006"));
Again, the first 32 bytes are correct, but the second 32 bytes are again added but completely different from the Open SSL implementation of CBC. Any clues as to why?
Also, the Open SSL implementation of ECB mode returns a 32 byte string, which means CBC encrypting strings currently greater than 16 bytes in length the new value of $pv will be 32 bytes in length and then it will be XOR'd against the second plain text block when in reality shouldn't $pv always be 16 bytes in length? Is it usually just accepted that you truncate $pv from 32 to 16 bytes?
Just in case it helps, the pkcs7Pad method in the code above looks like:
public function pkcs7Pad($str, $b = 8) {
if(trim($str) == '') return false;
if((int)$b < 1) return false;
$p = $b - (strlen($str) % $b);
return $str . str_repeat(chr($p),$p);
}
Any help would be greatly appreciated. Needless to say there's not much documentation on this as people aren't usually stupid enough to try and reinvent the wheel, certainly in PHP...
This has to do with padding. The test vectors don't use any padding at all, while the openssl_encrypt function and your function apply PKCS#7 padding.
PKCS#7 padding is specified so that padding is added in all cases. When the plaintext length is a multiple of the block size, a full block of padding is added, which is why the ciphertext is 32 bytes long when the plaintext is 16 bytes.
Can't immediately see anything wrong with your function (though I'm not familiar with PHP), but you could try padding $str before splitting it instead of checking whether each block requires padding.
Ah ha! Got it! Thanks to ntoskrnl's answer and this Stackoverflow question I managed to come up with a solution! If you do:
echo bin2hex(base64_decode(openssl_encrypt($str, 'aes-128-cbc', $key, OPENSSL_ZERO_PADDING, $iv)));
You will match the test case provided by the RFC! You just need to remember that because you're now not doing OPENSSL_RAW_DATA you will get back a base64 encoded string and so will need to base64 decode it before converting it to hex to match the test case!
TLDR: it was a padding issue, just not an obvious one!

AESCrypt decryption between iOS and PHP

I am having a heck of a time figuring out how to decrypt a string encrypted with the NSData+AESCrypt.m (Explained here)
I have been looking at a handful of other threads, but I only need the iDevice to send a string to a PHP file encrypted, and then it gets decrypted inside PHP (where it gets stored into a database).
This code :
NSString *encryptedString = [#"Hello" AES256EncryptWithKey:#"a16byteslongkey!"];
NSLog(#"The strign encrypted : %#",encryptedString);
Returns the string encrypted : 7opqbb7sEVNoXplyQv/X8g==
And here is my PHP code for decryption:
function decrypt_data($data, $key) {
return mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key,$data,MCRYPT_MODE_ECB);
}
function unpadPKCS7($data, $blockSize) {
$length = strlen ( $data );
if ($length > 0) {
$first = substr ( $data, - 1 );
if (ord ( $first ) <= $blockSize) {
for($i = $length - 2; $i > 0; $i --)
if (ord ( $data [$i] != $first ))
break;
return substr ( $data, 0, $i );
}
}
return $data;
}
function decrypt_string($string) {
$string = unpadPKCS7($string,128);
$string = decrypt_data($string,"a16byteslongkey!");
return $string;
}
die('<br>Basic :'.decrypt_string('7opqbb7sEVNoXplyQv/X8g=='));
UPDATE:
Been doing some MD5 decryption and experimenting a lot, but still far from achieving usable results. This is what I got so far:
Original string : Hello
AES256Encrypt result : 7opqbb7sEVNoXplyQv/X8
base64_decode Decrypted: îŠjm¾ìSh^™rBÿ×
mcrypt_rijndael_128 : Õ¯Öå«Ž(ás2’'u)
mcrypt_rijndael_128 & hex2bin : UÃ)ı+úy´e
Sadly, no matter how I bend and twist this, I just get jibberish. Can anyone see what I'm doing wrong?
Disclaimer: I have zero iPhone development experience.
Short answer - what tc. said. Something is horribly wrong with the AES256EncryptWithKey:
Being AES256 you would expect it to require a 32 byte key, not a 16 byte key. But OK, say it pads shorter keys with null bytes to make them 32 bytes. This might explain why your 16 byte key is being padded with 16 null characters.
But, when it comes to the actual act of encryption, it's using AES 128, but with the 32 byte key. Say wha?
Converting tc.'s Python to PHP:
$base64encoded_ciphertext = '7opqbb7sEVNoXplyQv/X8g==';
$key = 'a16byteslongkey!';
$padded_key = $key . str_repeat(chr(0x00), 16); // Argh!
$result = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $padded_key, base64_decode($base64encoded_ciphertext), 'ecb');
// Yetch - $result ends up being padded with 0x0b's (vertical tab).
var_dump(rtrim($result, chr(0x0b)));
Result:
string(5) "Hello"
~~
Edit: This post from Henno has some relevant details.
~~
Did some additional research. The null padding on your key is likely because AES256 requires a 32 byte key. The 0x0B padding on the plaintext is thanks to PKCS7. PKCS7 is a padding scheme where the byte used for padding is equal in value to the number of bytes added. In this example, 11 bytes were added to the end of 'Hello' turning your 5 byte input into a 16 byte block for AES. 11 = 0x0B.
Thus, the code above will not work when the plaintext is not length = 5. Try the following instead:
$pad_char = ord(substr($result, -1));
$result_without_padding = substr($result, 0, strlen($result) - $pad_char);
The encrypted string looks like it's been base64 encoded. Try decoding it before you decrypt it.
First off, the Objective-C code you're using is pretty terrible:
The keyspace is severely limited (presumably UTF-8 bytes terminated by a null byte, extended with null bytes to 32 bytes). The easiest way to generate a random key is to stick to ASCII, which limits you to about 223.6 bits for the default key size of 256 bits.
Encryption is done in ECB mode.
Data appears to be irreversibly padded with 0x0B.
Avoid it at all costs. It is not secure.
It can be "decrypted" in Python with something like this:
>>> import Crypto.Cipher.AES
>>> import base64
>>> Crypto.Cipher.AES.new('a16byteslongkey!'+'\0'*16).decrypt(base64.b64decode('7opqbb7sEVNoXplyQv/X8g=='))
'Hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'
see my post here: PHP iOS AES Encryption
I just got through this same sort of project. I used the library you referenced in "also considered..."
Here is some example code to decrypt with php:
$iv2 = '';
for($i=0;$i<16;$i++){
$iv2 .= "\0";
}
$plain_text_CBC = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encrypted_text, MCRYPT_MODE_CBC, $iv2);
var_dump($plain_text_CBC);
Make sure your keys are both 256-bit (32 characters, I have not yet had any encoding issues, but if you do, remember that you are encrypting bytes, not characters). Note that 128 in MCRYPT_RIJNDAEL_128 is the block size and not the key size, while in the method AES256DecryptWithKey, 256 is a reference to the key size, while the block size is 128. AES256DecryptWithKey runs in CBC mode, but has a null initialization vector (iv).
CBC means that each block depends on the last block, and so it uses a pre-set, usually random, "block -1" called the IV
ECB means that each block is encrypted in the same way, hence it reveals when two blocks in the same message are the same. The library mentioned does not use it, so I mentioned it just for contrast.
The use of a zero iv (0000000000000000 in bytes) is considered insecure, but it does provide you with some additional security (but one might still be able to tell if the fist 16 characters of your plain text were the same each time). To fix this you would have to create an NSData *iv variable for the IV and modify the CCcrypt argument of NSData+AESCrypt.m to add [iv bytes] for the iv parameter (I have not yet tested this code), and you would need to store this iv and pass it to the php along with you message. But first I would test and have everything working with a zero iv.

Categories