openssl_decrypt fails. cipher expects an IV of precisely 16 bytes - php

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.

Related

Exception handling for openssl decrypt In php

I need help for decryption algorithm in php.
I got error when I pass wrong encrypt data in my decryption function. so i need to handle error using exception. can it possible?
I tried to this code but no luck
function decrypt($encrypt_data) {
$key = ENC_KEY;
$encryption_key = base64_decode($key);
list($encrypted_data, $iv) = explode('::', base64_decode($encrypt_data), 2);
try {
if(openssl_decrypt($encrypted_data, 'aes-256-cbc', $encryption_key, 0, $iv)) {
throw new customException($encrypted_data);
}
}
catch (customException $e) {
echo $e->errorMessage();
}
}
But this function gives me error:-
Warning: openssl_decrypt(): IV passed is only 7 bytes long, cipher
expects an IV of precisely 16 bytes, padding with \0 in abc.php
As Magnus indicates, the key and IV should be specific sizes for AES in CBC mode. The key size should be 32 bytes for AES-256 and the IV is identical to the block size: 16 bytes.
Generally any language will validate the size of the key and IV before using it, but PHP has had the nasty habit (for both the OpenSSL and mcrypt API's) of padding it with zero's if it is too small or cutting part of it when it is too large. This is probably because the underlying C-libraries do not validate the size either as the key and IV are passed as pointers rather than arrays or objects.
To fix this you should make sure that the key is exactly 32 randomized bytes (apparently it is because there is no warning) and that the IV consists of exactly 16 randomized bytes.
Note that it is a security requirement for CBC mode that the IV consists of 16 bytes that are fully unpredictable to an adversary. Just fixing the error by padding the IV with zeros yourself is therefore not recommended. Although the use of 7 bytes as IV is unlikely to directly lead to exploits you should fix the protocol rather than to rely on workarounds in the code.
As indicated, other cryptographic API's (for different languages) will strictly enforce to use keys and IV's of the correct size; if you keep using a 7 byte IV then all those runtimes may need workarounds as well.

Decrypt mcrypt with openssl

Since mcrypt is considered obsolete, my task is upgrading the current code to use openssl. Sounds simple, but ... after a few days of try and failure I feel like going insane.
My question to you is: Is there any way you can decrypt with openssl data previously encrypted with mcrypt? I've read so many posts on this matter and most of them say that a previous manual padding of the data was/is necessary before running mcrypt on it.
The issue is that the mcrypt-ed data is already encrypted (with the automatic null padding mcrypt provides) and resides in a database, so modification of that is not possible and/or desired.
Mentions:
the algorithm used is rijndael-128 cbc with a 32-byte key (so I'm using aes-256-cbc for openssl).
I'm using an openssl wrapper for php (php-crypto).
I've managed to make the inverse operation work (decode openssl with mcrypt) by simply stripping the end decoded characters if they were non alpha-numerical.
Manually padding the data before mcrypt-ing and then decrypting it using openssl works like a charm, but that's not the problem here.
Some code snippets:
// Simple mcrypt encrypt, decrypt with php-crypto example
// This doesn't work and produces a "Finalizing of cipher failed" error
$data = "This is a text";
$strMcryptData=mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv);
$algorithm = 'aes-256-cbc';
$cipher = new Cipher($algorithm);
$sim_text = $cipher->decrypt($strMcryptData, $key, $iv);
// Simple mcrypt encrypt with padding, decrypt with php-crypto
// Works and produces the correct text on decryption
$pad = $blocksize - (strlen($data) % $blocksize);
$text = $data;
$text .= str_repeat(chr($pad), $pad);
$strPaddedData=mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $text, MCRYPT_MODE_CBC, $iv);
$sim_text = $cipher->decrypt($strPaddedData, $key, $iv);
Slightly old, but you can solve this with a bit of work. You can tell PHP's OpenSSL that the encrypted string is not padded, and tell it to give you the raw output (So you don't have to base64 decode it, either). You can then strip nulls from the end of the resulting string if the length of the string happens to be perfectly divisible by the IV (This is a sanity check, as if the resulting string isn't divisible by the IV then it wasn't padded at all).
Be aware, this code has two major limitations:
If, at any point, you encrypted a legitimate string that ended in two or more NULL bytes then this code will not give you the same output.
If the padding of the string needed only one null byte, then this code won't strip it.
You can solve both of these if you know for a FACT that you didn't encrypt anything that ends in null bytes, you can alter the code that strips the nulls to just do a preg_replace; just make sure you anchor the regex to the end of the string so it only strips from the end.
<?php
$message = 'test';
$key = openssl_random_pseudo_bytes(16);
$iv = openssl_random_pseudo_bytes(16);
$cipher = mcrypt_encrypt(
MCRYPT_RIJNDAEL_128,
$key,
$message,
MCRYPT_MODE_CBC,
$iv
);
$plain = openssl_decrypt(
$cipher,
'aes-128-cbc',
$key,
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING,
$iv
);
//try to detect null padding
if (mb_strlen($iv, '8bit') % mb_strlen($plain, '8bit') == 0) {
preg_match_all('#([\0]+)$#', $plain, $matches);
if (mb_strlen($matches[1][0], '8bit') > 1) {
$plain = rtrim($plain, "\0");
trigger_error('Detected and stripped null padding. Please double-check results!');
}
}
var_dump(
$message,
bin2hex($cipher),
$plain,
mb_strlen($message, '8bit'),
mb_strlen($plain, '8bit'),
$message === $plain
);
http://3v4l.org/kYAXn
Obviously this code comes with no major disclaimer and please test it in your use case, but someone might hopefully find this useful.
If you encrypt in mcrypt without adding PKCS7 manually, mcrypt will happily pad your plaintext with NUL bytes.
OpenSSL will do PKCS7 padding for you whenever using aes-X-cbc. The unfortunate consequence of this is that if you have AES-CBC(NULL_PADDED(plaintext)) and try to decrypt it, openssl_decrypt will attempt to remove the padding and fail.
Compare http://3v4l.org/bdQe9 vs http://3v4l.org/jr68f and http://3v4l.org/K6ZEU
The OpenSSL extension does not currently offer you a way to say "This string is not padded, please don't strip the padding for me" and then remove the NUL bytes on your own. You must encrypt with PKCS7 padding in order for decryption to succeed.
Although this is a limitation of OpenSSL, it bears emphasizing that the only reason you're running into it is because mcrypt is terrible.
There shouldn't be any major differences except for the padding. You should be able to call EVP_CIPHER_CTX_set_padding if you use the higher level OpenSSL (EVP) constructs directly. I presume that the padding argument should be zero, although it is not documented. You need a preconfigured encryption/decryption context for this.
Afterwards you will have your plaintext of the same length as the ciphertext. Zero to fifteen bytes at the end will be set to zero. You need to remove these bytes manually. If the plaintext happens to end with zero bytes then those will also be removed; that's however never the case if the plaintext is a printable string (that uses 8 bit encoding). You may want to ensure that you don't remove more than 15 bytes.
If you get completely random plaintext then your key or ciphertext is incorrect. If you get readable plaintext but for the first 16 bytes then your IV handling is incorrect.

AES encryption differences between php mcrypt and a Delphi component

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 !

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!

Categories