Merge branch 'MDL-73883-master' of https://github.com/andrewnicols/moodle
[moodle.git] / lib / tests / encryption_test.php
blob7fc05634885b9e8dfee3e989f6498c21aab6380c
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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/>.
17 /**
18 * Test encryption.
20 * @package core
21 * @copyright 2020 The Open University
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 namespace core;
27 /**
28 * Test encryption.
30 * @package core
31 * @copyright 2020 The Open University
32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 class encryption_test extends \basic_testcase {
36 /**
37 * Clear junk created by tests.
39 protected function tearDown(): void {
40 global $CFG;
41 $keyfile = encryption::get_key_file(encryption::METHOD_OPENSSL);
42 if (file_exists($keyfile)) {
43 chmod($keyfile, 0700);
45 $keyfile = encryption::get_key_file(encryption::METHOD_SODIUM);
46 if (file_exists($keyfile)) {
47 chmod($keyfile, 0700);
49 remove_dir($CFG->dataroot . '/secret');
50 unset($CFG->nokeygeneration);
53 protected function setUp(): void {
54 $this->tearDown();
56 require_once(__DIR__ . '/fixtures/testable_encryption.php');
59 /**
60 * Tests using Sodium need to check the extension is available.
62 * @param string $method Encryption method
64 protected function require_sodium(string $method) {
65 if ($method == encryption::METHOD_SODIUM) {
66 if (!encryption::is_sodium_installed()) {
67 $this->markTestSkipped('Sodium not installed');
72 /**
73 * Many of the tests work with both encryption methods.
75 * @return array[] Array of method options for test
77 public function encryption_method_provider(): array {
78 return ['Sodium' => [encryption::METHOD_SODIUM], 'OpenSSL' => [encryption::METHOD_OPENSSL]];
81 /**
82 * Tests the create_keys and get_key functions.
84 * @param string $method Encryption method
85 * @dataProvider encryption_method_provider
87 public function test_create_key(string $method): void {
88 $this->require_sodium($method);
89 encryption::create_key($method);
90 $key = testable_encryption::get_key($method);
92 // Conveniently, both encryption methods have the same key length.
93 $this->assertEquals(32, strlen($key));
95 $this->expectExceptionMessage('Key already exists');
96 encryption::create_key($method);
99 /**
100 * Tests encryption and decryption with empty strings.
102 * @throws \moodle_exception
104 public function test_encrypt_and_decrypt_empty(): void {
105 $this->assertEquals('', encryption::encrypt(''));
106 $this->assertEquals('', encryption::decrypt(''));
110 * Tests encryption when the keys weren't created yet.
112 * @param string $method Encryption method
113 * @dataProvider encryption_method_provider
115 public function test_encrypt_nokeys(string $method): void {
116 global $CFG;
117 $this->require_sodium($method);
119 // Prevent automatic generation of keys.
120 $CFG->nokeygeneration = true;
121 $this->expectExceptionMessage('Key not found');
122 encryption::encrypt('frogs', $method);
126 * Tests decryption when the data has a different encryption method
128 public function test_decrypt_wrongmethod(): void {
129 $this->expectExceptionMessage('Data does not match a supported encryption method');
130 encryption::decrypt('FAKE-CIPHER-METHOD:xx');
134 * Tests decryption when not enough data is supplied to get the IV and some data.
136 * @dataProvider encryption_method_provider
137 * @param string $method Encryption method
139 public function test_decrypt_tooshort(string $method): void {
140 $this->require_sodium($method);
142 $this->expectExceptionMessage('Insufficient data');
143 switch ($method) {
144 case encryption::METHOD_OPENSSL:
145 // It needs min 49 bytes (16 bytes IV + 32 bytes HMAC + 1 byte data).
146 $justtooshort = '0123456789abcdef0123456789abcdef0123456789abcdef';
147 break;
148 case encryption::METHOD_SODIUM:
149 // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1
150 // byte data); it splits out any authentication hashes itself.
151 $justtooshort = '0123456789abcdef01234567';
152 break;
155 encryption::decrypt($method . ':' .base64_encode($justtooshort));
159 * Tests decryption when data is not valid base64.
161 * @dataProvider encryption_method_provider
162 * @param string $method Encryption method
164 public function test_decrypt_notbase64(string $method): void {
165 $this->require_sodium($method);
167 $this->expectExceptionMessage('Invalid base64 data');
168 encryption::decrypt($method . ':' . chr(160));
172 * Tests decryption when the keys weren't created yet.
174 * @dataProvider encryption_method_provider
175 * @param string $method Encryption method
177 public function test_decrypt_nokeys(string $method): void {
178 global $CFG;
179 $this->require_sodium($method);
181 // Prevent automatic generation of keys.
182 $CFG->nokeygeneration = true;
183 $this->expectExceptionMessage('Key not found');
184 encryption::decrypt($method . ':' . base64_encode(
185 '0123456789abcdef0123456789abcdef0123456789abcdef0'));
189 * Test automatic generation of keys when needed.
191 * @dataProvider encryption_method_provider
192 * @param string $method Encryption method
194 public function test_auto_key_generation(string $method): void {
195 $this->require_sodium($method);
197 // Allow automatic generation (default).
198 $encrypted = encryption::encrypt('frogs', $method);
199 $this->assertEquals('frogs', encryption::decrypt($encrypted));
203 * Checks that invalid key causes failures.
205 * @dataProvider encryption_method_provider
206 * @param string $method Encryption method
208 public function test_invalid_key(string $method): void {
209 global $CFG;
210 $this->require_sodium($method);
212 // Set the key to something bogus.
213 $folder = $CFG->dataroot . '/secret/key';
214 check_dir_exists($folder);
215 file_put_contents(encryption::get_key_file($method), 'silly');
217 switch ($method) {
218 case encryption::METHOD_SODIUM:
219 $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/');
220 break;
222 case encryption::METHOD_OPENSSL:
223 $this->expectExceptionMessage('Invalid key');
224 break;
226 encryption::encrypt('frogs', $method);
230 * Checks that modified data causes failures.
232 * @dataProvider encryption_method_provider
233 * @param string $method Encryption method
235 public function test_modified_data(string $method): void {
236 $this->require_sodium($method);
238 $encrypted = encryption::encrypt('frogs', $method);
239 $mainbit = base64_decode(substr($encrypted, strlen($method) + 1));
240 $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16);
241 $encrypted = $method . ':' . base64_encode($mainbit);
242 $this->expectExceptionMessage('Integrity check failed');
243 encryption::decrypt($encrypted);
247 * Tests encryption and decryption for real.
249 * @dataProvider encryption_method_provider
250 * @param string $method Encryption method
251 * @throws \moodle_exception
253 public function test_encrypt_and_decrypt_realdata(string $method): void {
254 $this->require_sodium($method);
256 // Encrypt short string.
257 $encrypted = encryption::encrypt('frogs', $method);
258 $this->assertNotEquals('frogs', $encrypted);
259 $this->assertEquals('frogs', encryption::decrypt($encrypted));
261 // Encrypt really long string (1 MB).
262 $long = str_repeat('X', 1024 * 1024);
263 $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method)));