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. @@ -56,26 +51,15 @@ class encryption_test extends \basic_testcase { require_once(__DIR__ . '/fixtures/testable_encryption.php'); } - /** - * 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,21 +69,28 @@ 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'); encryption::create_key($method); } + /** + * 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; @@ -122,6 +112,15 @@ class encryption_test extends \basic_testcase { encryption::encrypt('frogs', $method); } + /** + * 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 */ @@ -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; @@ -185,6 +180,22 @@ class encryption_test extends \basic_testcase { '0123456789abcdef0123456789abcdef0123456789abcdef0')); } + /** + * 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. * @@ -192,7 +203,6 @@ class encryption_test extends \basic_testcase { * @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`