From c1c000aa153ef838325d1b9fdf86951284f7a9a3 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Tue, 20 Jun 2023 07:12:07 +0100 Subject: [PATCH] MDL-71421 core: deprecate openssl fallbacks in encryption library. Since c66dc591 the PHP Sodium library is required, negating the need for the OpenSSL equivalent. Remove fallbacks where possible, leaving only the ability to decrypt legacy OpenSSL-encrypted content (with debugging). --- admin/cli/generate_key.php | 6 +--- lib/classes/encryption.php | 66 +++++++++++++++------------------- lib/tests/encryption_test.php | 82 +++++++++++++++++++++++-------------------- lib/upgrade.txt | 5 +++ 4 files changed, 79 insertions(+), 80 deletions(-) diff --git a/admin/cli/generate_key.php b/admin/cli/generate_key.php index 9d5e8e5294b..3934f30dc8d 100644 --- a/admin/cli/generate_key.php +++ b/admin/cli/generate_key.php @@ -39,8 +39,6 @@ if ($unrecognized) { cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); } -// TODO: MDL-71421 - Remove the openssl alternative once sodium becomes a requirement in Moodle 4.2. - if ($options['help']) { echo "Generate secure key @@ -54,9 +52,7 @@ may be manually installed on multiple servers. Options: -h, --help Print out this help ---method Generate key for specified encryption method instead of default. - * sodium - * openssl-aes-256-ctr +--method Generate key for specified encryption method instead of default (sodium) Example: php admin/cli/generate_key.php diff --git a/lib/classes/encryption.php b/lib/classes/encryption.php index aff8599e4e5..5ae20478896 100644 --- a/lib/classes/encryption.php +++ b/lib/classes/encryption.php @@ -14,14 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Class used to encrypt or decrypt data. - * - * @package core - * @copyright 2020 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core; /** @@ -30,47 +22,51 @@ namespace core; * @package core * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @deprecated since Moodle 3.11 MDL-71420 - the openssl part of the class only. - * @todo MDL-71421 Remove the openssl part in Moodle 4.2. */ class encryption { /** @var string Encryption method: Sodium */ const METHOD_SODIUM = 'sodium'; - // TODO: MDL-71421 - Remove the following openssl constants and all uses once sodium becomes a requirement in Moodle 4.2. - - /** @var string Encryption method: hand-coded OpenSSL (less safe) */ + /** + * @var string Encryption method: hand-coded OpenSSL (less safe) + * + * @deprecated + */ const METHOD_OPENSSL = 'openssl-aes-256-ctr'; - /** @var string OpenSSL cipher method */ + /** + * @var string OpenSSL cipher method + * + * @deprecated + */ const OPENSSL_CIPHER = 'AES-256-CTR'; /** * Checks if Sodium is installed. * * @return bool True if the Sodium extension is available + * + * @deprecated since Moodle 4.3 Sodium is always present */ public static function is_sodium_installed(): bool { + debugging(__FUNCTION__ . '() is deprecated, sodium is now always present', DEBUG_DEVELOPER); return extension_loaded('sodium'); } /** - * Gets the encryption method to use. We use the Sodium extension if it is installed, or - * otherwise, OpenSSL. + * Gets the encryption method to use * * @return string Current encryption method */ protected static function get_encryption_method(): string { - if (self::is_sodium_installed()) { - return self::METHOD_SODIUM; - } else { - return self::METHOD_OPENSSL; - } + return self::METHOD_SODIUM; } /** * Creates a key for the server. * + * Note we currently retain support for all methods, in order to decrypt legacy {@see METHOD_OPENSSL} content + * * @param string|null $method Encryption method (only if you want to create a non-default key) * @param bool $chmod If true, restricts the file access of the key * @throws \moodle_exception If the server already has a key, or there is an error @@ -178,6 +174,8 @@ class encryption { /** * Gets the length in bytes of the initial values data required. * + * Note we currently retain support for all methods, in order to decrypt legacy {@see METHOD_OPENSSL} content + * * @param string $method Crypto method * @return int Length in bytes */ @@ -210,6 +208,12 @@ class encryption { $method = self::get_encryption_method(); } + // We currently retain support for all methods, falling back to Sodium if deprecated OpenSSL is requested. + if ($method === self::METHOD_OPENSSL) { + debugging('Encryption using legacy OpenSSL is deprecated, reverting to Sodium', DEBUG_DEVELOPER); + $method = self::METHOD_SODIUM; + } + // Create IV. $iv = random_bytes(self::get_iv_length($method)); @@ -223,22 +227,6 @@ class encryption { } break; - case self::METHOD_OPENSSL: - // This may not be a secure authenticated encryption implementation; - // administrators should enable the Sodium extension. - $key = self::get_key($method); - if (strlen($key) !== 32) { - throw new \moodle_exception('encryption_invalidkey', 'error'); - } - $encrypted = @openssl_encrypt($data, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); - if ($encrypted === false) { - throw new \moodle_exception('encryption_encryptfailed', 'error', - '', null, openssl_error_string()); - } - $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true); - $encrypted .= $hmac; - break; - default: throw new \coding_exception('Unknown method: ' . $method); } @@ -251,6 +239,8 @@ class encryption { /** * Decrypts data using the server's key. The decryption works with either supported method. * + * Note currently we retain support for all methods, in order to decrypt legacy {@see METHOD_OPENSSL} content + * * @param string $data Data to decrypt * @return string Decrypted data */ @@ -306,6 +296,8 @@ class encryption { '', null, 'Integrity check failed'); } + debugging('Decryption using legacy OpenSSL is deprecated, please upgrade to Sodium', DEBUG_DEVELOPER); + $decrypted = @openssl_decrypt($encrypted, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); if ($decrypted === false) { throw new \moodle_exception('encryption_decryptfailed', 'error', diff --git a/lib/tests/encryption_test.php b/lib/tests/encryption_test.php index 7fc05634885..f106159bd30 100644 --- a/lib/tests/encryption_test.php +++ b/lib/tests/encryption_test.php @@ -14,24 +14,19 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Test encryption. - * - * @package core - * @copyright 2020 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace core; +use advanced_testcase; + /** * Test encryption. * * @package core * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core\encryption */ -class encryption_test extends \basic_testcase { +class encryption_test extends advanced_testcase { /** * Clear junk created by tests. @@ -57,25 +52,14 @@ class encryption_test extends \basic_testcase { } /** - * Tests using Sodium need to check the extension is available. - * - * @param string $method Encryption method - */ - protected function require_sodium(string $method) { - if ($method == encryption::METHOD_SODIUM) { - if (!encryption::is_sodium_installed()) { - $this->markTestSkipped('Sodium not installed'); - } - } - } - - /** * Many of the tests work with both encryption methods. * * @return array[] Array of method options for test */ public function encryption_method_provider(): array { - return ['Sodium' => [encryption::METHOD_SODIUM], 'OpenSSL' => [encryption::METHOD_OPENSSL]]; + return [ + 'Sodium' => [encryption::METHOD_SODIUM], + ]; } /** @@ -85,11 +69,8 @@ class encryption_test extends \basic_testcase { * @dataProvider encryption_method_provider */ public function test_create_key(string $method): void { - $this->require_sodium($method); encryption::create_key($method); $key = testable_encryption::get_key($method); - - // Conveniently, both encryption methods have the same key length. $this->assertEquals(32, strlen($key)); $this->expectExceptionMessage('Key already exists'); @@ -97,9 +78,19 @@ class encryption_test extends \basic_testcase { } /** + * Test that we can create keys for legacy {@see encryption::METHOD_OPENSSL} content + */ + public function test_create_key_openssl(): void { + encryption::create_key(encryption::METHOD_OPENSSL); + $key = testable_encryption::get_key(encryption::METHOD_OPENSSL); + $this->assertEquals(32, strlen($key)); + + $this->expectExceptionMessage('Key already exists'); + encryption::create_key(encryption::METHOD_OPENSSL); + } + + /** * Tests encryption and decryption with empty strings. - * - * @throws \moodle_exception */ public function test_encrypt_and_decrypt_empty(): void { $this->assertEquals('', encryption::encrypt('')); @@ -114,7 +105,6 @@ class encryption_test extends \basic_testcase { */ public function test_encrypt_nokeys(string $method): void { global $CFG; - $this->require_sodium($method); // Prevent automatic generation of keys. $CFG->nokeygeneration = true; @@ -123,6 +113,15 @@ class encryption_test extends \basic_testcase { } /** + * Test that attempting to encrypt with legacy {@see encryption::METHOD_OPENSSL} method falls back to Sodium + */ + public function test_encrypt_openssl(): void { + $encrypted = encryption::encrypt('Frogs', encryption::METHOD_OPENSSL); + $this->assertStringStartsWith(encryption::METHOD_SODIUM . ':', $encrypted); + $this->assertDebuggingCalledCount(1, ['Encryption using legacy OpenSSL is deprecated, reverting to Sodium']); + } + + /** * Tests decryption when the data has a different encryption method */ public function test_decrypt_wrongmethod(): void { @@ -137,7 +136,6 @@ class encryption_test extends \basic_testcase { * @param string $method Encryption method */ public function test_decrypt_tooshort(string $method): void { - $this->require_sodium($method); $this->expectExceptionMessage('Insufficient data'); switch ($method) { @@ -162,8 +160,6 @@ class encryption_test extends \basic_testcase { * @param string $method Encryption method */ public function test_decrypt_notbase64(string $method): void { - $this->require_sodium($method); - $this->expectExceptionMessage('Invalid base64 data'); encryption::decrypt($method . ':' . chr(160)); } @@ -176,7 +172,6 @@ class encryption_test extends \basic_testcase { */ public function test_decrypt_nokeys(string $method): void { global $CFG; - $this->require_sodium($method); // Prevent automatic generation of keys. $CFG->nokeygeneration = true; @@ -186,13 +181,28 @@ class encryption_test extends \basic_testcase { } /** + * Test that we can decrypt legacy {@see encryption::METHOD_OPENSSL} content + */ + public function test_decrypt_openssl(): void { + $key = testable_encryption::get_key(encryption::METHOD_OPENSSL); + + // Construct encrypted string using openssl method/cipher. + $iv = random_bytes(openssl_cipher_iv_length(encryption::OPENSSL_CIPHER)); + $encrypted = @openssl_encrypt('Frogs', encryption::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); + $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true); + + $decrypted = encryption::decrypt(encryption::METHOD_OPENSSL . ':' . base64_encode($iv . $encrypted . $hmac)); + $this->assertEquals('Frogs', $decrypted); + $this->assertDebuggingCalledCount(1, ['Decryption using legacy OpenSSL is deprecated, please upgrade to Sodium']); + } + + /** * Test automatic generation of keys when needed. * * @dataProvider encryption_method_provider * @param string $method Encryption method */ public function test_auto_key_generation(string $method): void { - $this->require_sodium($method); // Allow automatic generation (default). $encrypted = encryption::encrypt('frogs', $method); @@ -207,7 +217,6 @@ class encryption_test extends \basic_testcase { */ public function test_invalid_key(string $method): void { global $CFG; - $this->require_sodium($method); // Set the key to something bogus. $folder = $CFG->dataroot . '/secret/key'; @@ -233,7 +242,6 @@ class encryption_test extends \basic_testcase { * @param string $method Encryption method */ public function test_modified_data(string $method): void { - $this->require_sodium($method); $encrypted = encryption::encrypt('frogs', $method); $mainbit = base64_decode(substr($encrypted, strlen($method) + 1)); @@ -248,10 +256,8 @@ class encryption_test extends \basic_testcase { * * @dataProvider encryption_method_provider * @param string $method Encryption method - * @throws \moodle_exception */ public function test_encrypt_and_decrypt_realdata(string $method): void { - $this->require_sodium($method); // Encrypt short string. $encrypted = encryption::encrypt('frogs', $method); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 7df51950287..194a6ac0819 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -16,6 +16,11 @@ information provided here is intended especially for developers. example, of `\moodle_url` instances) * The badges_get_oauth2_service_options() method has been deprecated, because it's not required anymore. It should no longer be used. +* The following class constants are deprecated, as Sodium is now required and we no longer support the OpenSSL fallback except + when decrypting existing content for backwards compatibility: + - `\core\encryption::METHOD_OPENSSL` + - `\core\encryption::OPENSSL_CIPHER` +* The `\core\encryption::is_sodium_installed` method is deprecated, as Sodium is now a requirement * Support for the following phpunit coverage info properties, deprecated since 3.11, has been removed: - `whitelistfolders` - `whitelistfiles` -- 2.11.4.GIT