From 95f2ce5eb2a1ba81e52549014f9de1037303073e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 5 Nov 2017 18:25:28 +0000 Subject: [PATCH 1/2] Add GCM support to encrypter --- src/Illuminate/Encryption/Encrypter.php | 166 ++++++++++++++++++++---- tests/Encryption/EncrypterTest.php | 28 +++- 2 files changed, 162 insertions(+), 32 deletions(-) diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 61670ea38a61..b5616073b3b1 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -9,6 +9,18 @@ class Encrypter implements EncrypterContract { + /** + * The supported cipher algorithms and their settings. + * + * @var array + */ + private static $supportedCiphers = [ + 'aes-128-cbc' => ['size' => 16, 'aead' => false], + 'aes-256-cbc' => ['size' => 32, 'aead' => false], + 'aes-128-gcm' => ['size' => 16, 'aead' => true, 'since' => '7.1.0'], + 'aes-256-gcm' => ['size' => 32, 'aead' => true, 'since' => '7.1.0'], + ]; + /** * The encryption key. * @@ -23,6 +35,13 @@ class Encrypter implements EncrypterContract */ protected $cipher; + /** + * Whether the cipher is AEAD cipher. + * + * @var bool + */ + protected $aead; + /** * Create a new encrypter instance. * @@ -35,12 +54,15 @@ class Encrypter implements EncrypterContract public function __construct($key, $cipher = 'AES-128-CBC') { $key = (string) $key; + $cipher = strtolower($cipher); if (static::supported($key, $cipher)) { $this->key = $key; $this->cipher = $cipher; + $this->aead = self::$supportedCiphers[$this->cipher]['aead']; } else { - throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.'); + $ciphers = implode(', ', array_map('strtoupper', array_keys(self::$supportedCiphers))); + throw new RuntimeException("The only supported ciphers are $ciphers with the correct key lengths."); } } @@ -53,10 +75,18 @@ public function __construct($key, $cipher = 'AES-128-CBC') */ public static function supported($key, $cipher) { - $length = mb_strlen($key, '8bit'); + if (! isset(self::$supportedCiphers[$cipher])) { + return false; + } + + $cipherSetting = self::$supportedCiphers[$cipher]; + if (isset($cipherSetting['since']) && + version_compare(PHP_VERSION, $cipherSetting['since'], '<') + ) { + return false; + } - return ($cipher === 'AES-128-CBC' && $length === 16) || - ($cipher === 'AES-256-CBC' && $length === 32); + return mb_strlen($key, '8bit') === $cipherSetting['size']; } /** @@ -67,7 +97,7 @@ public static function supported($key, $cipher) */ public static function generateKey($cipher) { - return random_bytes($cipher == 'AES-128-CBC' ? 16 : 32); + return random_bytes(self::$supportedCiphers[$cipher]['size'] ?? 32); } /** @@ -83,13 +113,56 @@ public function encrypt($value, $serialize = true) { $iv = random_bytes(openssl_cipher_iv_length($this->cipher)); + if ($serialize) { + $value = serialize($value); + } + $json = json_encode( + $this->aead ? $this->encryptAead($value, $iv) : $this->encryptMac($value, $iv) + ); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new EncryptException('Could not encrypt the data.'); + } + + return base64_encode($json); + } + + /** + * Encrypt value using AEAD cipher. + * + * @param string $value + * @param string $iv + * @return array + */ + protected function encryptAead($value, $iv) + { + // We will encrypt AEAD ciphers which will give us authentication tag. + $value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv, $tag); + + if ($value === false) { + throw new EncryptException('Could not encrypt the data.'); + } + + return [ + 'iv' => base64_encode($iv), + 'value' => $value, + 'tag' => base64_encode($tag), + ]; + } + + /** + * Encrypt value using non AEAD cipher and MAC. + * + * @param string $value + * @param string $iv + * @return array + */ + protected function encryptMac($value, $iv) + { // 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 - ); + $value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv); if ($value === false) { throw new EncryptException('Could not encrypt the data.'); @@ -100,13 +173,7 @@ public function encrypt($value, $serialize = true) // 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); + return compact('iv', 'value', 'mac'); } /** @@ -134,11 +201,54 @@ public function decrypt($payload, $unserialize = true) $payload = $this->getJsonPayload($payload); $iv = base64_decode($payload['iv']); + $decrypted = $this->aead + ? $this->decryptAead($payload, $iv) + : $this->decryptMac($payload, $iv); + return $unserialize ? unserialize($decrypted) : $decrypted; + } + + /** + * Decrypt value using AEAD cipher. + * + * @param array $payload + * @param string $iv + * @return string + */ + protected function decryptAead($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( + // we will return it. If we are nable to decrypt this value, it means that the tag + // is invalid and we will throw out an exception message. + $decrypted = openssl_decrypt( + $payload['value'], $this->cipher, $this->key, 0, $iv, base64_decode($payload['tag']) + ); + + if ($decrypted === false) { + throw new DecryptException('The authentication tag is invalid.'); + } + + return $decrypted; + } + + /** + * Decrypt value using non AEAD cipher and MAC. + * + * @param array $payload + * @param string $iv + * @return string + */ + protected function decryptMac($payload, $iv) + { + // First we will check if the MAC is valid + if (! $this->validMac($payload)) { + throw new DecryptException('The MAC is invalid.'); + } + + // Here we will decrypt the value. If we are able to successfully decrypt it + // we will return it. If we are unable to decrypt this value we will throw out + // an exception message (this should however never happen for AES CBC mode). + $decrypted = openssl_decrypt( $payload['value'], $this->cipher, $this->key, 0, $iv ); @@ -146,7 +256,7 @@ public function decrypt($payload, $unserialize = true) throw new DecryptException('Could not decrypt the data.'); } - return $unserialize ? unserialize($decrypted) : $decrypted; + return $decrypted; } /** @@ -186,15 +296,11 @@ protected function getJsonPayload($payload) // If the payload is not valid JSON or does not have the proper keys set we will // assume it is invalid and bail out of the routine since we will not be able - // to decrypt the given value. We'll also check the MAC for this encryption. + // to decrypt the given value. if (! $this->validPayload($payload)) { throw new DecryptException('The payload is invalid.'); } - if (! $this->validMac($payload)) { - throw new DecryptException('The MAC is invalid.'); - } - return $payload; } @@ -206,9 +312,13 @@ protected function getJsonPayload($payload) */ protected function validPayload($payload) { - return is_array($payload) && isset( - $payload['iv'], $payload['value'], $payload['mac'] - ); + if (! is_array($payload)) { + return false; + } + + return $this->aead + ? isset($payload['iv'], $payload['value'], $payload['tag']) + : isset($payload['iv'], $payload['value'], $payload['mac']); } /** diff --git a/tests/Encryption/EncrypterTest.php b/tests/Encryption/EncrypterTest.php index 3cf5db6d92ed..e8a7d064c12c 100755 --- a/tests/Encryption/EncrypterTest.php +++ b/tests/Encryption/EncrypterTest.php @@ -44,9 +44,19 @@ public function testWithCustomCipher() $this->assertEquals('foo', $e->decrypt($encrypted)); } + public function testAeadCipher() + { + $this->onlyForAead(); + + $e = new Encrypter(str_repeat('b', 32), 'AES-256-GCM'); + $encrypted = $e->encrypt('bar'); + $this->assertNotEquals('bar', $encrypted); + $this->assertEquals('bar', $e->decrypt($encrypted)); + } + /** * @expectedException \RuntimeException - * @expectedExceptionMessage The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths. + * @expectedExceptionMessageRegExp /The only supported ciphers are (AES-\d{3}-[A-Z]{3}(, )?)+ with the correct key lengths./ */ public function testDoNoAllowLongerKey() { @@ -55,7 +65,7 @@ public function testDoNoAllowLongerKey() /** * @expectedException \RuntimeException - * @expectedExceptionMessage The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths. + * @expectedExceptionMessageRegExp /The only supported ciphers are (AES-\d{3}-[A-Z]{3}(, )?)+ with the correct key lengths./ */ public function testWithBadKeyLength() { @@ -64,7 +74,7 @@ public function testWithBadKeyLength() /** * @expectedException \RuntimeException - * @expectedExceptionMessage The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths. + * @expectedExceptionMessageRegExp /The only supported ciphers are (AES-\d{3}-[A-Z]{3}(, )?)+ with the correct key lengths./ */ public function testWithBadKeyLengthAlternativeCipher() { @@ -73,7 +83,7 @@ public function testWithBadKeyLengthAlternativeCipher() /** * @expectedException \RuntimeException - * @expectedExceptionMessage The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths. + * @expectedExceptionMessageRegExp /The only supported ciphers are (AES-\d{3}-[A-Z]{3}(, )?)+ with the correct key lengths./ */ public function testWithUnsupportedCipher() { @@ -102,4 +112,14 @@ public function testExceptionThrownWithDifferentKey() $b = new Encrypter(str_repeat('b', 16)); $b->decrypt($a->encrypt('baz')); } + + /** + * Run test only for AEAD. + */ + private function onlyForAead() + { + if (version_compare(PHP_VERSION, 'PHP-7.1', '<')) { + $this->markTestSkipped('The AEAD is not supported in PHP 7.0'); + } + } } From 14424a9b2f0da1709a8ae615a915f57bd43aa99e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 5 Nov 2017 19:06:35 +0000 Subject: [PATCH 2/2] Improve listing of available ciphers and skip GCM test for 7.0 --- src/Illuminate/Encryption/Encrypter.php | 21 ++++++++++++++++++++- tests/Encryption/EncrypterTest.php | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index b5616073b3b1..03c765c8a427 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -61,7 +61,7 @@ public function __construct($key, $cipher = 'AES-128-CBC') $this->cipher = $cipher; $this->aead = self::$supportedCiphers[$this->cipher]['aead']; } else { - $ciphers = implode(', ', array_map('strtoupper', array_keys(self::$supportedCiphers))); + $ciphers = implode(', ', $this->getAvailableCiphers()); throw new RuntimeException("The only supported ciphers are $ciphers with the correct key lengths."); } } @@ -350,6 +350,25 @@ protected function calculateMac($payload, $bytes) ); } + /** + * Get available ciphers. + * + * @return array + */ + private function getAvailableCiphers() + { + $availableCiphers = []; + foreach (self::$supportedCiphers as $cipherName => $setting) { + if (! isset($setting['since']) || + version_compare(PHP_VERSION, $setting['since'], '>=') + ) { + $availableCiphers[] = strtoupper($cipherName); + } + } + + return $availableCiphers; + } + /** * Get the encryption key. * diff --git a/tests/Encryption/EncrypterTest.php b/tests/Encryption/EncrypterTest.php index e8a7d064c12c..ee432addd0d2 100755 --- a/tests/Encryption/EncrypterTest.php +++ b/tests/Encryption/EncrypterTest.php @@ -118,7 +118,7 @@ public function testExceptionThrownWithDifferentKey() */ private function onlyForAead() { - if (version_compare(PHP_VERSION, 'PHP-7.1', '<')) { + if (version_compare(PHP_VERSION, '7.1', '<')) { $this->markTestSkipped('The AEAD is not supported in PHP 7.0'); } }