Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

< /** < * 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. */ protected function tearDown(): void { global $CFG; $keyfile = encryption::get_key_file(encryption::METHOD_OPENSSL); if (file_exists($keyfile)) { chmod($keyfile, 0700); } $keyfile = encryption::get_key_file(encryption::METHOD_SODIUM); if (file_exists($keyfile)) { chmod($keyfile, 0700); } remove_dir($CFG->dataroot . '/secret'); unset($CFG->nokeygeneration); } protected function setUp(): void { $this->tearDown(); 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], > ];
} /** * Tests the create_keys and get_key functions. * * @param string $method Encryption method * @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 * Tests encryption and decryption with empty strings. > */ * > public function test_create_key_openssl(): void { * @throws \moodle_exception > encryption::create_key(encryption::METHOD_OPENSSL); */ > $key = testable_encryption::get_key(encryption::METHOD_OPENSSL); public function test_encrypt_and_decrypt_empty(): void { > $this->assertEquals(32, strlen($key)); $this->assertEquals('', encryption::encrypt('')); > $this->assertEquals('', encryption::decrypt('')); > $this->expectExceptionMessage('Key already exists'); } > encryption::create_key(encryption::METHOD_OPENSSL); > } /** > * Tests encryption when the keys weren't created yet. > /**
< * < * @throws \moodle_exception
* @dataProvider encryption_method_provider */ public function test_encrypt_nokeys(string $method): void { global $CFG;
< $this->require_sodium($method);
// Prevent automatic generation of keys. $CFG->nokeygeneration = true; $this->expectExceptionMessage('Key not found'); encryption::encrypt('frogs', $method); } /**
> * Test that attempting to encrypt with legacy {@see encryption::METHOD_OPENSSL} method falls back to Sodium * Tests decryption when the data has a different encryption method > */ */ > public function test_encrypt_openssl(): void { public function test_decrypt_wrongmethod(): void { > $encrypted = encryption::encrypt('Frogs', encryption::METHOD_OPENSSL); $this->expectExceptionMessage('Data does not match a supported encryption method'); > $this->assertStringStartsWith(encryption::METHOD_SODIUM . ':', $encrypted); encryption::decrypt('FAKE-CIPHER-METHOD:xx'); > $this->assertDebuggingCalledCount(1, ['Encryption using legacy OpenSSL is deprecated, reverting to Sodium']); } > } > /** > /**
* Tests decryption when not enough data is supplied to get the IV and some data. * * @dataProvider encryption_method_provider * @param string $method Encryption method */ public function test_decrypt_tooshort(string $method): void {
< $this->require_sodium($method);
$this->expectExceptionMessage('Insufficient data'); switch ($method) { case encryption::METHOD_OPENSSL: // It needs min 49 bytes (16 bytes IV + 32 bytes HMAC + 1 byte data). $justtooshort = '0123456789abcdef0123456789abcdef0123456789abcdef'; break; case encryption::METHOD_SODIUM: // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1 // byte data); it splits out any authentication hashes itself. $justtooshort = '0123456789abcdef01234567'; break; } encryption::decrypt($method . ':' .base64_encode($justtooshort)); } /** * Tests decryption when data is not valid base64. * * @dataProvider encryption_method_provider * @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)); } /** * Tests decryption when the keys weren't created yet. * * @dataProvider encryption_method_provider * @param string $method Encryption method */ public function test_decrypt_nokeys(string $method): void { global $CFG;
< $this->require_sodium($method);
// Prevent automatic generation of keys. $CFG->nokeygeneration = true; $this->expectExceptionMessage('Key not found'); encryption::decrypt($method . ':' . base64_encode( '0123456789abcdef0123456789abcdef0123456789abcdef0')); } /**
> * Test that we can decrypt legacy {@see encryption::METHOD_OPENSSL} content * Test automatic generation of keys when needed. > */ * > public function test_decrypt_openssl(): void { * @dataProvider encryption_method_provider > $key = testable_encryption::get_key(encryption::METHOD_OPENSSL); * @param string $method Encryption method > */ > // Construct encrypted string using openssl method/cipher. public function test_auto_key_generation(string $method): void { > $iv = random_bytes(openssl_cipher_iv_length(encryption::OPENSSL_CIPHER)); $this->require_sodium($method); > $encrypted = @openssl_encrypt('Frogs', encryption::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); > $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true); // Allow automatic generation (default). > $encrypted = encryption::encrypt('frogs', $method); > $decrypted = encryption::decrypt(encryption::METHOD_OPENSSL . ':' . base64_encode($iv . $encrypted . $hmac)); $this->assertEquals('frogs', encryption::decrypt($encrypted)); > $this->assertEquals('Frogs', $decrypted); } > $this->assertDebuggingCalledCount(1, ['Decryption using legacy OpenSSL is deprecated, please upgrade to Sodium']); > } /** > * Checks that invalid key causes failures. > /**
< $this->require_sodium($method);
* @dataProvider encryption_method_provider * @param string $method Encryption method */ 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'; check_dir_exists($folder); file_put_contents(encryption::get_key_file($method), 'silly'); switch ($method) { case encryption::METHOD_SODIUM: $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/'); break; case encryption::METHOD_OPENSSL: $this->expectExceptionMessage('Invalid key'); break; } encryption::encrypt('frogs', $method); } /** * Checks that modified data causes failures. * * @dataProvider encryption_method_provider * @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)); $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16); $encrypted = $method . ':' . base64_encode($mainbit); $this->expectExceptionMessage('Integrity check failed'); encryption::decrypt($encrypted); } /** * Tests encryption and decryption for real. * * @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); $this->assertNotEquals('frogs', $encrypted); $this->assertEquals('frogs', encryption::decrypt($encrypted)); // Encrypt really long string (1 MB). $long = str_repeat('X', 1024 * 1024); $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method))); } }