I am working on integrating an API to our web-application. On the initial request, the API returns a response that is encrypted using PBEWithMD5AndTripleDES encryption, and then base 64 encoded. I have an encryption password that is provided to me beforehand. Because of my lack of experience and PBEWithMD5AndTripleDES documentation, I am struggling to decrypt the response. I have tried using phpseclib without any luck.
This is my code with phpseclib
$res = $response->getBody()->getContents();
$res = base64_decode($res);
// this is provided by vendor
$password = self::PASSWORD;
// I tried this too.
//$password = md5(utf8_encode($password), true);
$tripleDes = new TripleDES(TripleDES::MODE_CBC);
$tripleDes->setKey($password);
$ddd = $tripleDes->decrypt($res);
// this is returning false
var_dump($ddd); die();
Can you please provide me some examples of how to use PBEWithMD5AndTripleDES in PHP or point me to some direction or documentation.
PBEWithMD5AndTripleDES uses an MD5 based algorithm for key / IV derivation, which expects a password, a salt and an iteration count as parameters. For encryption TripleDES in CBC mode (des-ede3-cbc) with a 24 bytes key is applied.
PBEWithMD5AndTripleDES is an Oracle proprietary extension of the password-based encryption defined in PKCS#5 (RFC 8018) to support longer keys, here. Because it is proprietary and because of the outdated algorithms like MD5 and the relatively slow TripleDES compared to AES, it should not be used for new implementations, but only for compatibility with legacy code.
I have not found any PHP library on the web that supports PBEWithMD5AndTripleDES out-of-the-box (only for the different PBEWithMD5AndDES, e.g. here). For a custom implementation you actually only need the derivation of the key / IV. So if you don't find an implementation either, but you have compelling reasons to use this algorithm: Here is a Java code that implements the derivation. A port to PHP could be:
function deriveKeyIV($key, $salt, $count){
$result = "";
for ($var = 0; $var < 4; $var++){
if($salt[$var] != $salt[$var + 4])
break;
}
if ($var == 4){
for ($var = 0; $var < 2; $var++){
$tmp = $salt[$var];
$salt[$var] = $salt[3 - $var];
$salt[3 - 1] = $tmp;
}
}
for ($var = 0; $var < 2; $var++){
$toBeHashed = substr($salt, $var * (strlen($salt) / 2), strlen($salt) / 2);
for ($var2 = 0; $var2 < $count; $var2++){
$toBeHashed = hash ("MD5", $toBeHashed . $key, TRUE);
}
$result = $result . $toBeHashed;
}
return $result;
}
The function returns 32 bytes, of which the first 24 bytes are the key and the last 8 bytes are the IV. With this key and IV the encryption with TripleDES in CBC mode can then be performed.
Example:
$keyIv = deriveKeyIV(hex2bin("01026161afaf0102fce2"), hex2bin("0788fe53cc663f55"), 65536);
$key = substr($keyIv, 0, 24);
$iv = substr($keyIv, 24, 8);
print(bin2hex($key) . "\n");
print(bin2hex($iv) . "\n");
print(openssl_encrypt("The quick brown fox jumps over the lazy dog", "des-ede3-cbc", $key, 0, $iv));
Output:
543650085edbbd6c26149c53a57cdd85871fd91c0f6d0be4
d7ffaa69502309ab
m4pye0texirKz1OeKqyKRJ5fSgWcpIPEhSok1SBDzgPthsw9XUuoiqXQBPdsVdUr
As reference I used a Java implementation, more precisely the implementation of PBEWithMD5AndTripleDES of the SunJCE provider, which gives the same result.
Note that the original implementation of PBEWithMD5AndTripleDES only allows a salt that is exactly 8 bytes in size (although the derivation function can handle larger salts), otherwise an exception is thrown (salt must be 8 bytes long). To add this constraint, the following can be added at the beginning of deriveKeyIV:
if (strlen($salt) != 8) {
throw new Exception('Salt must be 8 bytes long');
}
Related
I am trying to encrypt a string using openssl_encrypt in PHP but it keeps returning FALSE.
$encrypted = openssl_encrypt('1234', 'AES-256-CBC', 'kGJeGF2hEQ', OPENSSL_ZERO_PADDING, '1234123412341234');
What am I doing wrong?
On top of answers posted, which are excellent, the code you're after, given your input parameters would be the following:
$plaintext = '1234';
$cipher = 'AES-256-CBC';
$key = 'this is a bad key';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
$encrypted = openssl_encrypt($plaintext, $cipher, $key, 0, $iv);
if(false === $encrypted)
{
echo openssl_error_string();
die;
}
$decrypted = openssl_decrypt($encrypted, $cipher, $key, 0, $iv);
$result = $decrypted === $plaintext;
print $result ? 'Everything is fine' : 'Well, we did not decrypt good, did we?';
Having written the above, I advise against using it and instead, please use a tested library designed to handle the complexities of encryption and decryption for you.
I suggest using defuse/php-encryption
php > var_dump (openssl_encrypt('1234', 'AES-256-CBC', 'kGJeGF2hEQ', OPENSSL_ZERO_PADDING, '1234123412341234'));
php shell code:1:
bool(false)
php > var_dump (openssl_error_string ());
php shell code:1:
string(94) "error:0607F08A:digital envelope routines:EVP_EncryptFinal_ex:data not multiple of block length"
It seems that the cypher you're using requires that the data you're encrypting has a length that's an exact multiple of the block length. With some experimentation I found that 1234123412341234 is successfully encrypted.
I don't know if this is a universal feature of all openssl encryption schemes, or whether it's something that's specific to certain schemes. In the former case you'll need to pad the input to a multiple of the block size. If the latter is true then you can either pad, or switch to a different encryption scheme that doesn't impose the same restrictions on the input.
For padding you need to find out what the blocksize of your chosen cypher is (I don't know if there's an openssl function or constant provided for that), then work out how many characters you need to pad your input string by.
Note that the following example assumes that a) there's some way of getting the blocksize programmatically (if not then you'll have to hard-code that yourself) and b) you're working with a byte-oriented character format (unicode might cause issues!)
$plaintext = "Hello, I'm some plaintext!";
$blocksize = function_that_gets_a_blocksize_for_a_given_cypher ($cypher);
$strlen = strlen ($plaintext);
$pad = $blocksize - ($strlen % $blocksize);
// If the string length is already a multiple of the blocksize then we don't need to do anything
if ($pad === $blocksize) {
$pad = 0;
}
$plaintext = str_pad ($plaintext, $strlen + $pad);
As for your code, this suggests you need to implement some error detection into it (but be careful what you actually log/echo out as part of the error detection!).
$encrypted = openssl_encrypt('1234', 'AES-256-CBC', 'kGJeGF2hEQ', OPENSSL_ZERO_PADDING, '1234123412341234');
if (false === $encrypted) {
error_log ("Encryption failed! " . openssl_error_string ());
}
Since block ciphers such as AES require input data to be an exact multiple of the block size (16-bytes for AES) padding is necessary. The usual method is just to specify PKCS#7 (née PKCS#5) by passing it as an option and the padding will be automatically added on encryption and removed on decryption. Zero padding (OPENSSL_ZERO_PADDING) is a poor solution since it will not work for binary data.
The IV needs to be block size, 8-bytes for AES. Do not rely on the implementation for padding.
The key should be the exact size specified, valid block sizes foe AES are 128, 192 or 256 bits (16, 24 or 32 bytes). Do not rely on the implementation for padding.
Before start fixing this bug, check all extension which is required for openssl_encrypt/decrypt is enabled?
class AnswerEncryption
{
const CURRENT_ALGO = 'AES-128-ECB';
const CIPHER='A?N#G+KbPe778mYq3t6w9z$C&F!J#jcQ';
CONST IV='1234567890123455';
/**
* #param null $Value
* #param null $cipher
* #return false|string
*/
public static function Encrypt($Value=null){
$iv = substr(self::IV, 0, 16);
return (openssl_encrypt($Value,self::CURRENT_ALGO,self::CIPHER,0,$iv));
}
/**
* #param null $Value
* #return int
*/
public static function Decrypt($Value=null): int
{
$iv = substr(self::IV, 0, 16);
return intval(openssl_decrypt($Value,self::CURRENT_ALGO,self::CIPHER,0,$iv));
}
}
in the decrypt method, I want the integer value, so you can change it accordingly
I'm trying to migrate some legacy PHP code to ruby, and I've encountered a problem with some 3DES encryption. This is the PHP implementation using mcrypt:
function encrypt_3DES($message, $key){
$bytes = array(0,0,0,0,0,0,0,0); //byte [] IV = {0, 0, 0, 0, 0, 0, 0, 0}
$iv = implode(array_map("chr", $bytes)); //PHP 4 >= 4.0.2
$ciphertext = mcrypt_encrypt(MCRYPT_3DES, $key, $message, MCRYPT_MODE_CBC, $iv);
return $ciphertext;
}
and this is my ruby code:
def encrypt_3DES(message, key)
des=OpenSSL::Cipher.new('des3')
des.encrypt
des.key = key
des.update(message)+des.final
end
However results are slightly different (base64 encoded):
//PHP
ZpgH7NWpRx+Mi6tDBZ9q2Q==
# Ruby
ZpgH7NWpRx/usGDIsQ+A8A==
As you can see it's the lowest portion of the string's bytes that differs. Any pointers are much appreciated.
I answer my own question.
It was an issue about how openssl and mcrypt implementations use padding. My cryptography knowledge isn't too deep, but I found a usable code sample here http://opensourcetester.co.uk/2012/11/29/zeros-padding-3des-ruby-openssl/
#ENCRYPTION
block_length = 8
des.padding = 0 #Tell Openssl not to pad
des.encrypt
json = '{"somekey":"somevalue"}'
json += "\0" until json.bytesize % block_length == 0 #Pad with zeros
edata = des.update(json) + des.final
b64data = Base64.encode64(edata).gsub("\n",'')
Basically, ruby openssl will use PKCS padding, while mcrypt uses 0 padding. So in our code I had to tell openssl not to pad the string with des.padding = 0 and then do the padding manually: json += "\0" until json.bytesize % block_length == 0.
Those are the important bits that were missing in my original implementation.
Could the PHP combination of MCRYPT_3DES and MCRYPT_MODE_CBC not be equivalent to the ruby OpenSSL library's 3des (or des-ede3-cbc). There are examples of other mismatches.
Try one of the other modes, such as des-ede-cbc or des-cbc (See full list here)
Similar to this question, I've migrated a site over to use VPS protocol 3.00.
I have done my testing against the test site with the test encryption key and all works well. When I switch this over to use the live site with the live encryption key, I get the dreaded 3045 : The Currency field is missing error.
The same encryption key works on the live site when using VPS protocol 2.22, but not when it's switched to 3.00.
I have also fed the post data to a decryption script that is using the same key to decrypt the crypt without issue.
Can anyone think of why the code would work against test., but not live. with the appropriate key, or why 2.22 accepts the key and 3.0 does not? Is live. doing any extra checking with 3.00 that test. isn't?
My code has slightly modified the functions from the integration kit:
function addPKCS5Padding($input) {
$blockSize = 16;
$padd = "";
// Pad input to an even block size boundary.
$length = $blockSize - (strlen($input) % $blockSize);
for ($i = 1; $i <= $length; $i++) {
$padd .= chr($length);
}
return $input . $padd;
}
// AES encryption, CBC blocking with PKCS5 padding then HEX encoding.
function sagepay_encrypt($string, $key) {
// Add PKCS5 padding to the text to be encrypted.
$string = addPKCS5Padding($string);
// Perform encryption with PHP's MCRYPT module.
$crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $key);
// Perform hex encoding and return.
return "#" . strtoupper(bin2hex($crypt));
}
It is called by populating the $crypt_values array and calling:
$crypt_source = sagepay_buildcrypt($crypt_values);
$crypt = sagepay_encrypt($crypt_source, $sagepay_key);
The $crypt_source is valid and (largely) the same in all cases:
VendorTxCode=20150721020857Deannatest&VendorData=Deanna test&Amount=1&Currency=GBP&Description=Quote Reference Deanna test&BillingSurname=Earley&BillingFirstnames=Deanna&BillingAddress1.....
Looking at the encryption password, its too short by one character. I've updated it by adding 'X' (upper case) to the end, so just update your value accordingly.
I've given it a try and it is now fine.
I'm assuming you are using the password that begins '3Gd' (if not let me know).
How can I generate key and IV each from a common string, that they may serve in the encryption algorithm using the AES in PHP?
Example:
$key_string = derivate_in_valid_key("i love stackoverflow");
$iv_string = derivate__in_valid_iv("i love questions");
and in a way that I can repeat the derivation process in JavaScript also.
You can use PBKDF2 to derive a key and IV from a password. CryptoJS and PHP both provide implementations thereof.
AES supports key sizes of 128, 192 and 256 bits, and a block size of 128-bit. The IV must be the same size as the block size for modes such as CBC.
Single invocation of PBKDF2 to generate only the key
The following code works also to generate the IV. Doing so, requires a second random salt that must be sent alongside the ciphertext.
JavaScript (DEMO):
var password = "test";
var iterations = 500;
var keySize = 256;
var salt = CryptoJS.lib.WordArray.random(128/8);
console.log(salt.toString(CryptoJS.enc.Base64));
var output = CryptoJS.PBKDF2(password, salt, {
keySize: keySize/32,
iterations: iterations
});
console.log(output.toString(CryptoJS.enc.Base64));
example output:
CgxEDCi5z4ju1ycmKRh6aw==
7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=
PHP:
$password = "test";
$expected = "7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=";
$salt = 'CgxEDCi5z4ju1ycmKRh6aw==';
$hasher = "sha1"; // CryptoJS uses SHA1 by default
$iterations = 500;
$outsize = 256;
$out = hash_pbkdf2($hasher, $password, base64_decode($salt), $iterations, $outsize/8, true);
echo "expected: ".$expected."\ngot: ".base64_encode($out);
output:
expected: 7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=
got: 7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=
Single invocation of PBKDF2 to generate key and IV
The previous section is a bit clunky, because one needs to generate two salts and do two invocations of PBKDF2. PBKDF2 supports a variable output, so it is possible to simply use one salt, request an output of the key size plus iv size and slice them off. The following code does that, so only one salt must be sent alongside of the ciphertext.
JavaScript (DEMO):
var password = "test";
var iterations = 1000;
// sizes must be a multiple of 32
var keySize = 256;
var ivSize = 128;
var salt = CryptoJS.lib.WordArray.random(128/8);
console.log(salt.toString(CryptoJS.enc.Base64));
var output = CryptoJS.PBKDF2(password, salt, {
keySize: (keySize+ivSize)/32,
iterations: iterations
});
// the underlying words arrays might have more content than was asked: remove insignificant words
output.clamp();
// split key and IV
var key = CryptoJS.lib.WordArray.create(output.words.slice(0, keySize/32));
var iv = CryptoJS.lib.WordArray.create(output.words.slice(keySize/32));
console.log(key.toString(CryptoJS.enc.Base64));
console.log(iv.toString(CryptoJS.enc.Base64));
example output:
0Iulef2TncciKGmdwvQX3Q==
QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=
L1YNlFe54+Cvepp/pXsHtg==
PHP:
$password = "test";
$expectedKey = "QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=";
$expectedIV = "L1YNlFe54+Cvepp/pXsHtg==";
$salt = '0Iulef2TncciKGmdwvQX3Q==';
$hasher = "sha1";
$iterations = 1000;
$keysize = 256;
$ivsize = 128;
$out = hash_pbkdf2($hasher, $password, base64_decode($salt), $iterations, ($keysize+$ivsize)/8, true);
// split key and IV
$key = substr($out, 0, $keysize/8);
$iv = substr($out, $keysize/8, $ivsize/8);
// print for demonstration purposes
echo "expected key: ".$expectedKey."\ngot: ".base64_encode($key);
echo "\nexpected iv: ".$expectedIV."\ngot: ".base64_encode($iv);
output:
expected key: QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=
got: QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=
expected iv: L1YNlFe54+Cvepp/pXsHtg==
got: L1YNlFe54+Cvepp/pXsHtg==
CryptoJS uses SHA1 by default, so you could use a different hash function by passing the hash function as the hasher object property. You should also probably use a higher iteration count.
I don't have a PHP 5.5+ version available so I used the PBKDF2 implementation from here.
I'm working on a cross language project wrapping a ruby/Sinatra API in PHP to be consumed by another team. None of the information exposed by the API is sensitive, but we would prefer it not be easily accessible to a casual observer guessing the URL.
private function generateSliceIDToken($key){
$currentEpoch = time();
$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($ivSize, MCRYPT_RAND);
$encryptedBytes = mcrypt_encrypt(
MCRYPT_RIJNDAEL_128,
$key,
$currentEpoch.**Passcode**,
MCRYPT_MODE_CBC, $iv
);
$ivAndEncryptedBytes = $iv . $encryptedBytes;
return urlencode(urlencode(base64_encode($ivAndEncryptedBytes)));
The code above Encrypts a password and time stamp using mcrypt's RIJNDAEL implementation and encodes it to send off to the ruby API
if identifier.validate_token Base64.decode64(URI.unescape( URI.unescape(params[:token])))
Sinatra grabs it and decodes it
def validate_token(token)
cipher = OpenSSL::Cipher::AES.new(128, 'CBC')
cipher.decrypt
cipher.key = **key**
cipher.iv = token[0,16]
plain = cipher.update(token[16..-1]) + cipher.final
return plain[10,8] == **Passcode**
end
and passes it along to be decrypted
The problem is, the decryption fails with a 'Bad Decrypt' Error
I was lead to believe Mcrypt's RIJNDAEL and Cipher's AES were compatible, but is this assumption incorrect? Any help I can get one this would be most helpful.
I was lead to believe Mcrypt's RIJNDAEL and Cipher's AES were compatible, but is this assumption incorrect?
You need to slightly tweak data being encoded to make it AES compatible. Data must be right padded, with character and amount depending of its current width:
$encode = $currentEpoch.'**Passcode**';
$len = strlen($encode);
$pad = 16 - ($len % 16);
$encode .= str_repeat(chr($pad), $pad);
Also remember to have $key exactly 16 characters long. If it is shorter, ruby throws CipherError, while php pads key with null bytes. If it is longer, ruby uses only first 16 character but php pads it again, and uses last 16 characters.