2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 use advanced_testcase
;
25 * @copyright 2020 The Open University
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 * @covers \core\encryption
29 class encryption_test
extends advanced_testcase
{
32 * Clear junk created by tests.
34 protected function tearDown(): void
{
36 $keyfile = encryption
::get_key_file(encryption
::METHOD_OPENSSL
);
37 if (file_exists($keyfile)) {
38 chmod($keyfile, 0700);
40 $keyfile = encryption
::get_key_file(encryption
::METHOD_SODIUM
);
41 if (file_exists($keyfile)) {
42 chmod($keyfile, 0700);
44 remove_dir($CFG->dataroot
. '/secret');
45 unset($CFG->nokeygeneration
);
48 protected function setUp(): void
{
51 require_once(__DIR__
. '/fixtures/testable_encryption.php');
55 * Many of the tests work with both encryption methods.
57 * @return array[] Array of method options for test
59 public function encryption_method_provider(): array {
61 'Sodium' => [encryption
::METHOD_SODIUM
],
66 * Tests the create_keys and get_key functions.
68 * @param string $method Encryption method
69 * @dataProvider encryption_method_provider
71 public function test_create_key(string $method): void
{
72 encryption
::create_key($method);
73 $key = testable_encryption
::get_key($method);
74 $this->assertEquals(32, strlen($key));
76 $this->expectExceptionMessage('Key already exists');
77 encryption
::create_key($method);
81 * Test that we can create keys for legacy {@see encryption::METHOD_OPENSSL} content
83 public function test_create_key_openssl(): void
{
84 encryption
::create_key(encryption
::METHOD_OPENSSL
);
85 $key = testable_encryption
::get_key(encryption
::METHOD_OPENSSL
);
86 $this->assertEquals(32, strlen($key));
88 $this->expectExceptionMessage('Key already exists');
89 encryption
::create_key(encryption
::METHOD_OPENSSL
);
93 * Tests encryption and decryption with empty strings.
95 public function test_encrypt_and_decrypt_empty(): void
{
96 $this->assertEquals('', encryption
::encrypt(''));
97 $this->assertEquals('', encryption
::decrypt(''));
101 * Tests encryption when the keys weren't created yet.
103 * @param string $method Encryption method
104 * @dataProvider encryption_method_provider
106 public function test_encrypt_nokeys(string $method): void
{
109 // Prevent automatic generation of keys.
110 $CFG->nokeygeneration
= true;
111 $this->expectExceptionMessage('Key not found');
112 encryption
::encrypt('frogs', $method);
116 * Test that attempting to encrypt with legacy {@see encryption::METHOD_OPENSSL} method falls back to Sodium
118 public function test_encrypt_openssl(): void
{
119 $encrypted = encryption
::encrypt('Frogs', encryption
::METHOD_OPENSSL
);
120 $this->assertStringStartsWith(encryption
::METHOD_SODIUM
. ':', $encrypted);
121 $this->assertDebuggingCalledCount(1, ['Encryption using legacy OpenSSL is deprecated, reverting to Sodium']);
125 * Tests decryption when the data has a different encryption method
127 public function test_decrypt_wrongmethod(): void
{
128 $this->expectExceptionMessage('Data does not match a supported encryption method');
129 encryption
::decrypt('FAKE-CIPHER-METHOD:xx');
133 * Tests decryption when not enough data is supplied to get the IV and some data.
135 * @dataProvider encryption_method_provider
136 * @param string $method Encryption method
138 public function test_decrypt_tooshort(string $method): void
{
140 $this->expectExceptionMessage('Insufficient data');
142 case encryption
::METHOD_OPENSSL
:
143 // It needs min 49 bytes (16 bytes IV + 32 bytes HMAC + 1 byte data).
144 $justtooshort = '0123456789abcdef0123456789abcdef0123456789abcdef';
146 case encryption
::METHOD_SODIUM
:
147 // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1
148 // byte data); it splits out any authentication hashes itself.
149 $justtooshort = '0123456789abcdef01234567';
153 encryption
::decrypt($method . ':' .base64_encode($justtooshort));
157 * Tests decryption when data is not valid base64.
159 * @dataProvider encryption_method_provider
160 * @param string $method Encryption method
162 public function test_decrypt_notbase64(string $method): void
{
163 $this->expectExceptionMessage('Invalid base64 data');
164 encryption
::decrypt($method . ':' . chr(160));
168 * Tests decryption when the keys weren't created yet.
170 * @dataProvider encryption_method_provider
171 * @param string $method Encryption method
173 public function test_decrypt_nokeys(string $method): void
{
176 // Prevent automatic generation of keys.
177 $CFG->nokeygeneration
= true;
178 $this->expectExceptionMessage('Key not found');
179 encryption
::decrypt($method . ':' . base64_encode(
180 '0123456789abcdef0123456789abcdef0123456789abcdef0'));
184 * Test that we can decrypt legacy {@see encryption::METHOD_OPENSSL} content
186 public function test_decrypt_openssl(): void
{
187 $key = testable_encryption
::get_key(encryption
::METHOD_OPENSSL
);
189 // Construct encrypted string using openssl method/cipher.
190 $iv = random_bytes(openssl_cipher_iv_length(encryption
::OPENSSL_CIPHER
));
191 $encrypted = @openssl_encrypt
('Frogs', encryption
::OPENSSL_CIPHER
, $key, OPENSSL_RAW_DATA
, $iv);
192 $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true);
194 $decrypted = encryption
::decrypt(encryption
::METHOD_OPENSSL
. ':' . base64_encode($iv . $encrypted . $hmac));
195 $this->assertEquals('Frogs', $decrypted);
196 $this->assertDebuggingCalledCount(1, ['Decryption using legacy OpenSSL is deprecated, please upgrade to Sodium']);
200 * Test automatic generation of keys when needed.
202 * @dataProvider encryption_method_provider
203 * @param string $method Encryption method
205 public function test_auto_key_generation(string $method): void
{
207 // Allow automatic generation (default).
208 $encrypted = encryption
::encrypt('frogs', $method);
209 $this->assertEquals('frogs', encryption
::decrypt($encrypted));
213 * Checks that invalid key causes failures.
215 * @dataProvider encryption_method_provider
216 * @param string $method Encryption method
218 public function test_invalid_key(string $method): void
{
221 // Set the key to something bogus.
222 $folder = $CFG->dataroot
. '/secret/key';
223 check_dir_exists($folder);
224 file_put_contents(encryption
::get_key_file($method), 'silly');
227 case encryption
::METHOD_SODIUM
:
228 $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/');
231 case encryption
::METHOD_OPENSSL
:
232 $this->expectExceptionMessage('Invalid key');
235 encryption
::encrypt('frogs', $method);
239 * Checks that modified data causes failures.
241 * @dataProvider encryption_method_provider
242 * @param string $method Encryption method
244 public function test_modified_data(string $method): void
{
246 $encrypted = encryption
::encrypt('frogs', $method);
247 $mainbit = base64_decode(substr($encrypted, strlen($method) +
1));
248 $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16);
249 $encrypted = $method . ':' . base64_encode($mainbit);
250 $this->expectExceptionMessage('Integrity check failed');
251 encryption
::decrypt($encrypted);
255 * Tests encryption and decryption for real.
257 * @dataProvider encryption_method_provider
258 * @param string $method Encryption method
260 public function test_encrypt_and_decrypt_realdata(string $method): void
{
262 // Encrypt short string.
263 $encrypted = encryption
::encrypt('frogs', $method);
264 $this->assertNotEquals('frogs', $encrypted);
265 $this->assertEquals('frogs', encryption
::decrypt($encrypted));
267 // Encrypt really long string (1 MB).
268 $long = str_repeat('X', 1024 * 1024);
269 $this->assertEquals($long, encryption
::decrypt(encryption
::encrypt($long, $method)));