What's behind Laravel Encryption/Decryption
My system is safe, it uses encryption.
You heard/said this from time to time. Sure, but why and how is it safe? Do you really know this?
Laravel encryption/decryption fundamentals
Laravel encryption/decryption is based on the Illuminate\Encryption\Encrypter class, which is constructed passing an encryption key and a cipher (i.e. the encryption algorithm):
__construct($key, $cipher = 'AES-128-CBC')
It supports (among the others) the following main methods:
encrypt($value, $serialize = true)decrypt($payload, $unserialize = true)
which, not surprisingly, are used to encrypt and decrypt data.
1$encrypter = new Illuminate\Encryption\Encrypter('1234567812345678', 'AES-128-CBC');
2
3$encrypted = $encrypter->encrypt('Hello world');
4dump($encrypted);
5// prints something similar to "eyJpdiI6ImdMd2dWcW5jMXBrUDBranRJZXQ5MEE9PSIsInZhbHVlIjoiNnhTODBSclB3ZVp3SFRRUWFWTHpReFQwYWQ1aXVmTmhXOXV5WHM2TzR1WT0iLCJtYWMiOiIwODQyZDhiMzZlNDQwZTZjYTRiYmI2MGE0MTgzNzk5NGNkZTU1Yzc5NDIyYzdjYmYwNzk2ZTA5MGNjYjc4MGYzIn0="
6
7$decrypted = $encrypter->decrypt($encrypted);
8dump($decrypted);
9// prints "Hello world" again
This is great! And it is enough to use it in the best way.
However, if you want to know what happens under the hood, keep reading.
Note that the results wouldn't be the very same since some values are computed randomly, so changes at every run.
How encryption works
Laravel's encrypter currently uses OpenSSL for performing AES-256 and AES-128 encryption. It also uses Message Authentication Code (MAC) protection, a mechanism to ensure data has not been tampered with after encryption.
What's in the result?
Recalling the previous example, you may think that the encrypted string "eyJpdiI6ImdMd2dWcW5jMXBrUDBranRJZXQ5MEE9PSIsInZhbHVlIjoiNnhTODBSclB3ZVp3SFRRUWFWTHpReFQwYWQ1aXVmTmhXOXV5WHM2TzR1WT0iLCJtYWMiOiIwODQyZDhiMzZlNDQwZTZjYTRiYmI2MGE0MTgzNzk5NGNkZTU1Yzc5NDIyYzdjYmYwNzk2ZTA5MGNjYjc4MGYzIn0=" is by itself the ciphered version of the input. This is definitely true, but there is more to know about.
Indeed, it is a base64 conversion of a string. "What string?" you may ask... And you can get an answer by simply run:
1$encrypted = $encrypter->encrypt('Hello world');
2$decodedEncrypted = base64_decode($encrypted);
which results in a json string similar to the following:
1{
2 "iv":"gLwgVqnc1pkP0kjtIet90A==",
3 "value":"6xS80RrPweZwHTQQaVLzQxT0ad5iufNhW9uyXs6O4uY=",
4 "mac":"0842d8b36e440e6ca4bbb60a41837994cde55c79422c7cbf0796e090ccb780f3"
5}
Now is more opaque than clearer... What is this?
This document is composed of the three main parts of encryption:
value: the actual ciphered data, coded in base64iv: the Initialization Vector is a randomly generated fixed-size data sequence inject at each run, preventing semantic-based attacks, see (Initialization vector - Wikipedia for more details). It is base64-coded toomac: the Message Authentication Code is a signature used to detectvaluetampering, generated hashingvalueandiv. It is represented in hex-string format
Note that both iv and value are base64 encoded too since they are generic bytes sequences and may contain not printable values.
How encryption works - looking at the code
To understand how the payload is generated, let's give a closer look to the encrypt() method:
1 public function encrypt($value, $serialize = true)
2 {
3 $iv = random_bytes(openssl_cipher_iv_length($this->cipher));
4
5 $value = \openssl_encrypt(
6 $serialize ? serialize($value) : $value,
7 $this->cipher, $this->key, 0, $iv
8 );
9
10 if ($value === false) {
11 throw new EncryptException('Could not encrypt the data.');
12 }
13
14 $mac = $this->hash($iv = base64_encode($iv), $value);
15
16 $json = json_encode(compact('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);
17
18 if (json_last_error() !== JSON_ERROR_NONE) {
19 throw new EncryptException('Could not encrypt the data.');
20 }
21
22 return base64_encode($json);
23 }
Looking at the code, 5 steps are performed:
- Initialization Vector is generated on line
3by generating 128 or 256 bits (according to the used cipher) of random data - Encrypted Value is generated on lines
5-8by running OpenSSL over a (possibly) serialized version of the clear text data, using the chosen cipher, encryption key and IV. Note that the result is base64-coded - The MAC is generated by the
hash()method, fed with base64 iv and value. Hashing is defined as:
1 protected function hash($iv, $value)
2 {
3 return hash_hmac('sha256', $iv.$value, $this->key);
4 }
i.e. the SHA256 hashing of the concatenation of IV and value, using the provided encryption key.
4. An array containing iv, value and mac is generated and converted to json (line 16)
5. The json is encoded in base64 and finally returned (line 22)
How decryption works - in depth
To understand how original data is recovered, let's give a closer look at the decrypt() method:
1 public function decrypt($payload, $unserialize = true)
2 {
3 $payload = $this->getJsonPayload($payload);
4
5 $iv = base64_decode($payload['iv']);
6
7 // Here we will decrypt the value. If we are able to successfully decrypt it
8 // we will then unserialize it and return it out to the caller. If we are
9 // unable to decrypt this value we will throw out an exception message.
10 $decrypted = \openssl_decrypt(
11 $payload['value'], $this->cipher, $this->key, 0, $iv
12 );
13
14 if ($decrypted === false) {
15 throw new DecryptException('Could not decrypt the data.');
16 }
17
18 return $unserialize ? unserialize($decrypted) : $decrypted;
19 }
Looking at the code, 5 steps are performed:
- The json payload is extracted in line
3. During extraction, it is also validated by ensuring that:
1.1. It has an array form
1.2. It containsiv,valueandmacfields.
1.3.ivlengths is compatible with cipher requirements
1.4. Themacis valid - Data is decrypted using OpenSSL (lines
5-12) - Result is (possibly) unserialized and returned
Why is it secure?
This scheme provides security until the encryption key is kept secret. Let's see why:
- confidence: the clear text message can be recovered only by who knows the secret key
- integrity: if the value is modified, decryption fails. If iv and value are both modified, the message could be potentially decryptable, but MAC protection will detect tampering and decryption fails. In any case, varying any combination of iv and/or value and/or mac, decryption fails due to payload corruption.
- The only way to deceive MAC protection is by knowing the encryption key, which allows forging new valid ciphered full payloads.
Let's try: create a different encrypted message:
1$encrypted2 = $encrypter->encrypt('Hello hacker');
2$decodedEncrypted2 = json_decode(base64_decode($encrypted2), true);
3dump('DECODED ENCRYPTED 2: ');
4var_dump($decodedEncrypted2);
Now, in turn, try to tamper with one or more of the three values and attempt to decrypt the result.
1// swapping the ciphered data and try to decipher
2try {
3 $tampered = $decodedEncrypted;
4 $tampered['value'] = $decodedEncrypted2['value'];
5 $encrypter->decrypt(base64_encode(json_encode($tampered)));
6} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
7 dump($exception->getMessage());
8}
1// swapping the iv and try to decipher
2try {
3 $tampered = $decodedEncrypted;
4 $tampered['iv'] = $decodedEncrypted2['iv'];
5 $encrypter->decrypt(base64_encode(json_encode($tampered)));
6} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
7 dump($exception->getMessage());
8}
1// swapping the MAC and try to decipher
2try {
3 $tampered = $decodedEncrypted;
4 $tampered['mac'] = $decodedEncrypted2['mac'];
5 $encrypter->decrypt(base64_encode(json_encode($tampered)));
6} catch (\Illuminate\Contracts\Encryption\DecryptException $exception) {
7 dump($exception->getMessage());
8}
In each of the three cases, the MAC control will fail to prevent message decryption and DecryptException is raised.
Conclusion
Now you should understand more in detail how Laravel encryption works under the hoods. Nothing changes in how you use it, but you earned more confidence in the tools you used. Moreover, you can now justify with your customer "how" your system is safe.