Published | 16/08/2021 |
---|---|
Last Updated | 20/11/2024 |
On a recent project I have to make a request from a Rails app to a Laravel app. The Laravel app requires payload data to be encrypted and serialized in PHP style. While the situation is not ideal, I have interesting time working on the issue and learn about Laravel encryption and a few PHP functions.
Laravel uses Illuminate Encrypter for encrypt/decrypt purposes. Let look at the code for this encrypter/decrypter available at https://github.com/laravel/framework/blob/5.5/src/Illuminate/Encryption/Encrypter.php
I copy here the encrypt and decrypt functions, the main functions of the class:
/**
* Encrypt the given value.
*
* @param mixed $value
* @param bool $serialize
* @return string
*
* @throws \\\\Illuminate\\\\Contracts\\\\Encryption\\\\EncryptException
*/
public function encrypt($value, $serialize = true)
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
// First we will encrypt the value using OpenSSL. After this is encrypted we
// will proceed to calculating a MAC for the encrypted value so that this
// value can be verified later as not having been changed by the users.
$value = \\\\openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher, $this->key, 0, $iv
);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
// Once we get the encrypted value we'll go ahead and base64_encode the input
// vector and create the MAC for the encrypted value so we can then verify
// its authenticity. Then, we'll JSON the data into the "payload" array.
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'));
if (json_last_error() !== JSON_ERROR_NONE) {
throw new EncryptException('Could not encrypt the data.');
}
return base64_encode($json);
}
/**
* Decrypt the given value.
*
* @param mixed $payload
* @param bool $unserialize
* @return string
*
* @throws \\\\Illuminate\\\\Contracts\\\\Encryption\\\\DecryptException
*/
public function decrypt($payload, $unserialize = true)
{
$payload = $this->getJsonPayload($payload);
$iv = base64_decode($payload['iv']);
// Here we will decrypt the value. If we are able to successfully decrypt it
// we will then unserialize it and return it out to the caller. If we are
// unable to decrypt this value we will throw out an exception message.
$decrypted = \\\\openssl_decrypt(
$payload['value'], $this->cipher, $this->key, 0, $iv
);
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
return $unserialize ? unserialize($decrypted) : $decrypted;
}
Reading this Encrypter class gives me rough ideas on how the encryption/decryption work. So the real task here is to replicate the encrypt
function in Rails.
I was provided with an APP_KEY
with the format base64:xxx..
. A quick search reveals this is used as secret key in Laravel app config similar to Rails secret key. And next to it in app config is cipher
. So now I know the key and cipher used in the encryption process.
// config/app.php
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
Also from a quick scan through the functions we could see that the encrypter uses OpenSSL for encryption/descryption purposes and all of those encrypt/descrypt functions are available in PHP crypto extensions (see https://www.php.net/manual/en/ref.openssl.php)
Let move into the first line of encrypt
function:
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
This line checks for cipher initialization vector iv
length. Basically if cipher === 'AES-256-CBC'
iv length should be 32 characters, and if cipher === 'AES-128-CBC'
it should be 16 characters. Then it generates a sequence of random bytes that match with the cipher iv length.
The equivalent for the line above in Ruby should be:
require 'openssl'
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.iv = cipher.random_iv
Move to the next line
$value = \\\\openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher, $this->key, 0, $iv
);
If we look into the function arguments, by default $serialize
is true, that means we need to serialize input. However serialization in Ruby and PHP are different. For example:
# A hash in Ruby
{
foo: 'bar',
baz: 'bazz'
}
# equivalent in PHP
[
"foo" => "bar",
"baz" => "bazz"
]
A quick solution to this is to use PHP serialize gem which wrap the PHP searialize
and unserialize
functions https://github.com/jqr/php-serialize
Also, we should notice the 0
value in the openssl_encrypt
arguments. It is described as options
in PHP manual: