From 3fef2e2135ae4f05d8d6d506f97fa150a338c44f Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 16 Dec 2020 13:27:33 -0800 Subject: [PATCH] (dev/core#2258) PhpseclibCipherSuite - Add default crypto provider --- Civi/Core/Container.php | 1 + Civi/Crypto/PhpseclibCipherSuite.php | 213 +++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 Civi/Crypto/PhpseclibCipherSuite.php diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 404405adad..6c8ac4ffbc 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -511,6 +511,7 @@ class Container { */ public static function createCryptoRegistry() { $crypto = new \Civi\Crypto\CryptoRegistry(); + $crypto->addCipherSuite(new \Civi\Crypto\PhpseclibCipherSuite()); $crypto->addPlainText(['tags' => ['CRED']]); if (defined('CIVICRM_CRED_KEYS')) { diff --git a/Civi/Crypto/PhpseclibCipherSuite.php b/Civi/Crypto/PhpseclibCipherSuite.php new file mode 100644 index 0000000000..6168330754 --- /dev/null +++ b/Civi/Crypto/PhpseclibCipherSuite.php @@ -0,0 +1,213 @@ +ciphers = []; + if (class_exists('\phpseclib\Crypt\AES')) { + // phpseclib v2 + $this->ciphers['aes-cbc'] = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_CBC); + $this->ciphers['aes-cbc']->setKeyLength(256); + $this->ciphers['aes-ctr'] = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_CTR); + $this->ciphers['aes-ctr']->setKeyLength(256); + } + elseif (class_exists('Crypt_AES')) { + // phpseclib v1 + $this->ciphers['aes-cbc'] = new \Crypt_AES(\Crypt_AES::MODE_CBC); + $this->ciphers['aes-cbc']->setKeyLength(256); + $this->ciphers['aes-ctr'] = new \Crypt_AES(\Crypt_AES::MODE_CTR); + $this->ciphers['aes-ctr']->setKeyLength(256); + } + else { + throw new CryptoException("Failed to find phpseclib"); + } + } + + /** + * @inheritdoc + */ + public function getSuites(): array { + return ['aes-cbc', 'aes-ctr', 'aes-cbc-hs', 'aes-ctr-hs']; + } + + /** + * @inheritdoc + */ + public function encrypt(string $plainText, array $key): string { + switch ($key['suite']) { + case 'aes-cbc-hs': + case 'aes-ctr-hs': + return $this->encryptThenSign($plainText, substr($key['suite'], 0, -3), 'sha256', $key['key']); + + case 'aes-cbc': + case 'aes-ctr': + return $this->encryptOnly($plainText, $key['suite'], $key['key']); + } + } + + /** + * @inheritdoc + */ + public function decrypt(string $cipherText, array $key): string { + switch ($key['suite']) { + case 'aes-cbc-hs': + case 'aes-ctr-hs': + return $this->authenticateThenDecrypt($cipherText, substr($key['suite'], 0, -3), 'sha256', $key['key']); + + case 'aes-cbc': + case 'aes-ctr': + return $this->decryptOnly($cipherText, $key['suite'], $key['key']); + } + } + + /** + * Given an master key, derive a pair of encryption+authentication keys. + * + * @param string $masterKey + * @return array + */ + protected function createEncAuthKeys($masterKey) { + return [ + hash_hmac('sha256', 'enc', $masterKey, TRUE), + hash_hmac('sha256', 'auth', $masterKey, TRUE), + ]; + } + + protected function encryptOnly($plainText, $suite, $key) { + $cipher = $this->createCipher($suite, $key); + $blockBytes = $cipher->getBlockLength() >> 3; + $iv = random_bytes($blockBytes); + $cipher->setIV($iv); + return $iv . $cipher->encrypt($plainText); + } + + protected function decryptOnly(string $cipherText, $suite, $key) { + $cipher = $this->createCipher($suite, $key); + $blockBytes = $cipher->getBlockLength() >> 3; + $iv = substr($cipherText, 0, $blockBytes); + $cipher->setIV($iv); + return $cipher->decrypt(substr($cipherText, $blockBytes)); + } + + /** + * @param string $plainText + * @param string $suite + * The encryption algorithms + * Ex: aes-cbc, aes-ctr + * @param string $digest + * The authentication algorithm + * Ex: sha256 + * @param string $masterKey + * Binary representation of the key + * + * @return string + * The concatenation of IV, ciphertext, signature + */ + protected function encryptThenSign($plainText, $suite, $digest, $masterKey) { + list ($encKey, $authKey) = $this->createEncAuthKeys($masterKey); + $cipher = $this->createCipher($suite, $encKey); + $blockBytes = $cipher->getBlockLength() >> 3; + $iv = random_bytes($blockBytes); + $cipher->setIV($iv); + $ivText = $iv . $cipher->encrypt($plainText); + $sig = hash_hmac($digest, $ivText, $authKey, TRUE); + $this->assertLen($this->getDigestBytes($digest), $sig); + return $ivText . $sig; + } + + /** + * @param string $cipherText + * Combined ciphertext (IV + encrypted text + signature) + * @param string $suite + * The encryption algorithms + * Ex: aes-cbc, aes-ctr + * @param string $digest + * The authentication algorithm + * Ex: sha256 + * @param string $masterKey + * Binary representation of the key + * + * @return string + * Decrypted text + * @throws CryptoException + * Throws an exception if authentication fails. + */ + protected function authenticateThenDecrypt($cipherText, $suite, $digest, $masterKey) { + list ($encKey, $authKey) = $this->createEncAuthKeys($masterKey); + $cipher = $this->createCipher($suite, $encKey); + $blockBytes = $cipher->getBlockLength() >> 3; + $digestBytes = $this->getDigestBytes($digest); + $sigExpect = substr($cipherText, -1 * $digestBytes); + $sigActual = hash_hmac($digest, substr($cipherText, 0, -1 * $digestBytes), $authKey, TRUE); + if (!hash_equals($sigActual, $sigExpect)) { + throw new CryptoException("Failed to decrypt token. Invalid digest."); + } + $cipher->setIV(substr($cipherText, 0, $blockBytes)); + return $cipher->decrypt(substr($cipherText, $blockBytes, -1 * $digestBytes)); + } + + /** + * @param $suite + * @param $key + * @return \phpseclib\Crypt\Base|\Crypt_Base + */ + protected function createCipher($suite, $key) { + if (!isset($this->ciphers[$suite])) { + throw new \RuntimeException("Cipher suite does not support " . $suite); + } + + $cipher = clone $this->ciphers[$suite]; + $this->assertLen($cipher->getKeyLength() >> 3, $key); + $cipher->setKey($key); + return $cipher; + } + + protected function getDigestBytes($digest) { + if ($digest === 'sha256') { + return 32; + } + throw new \RuntimeException('Unrecognized digest'); + } + + private function assertLen($bytes, $value) { + if ($bytes != strlen($value)) { + throw new \InvalidArgumentException("Malformed AES key"); + } + } + +} -- 2.25.1