Encrypting in PHP (mcrypt), Decrypting in Ruby (OpenSSL::Cipher) - php

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.

Related

How to create openssl encryption and decryption equivalent of php code in nodejs application

I have an application running on php which have some values encrypted using openssl encrption by using the code below
<?php
define('OSSLENCKEY','14E2E2D1582A36172AE401CB826003C1');
define('OSSLIVKEY', '747E314D23DBC624E971EE59A0BA6D28');
function encryptString($data) {
$encrypt_method = "AES-256-CBC";
$key = hash('sha256', OSSLENCKEY);
$iv = substr(hash('sha256', OSSLIVKEY), 0, 16);
$output = openssl_encrypt($data, $encrypt_method, $key, 0, $iv);
$output = base64_encode($output);
return $output;
}
function decryptString($data){
$encrypt_method = "AES-256-CBC";
$key = hash('sha256', OSSLENCKEY);
$iv = substr(hash('sha256', OSSLIVKEY), 0, 16);
$output = openssl_decrypt(base64_decode($data), $encrypt_method, $key, 0, $iv);
return $output;
}
echo encryptString("Hello World");
echo "<br>";
echo decryptString("MTZHaEoxb0JYV0dzNnptbEI2UXlPUT09");
?>
I have another endpoint which runs on nodejs where I need to decrypt and encrypt values based on the above php encrypt/decrypt rule.
I have searched but could'nt find a solution for this.
I tried with the library crypto But ends up with errors Reference
My nodejs code which I have tried is given below
message = 'MTZHaEoxb0JYV0dzNnptbEI2UXlPUT09';
const cypher = Buffer.from(message, "base64");
const key = crypto.createHash('sha256').update('14E2E2D1582A36172AE401CB826003C1');//.digest('hex');
// $iv = substr(hash('sha256', '747E314D23DBC624E971EE59A0BA6D28'), 0, 16); from php returns '0ed9c2aa27a31693' need nodejs equivalent
const iv = '0ed9c2aa27a31693';
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
console.log( decipher.update(contents) + decipher.final());
Someone please help me to find a nodejs code for openssl encryption and decyption
Thanks in advance
There are the following problems in the code:
The key is returned hex encoded in the PHP code, so in the NodeJS code for AES-256 only the first 32 bytes must be considered for the key (PHP does this automatically).
The PHP code Base64 encodes the ciphertext implicitly, so because of the explicit Base64 encoding the ciphertext is Base64 encoded twice (which is unnecessary). Therefore, a double Base64 encoding is necessary in the NodeJS code as well.
Also, note that using a static IV is insecure (but you are probably only doing this for testing purposes).
The following NodeJS code produces the same ciphertext as the PHP code:
const crypto = require('crypto');
const plain = 'Hello World';
const hashKey = crypto.createHash('sha256');
hashKey.update('14E2E2D1582A36172AE401CB826003C1');
const key = hashKey.digest('hex').substring(0, 32);
const hashIv = crypto.createHash('sha256');
hashIv.update('747E314D23DBC624E971EE59A0BA6D28');
const iv = hashIv.digest('hex').substring(0, 16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
var encrypted = cipher.update(plain, 'utf-8', 'base64');
encrypted += cipher.final('base64');
encrypted = Buffer.from(encrypted, 'utf-8').toString('base64');
console.log(encrypted); // MTZHaEoxb0JYV0dzNnptbEI2UXlPUT09
encrypted = Buffer.from(encrypted, 'base64').toString('utf-8');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
var decrypted = decipher.update(encrypted, 'base64', 'utf-8');
decrypted += decipher.final('utf-8');
console.log(decrypted); // Hello World
EDIT:
As mentioned in the comments, PHP's hash() method returns the hash as a hexadecimal string by default (unless the third parameter is explicitly set to true, which is not the case in the reference code). This doubles the length, because in this encoding each byte of the hash is represented by two hex digits (hexits), i.e. 2 bytes.
Therefore it is necessary to shorten the key in the NodeJS code (see the first point of my original answer). This shortening is not necessary in the PHP code, since PHP does this implicitly (which is actually a design flaw, since this way the user does not notice a possible issue with the key).
The use of the hex string has two disadvantages:
With a hex encoded string, each byte consists of 16 possible values (0-15), as opposed to 256 possible values of a byte (0-255). This reduces the security from 256 bit to 128 bit (which is arithmetically equivalent to AES-128), see here.
Depending on the platform, the hexits a-f can be represented as lowercase or uppercase letters, which can result in different keys and IVs (without explicit agreement on one of the two cases).
For these reasons it is more secure and robust to use the raw binary data of the hash instead of the hex encoded strings. If you want to do this, then the following changes are necessary.
In the PHP code:
$key = hash('sha256', OSSLENCKEY, true);
$iv = substr(hash('sha256', OSSLIVKEY, true), 0, 16);
in the NodeJS code:
const key = hashKey.digest();
const iv = hashIv.digest().slice(0, 16)
Note, however, that this version is not compatible with the old one, i.e. encryptions before this change cannot be decrypted after the change. So the old data would have to be migrated.

Node.js AES-256-CBC encryption issue

I'm trying to create an encrypted string according to one sample code which is given in PHP, my main problem is Node.js crypto module doesn't accept keys with more than 32 bytes length but PHP openssl_encrypt does, it seems that's why I'm getting Invalid key size error.
here is my js code:
let iv = sha1(await HelpersService.makeRandomNumber(null, null, 16));
iv = iv.substr(0, 16);
const text = bundledData;
const password = sha1(this.credentials.appSecret);
let salt = sha1(await HelpersService.makeRandomNumber(null, null, 4));
salt = salt.substr(0, 4);
const key = crypto.createHash('sha256').update(password + salt).digest('hex');
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encoded = cipher.update(text, 'utf8', 'hex');
encoded += cipher.final('hex');
and this is the PHP sample:
function generateCashOutAPIHashKey($app_secret ){
//remove plus(+) sign from gsm number.
$data = 'text';
$iv = substr(sha1(mt_rand()), 0, 16);
$password = sha1($app_secret);
$salt = substr(sha1(mt_rand()), 0, 4);
$saltWithPassword = hash('sha256', $password . $salt);
$encrypted = openssl_encrypt("$data", 'aes-256-cbc', "$saltWithPassword", null, $iv );
return $encrypted;
}
The key generated in the PHP and NodeJS code has a length of 64 bytes. The used encryption AES-256 needs a 32 bytes key. In the PHP code, openssl_encrypt() implicitly shortens the 64 bytes key to 32 bytes. In the NodeJS code this has to be done explicitly:
const key = crypto.createHash('sha256').update(password + salt).digest('hex').substr(0, 32);
Also, openssl_encrypt() returns the ciphertext Base64 encoded by default. In the NodeJS code the result is returned hex encoded. Here you have to change the output encoding from 'hex' to 'base64' in the update() and final() call:
let encoded = cipher.update(text, 'utf8', 'base64');
encoded += cipher.final('base64');
Please note that the PHP reference code has a number of vulnerabilities:
mt_rand() is not a cryptographically secure pseudorandom number generator (CSPRNG). PHP provides cryptographically secure methods for deriving a random IV / salt, e.g. random_bytes() or random_int().
The key is inferred using SHA-256. It is more secure to use a reliable key derivation function such as PBKDF2. A 4 bytes salt is generally too small.
SHA1 is considered insecure in most contexts and must no longer be used.
Anyone have a problem with the following ?
crypto.createHash('sha256').update(password +
salt).digest('hex').substr(0, 32);
I understand the intent of needing a 32 byte value produced by sha256 but if you hex encode and then take the first 32 characters of the string you have SIGNIFICANTLY reduced the value of this. You now have a string 32 characters long that has only 16 possible values (0-9a-f) for each character. You really want a 32 byte value that has 256 possible values for each byte. You have changed your key security of your encryption key from the intended 256^32 (1.1E77) to 16^32 (3.4E38).
crypto.createHash('sha256').update('').digest('hex');
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
crypto.createHash('sha256').update('').digest('hex').substr(0,32);
'e3b0c44298fc1c149afbf4c8996fb924'
Find a way to use the byte values produced by SHA256.

How to convert mcrypt_generic to openssl_encrypt?

this is my php code in PHP7.0.30
$key = strtoupper(md5('799ae002c7e940ef8a890b3a428f8f458e3f7c39d1cc2bf24390f0c46cf932c8'));
$text ='name=王星星&mobile=15212345678&idNumber=620402198709215456&bankName=招商银行&bankNum=6214830100799652';
$plaintext = $text;
$size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
//PKCS5Padding
$padding = $size - strlen($plaintext) % $size;
$plaintext .= str_repeat(chr($padding), $padding);
$module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = str_repeat("\0", $size);
/* Intialize encryption */
mcrypt_generic_init($module, base64_decode($key), $iv);
/* Encrypt data */
$encrypted = mcrypt_generic($module, $plaintext);
/* Terminate encryption handler */
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
var_dump(base64_encode($encrypted));
/* openssl_encrypt */
$encrypted = openssl_encrypt($plaintext, 'AES-256-CBC',base64_decode($key), OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,$iv);
var_dump(base64_encode($encrypted));
mcrypt_generic output: 8LZZEXRhAfeeQOxF1iI9GpBcA2hSCelrUq2OimhSgZly6RfRonzGiE32vHh/JkdK+X5N5hFBMKz+iOmWAbgL9BIu2GIAxBIXCOusxFU4eDJ/5uy7F9vR9EE5NqOAiHBZhTP3pzMtEc0fLAzg8Tsngg==
but openssl_encrypt output:
g/YBzu+SGy9jfR+DIVY2S0iGM2O0QEs+J3IEv7bNAoz7+3iX9FboJZT0h+OH6uUeQBoSsD+eAga69U5C86Ibcp5Q2ay1FzfDFG/eGBtUaAJxRBwhxiNeBxPw2jBar2fR42wZUZOGTjlT5Ujgz+s/Iw==
I don't know how to convert mcrypt_generic to openssl_encrypt , thanks!
You need a 256 bit key. Your current "key" is 32 bytes which is 256 bits.
You problem is that your are decoding the key. When you decode the key you are reducing the key's size to 24 bytes or 192 bits.
So you have two options.
Increase the key size by another 8 bytes before you base64_encode() it.
OR
Just remove the base64_decode() functions.
Secondly, I hope you are using the md5 for testing purposes and not for your application. The md5 hash is not suited to give you a hash that is designed for modern cryptography.
You should use something like $key = openssl_random_pseudo_bytes($size); to generate a key with.
I would also like to point you in the direction of the Libsodium library. It is now native on the latest version of PHP.
UPDATED
If your goal is to have AES encryption with a 256 bit key like your code suggest, then you will have to do what I stated above. If that is not one of your requirements then the only thing you have to do is change the AES-256-CBC to AES-192-CBC in the openssl_encrypt() function like so.
$encrypted = openssl_encrypt($plaintext, 'AES-192-CBC', base64_decode($key), OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
One thing to point out is that the "128" in the name MCRYPT_RIJNDAEL_128 refers to the block size not the key size, where as the "256" in AES-256-CBC actually refers to the key size. Which is why the AES-192-CBC will work for your current key size of 192 bits. AES encryption uses a standard of a 128 bit block size, so that is compatible the MCRYPT_RIJNDAEL_128 block size.
Cheers

openssl_encrypt returns false

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

PHP secured code with SHA1 and AES256

I have one issue, that I'm not able to solve...
Instructions:
Create hash with SHA1 from string
Take first 16 bytes from string and encode them with AES256 and KEY
You get 16 bytes signature. You have to convert this signature to 32-bytes string, which represent signature in hexadecimal
My function:
public function GetSign($str) {
$strSIGN = sha1($str, true);
$strSIGN = substr($strSIGN, 0, 16);
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
mcrypt_generic_init($td, self::KEY, $iv);
$strSIGN = mcrypt_generic($td, substr($strSIGN, 0, 16));
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
$strSIGNhex = '';
for ($i = 0; $i < strlen($strSIGN); $i++)
{
$ord = ord($strSIGN[$i]);
$hexCode = dechex($ord);
$strSIGNhex .= ((strlen($hexCode) == 1) ? '0' : '') . $hexCode;
}
return $strSIGNhex;
}
But the result is incorrect...
Any suggestions?
Are you sure, the result is incorrect? AES256 returns different values based on the iv, which is in your case random.
Its completely acceptable to have different signatures after different executions - the only requirement is, you can verify, that the output is correct.
I'm not sure what problem this instructions should solve. As far as i understand, you want to generate a signature, using a hash and a key. This problem is normally solved with a HMAC.
With your code you won't be able to recreate the signature to do a verification, because you used the CBC mode with an IV (initialisation vector). This is actually a good thing, but you would have to store the IV too, so you could use the same IV to encrypt another string and do the verification. Of course storing the IV would result in a much longer string than 16 bytes.
Either you use the ECB mode, which doesn't need an IV, or you use the HMAC which is made for such situations.

Categories