| 1 | efrain | 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.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 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/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | namespace core;
 | 
        
           |  |  | 18 |   | 
        
           |  |  | 19 | use advanced_testcase;
 | 
        
           |  |  | 20 |   | 
        
           |  |  | 21 | /**
 | 
        
           |  |  | 22 |  * Test encryption.
 | 
        
           |  |  | 23 |  *
 | 
        
           |  |  | 24 |  * @package core
 | 
        
           |  |  | 25 |  * @copyright 2020 The Open University
 | 
        
           |  |  | 26 |  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 27 |  * @covers  \core\encryption
 | 
        
           |  |  | 28 |  */
 | 
        
           | 1441 | ariadna | 29 | final class encryption_test extends advanced_testcase {
 | 
        
           | 1 | efrain | 30 |   | 
        
           |  |  | 31 |     protected function setUp(): void {
 | 
        
           | 1441 | ariadna | 32 |         parent::setUp();
 | 
        
           | 1 | efrain | 33 |         require_once(__DIR__ . '/fixtures/testable_encryption.php');
 | 
        
           |  |  | 34 |     }
 | 
        
           |  |  | 35 |   | 
        
           |  |  | 36 |     /**
 | 
        
           | 1441 | ariadna | 37 |      * Return a list of supported encryption methods
 | 
        
           | 1 | efrain | 38 |      *
 | 
        
           |  |  | 39 |      * @return array[] Array of method options for test
 | 
        
           |  |  | 40 |      */
 | 
        
           | 1441 | ariadna | 41 |     public static function encryption_method_provider(): array {
 | 
        
           | 1 | efrain | 42 |         return [
 | 
        
           |  |  | 43 |             'Sodium' => [encryption::METHOD_SODIUM],
 | 
        
           |  |  | 44 |         ];
 | 
        
           |  |  | 45 |     }
 | 
        
           |  |  | 46 |   | 
        
           |  |  | 47 |     /**
 | 
        
           |  |  | 48 |      * Tests the create_keys and get_key functions.
 | 
        
           |  |  | 49 |      *
 | 
        
           |  |  | 50 |      * @param string $method Encryption method
 | 
        
           |  |  | 51 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 52 |      */
 | 
        
           |  |  | 53 |     public function test_create_key(string $method): void {
 | 
        
           |  |  | 54 |         encryption::create_key($method);
 | 
        
           |  |  | 55 |         $key = testable_encryption::get_key($method);
 | 
        
           |  |  | 56 |         $this->assertEquals(32, strlen($key));
 | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |         $this->expectExceptionMessage('Key already exists');
 | 
        
           |  |  | 59 |         encryption::create_key($method);
 | 
        
           |  |  | 60 |     }
 | 
        
           |  |  | 61 |   | 
        
           |  |  | 62 |     /**
 | 
        
           |  |  | 63 |      * Tests encryption and decryption with empty strings.
 | 
        
           |  |  | 64 |      */
 | 
        
           |  |  | 65 |     public function test_encrypt_and_decrypt_empty(): void {
 | 
        
           |  |  | 66 |         $this->assertEquals('', encryption::encrypt(''));
 | 
        
           |  |  | 67 |         $this->assertEquals('', encryption::decrypt(''));
 | 
        
           |  |  | 68 |     }
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 |     /**
 | 
        
           |  |  | 71 |      * Tests encryption when the keys weren't created yet.
 | 
        
           |  |  | 72 |      *
 | 
        
           |  |  | 73 |      * @param string $method Encryption method
 | 
        
           |  |  | 74 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 75 |      */
 | 
        
           |  |  | 76 |     public function test_encrypt_nokeys(string $method): void {
 | 
        
           |  |  | 77 |         global $CFG;
 | 
        
           |  |  | 78 |   | 
        
           |  |  | 79 |         // Prevent automatic generation of keys.
 | 
        
           |  |  | 80 |         $CFG->nokeygeneration = true;
 | 
        
           |  |  | 81 |         $this->expectExceptionMessage('Key not found');
 | 
        
           |  |  | 82 |         encryption::encrypt('frogs', $method);
 | 
        
           |  |  | 83 |     }
 | 
        
           |  |  | 84 |   | 
        
           |  |  | 85 |     /**
 | 
        
           |  |  | 86 |      * Tests decryption when the data has a different encryption method
 | 
        
           |  |  | 87 |      */
 | 
        
           |  |  | 88 |     public function test_decrypt_wrongmethod(): void {
 | 
        
           |  |  | 89 |         $this->expectExceptionMessage('Data does not match a supported encryption method');
 | 
        
           |  |  | 90 |         encryption::decrypt('FAKE-CIPHER-METHOD:xx');
 | 
        
           |  |  | 91 |     }
 | 
        
           |  |  | 92 |   | 
        
           |  |  | 93 |     /**
 | 
        
           |  |  | 94 |      * Tests decryption when not enough data is supplied to get the IV and some data.
 | 
        
           |  |  | 95 |      *
 | 
        
           |  |  | 96 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 97 |      * @param string $method Encryption method
 | 
        
           |  |  | 98 |      */
 | 
        
           |  |  | 99 |     public function test_decrypt_tooshort(string $method): void {
 | 
        
           |  |  | 100 |         switch ($method) {
 | 
        
           |  |  | 101 |             case encryption::METHOD_SODIUM:
 | 
        
           |  |  | 102 |                 // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1
 | 
        
           |  |  | 103 |                 // byte data); it splits out any authentication hashes itself.
 | 
        
           |  |  | 104 |                 $justtooshort = '0123456789abcdef01234567';
 | 
        
           |  |  | 105 |                 break;
 | 
        
           |  |  | 106 |         }
 | 
        
           |  |  | 107 |   | 
        
           | 1441 | ariadna | 108 |         $this->expectExceptionMessage('Insufficient data');
 | 
        
           | 1 | efrain | 109 |         encryption::decrypt($method . ':' .base64_encode($justtooshort));
 | 
        
           |  |  | 110 |     }
 | 
        
           |  |  | 111 |   | 
        
           |  |  | 112 |     /**
 | 
        
           |  |  | 113 |      * Tests decryption when data is not valid base64.
 | 
        
           |  |  | 114 |      *
 | 
        
           |  |  | 115 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 116 |      * @param string $method Encryption method
 | 
        
           |  |  | 117 |      */
 | 
        
           |  |  | 118 |     public function test_decrypt_notbase64(string $method): void {
 | 
        
           |  |  | 119 |         $this->expectExceptionMessage('Invalid base64 data');
 | 
        
           |  |  | 120 |         encryption::decrypt($method . ':' . chr(160));
 | 
        
           |  |  | 121 |     }
 | 
        
           |  |  | 122 |   | 
        
           |  |  | 123 |     /**
 | 
        
           |  |  | 124 |      * Tests decryption when the keys weren't created yet.
 | 
        
           |  |  | 125 |      *
 | 
        
           |  |  | 126 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 127 |      * @param string $method Encryption method
 | 
        
           |  |  | 128 |      */
 | 
        
           |  |  | 129 |     public function test_decrypt_nokeys(string $method): void {
 | 
        
           |  |  | 130 |         global $CFG;
 | 
        
           |  |  | 131 |   | 
        
           |  |  | 132 |         // Prevent automatic generation of keys.
 | 
        
           |  |  | 133 |         $CFG->nokeygeneration = true;
 | 
        
           |  |  | 134 |         $this->expectExceptionMessage('Key not found');
 | 
        
           |  |  | 135 |         encryption::decrypt($method . ':' . base64_encode(
 | 
        
           |  |  | 136 |                 '0123456789abcdef0123456789abcdef0123456789abcdef0'));
 | 
        
           |  |  | 137 |     }
 | 
        
           |  |  | 138 |   | 
        
           |  |  | 139 |     /**
 | 
        
           |  |  | 140 |      * Test automatic generation of keys when needed.
 | 
        
           |  |  | 141 |      *
 | 
        
           |  |  | 142 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 143 |      * @param string $method Encryption method
 | 
        
           |  |  | 144 |      */
 | 
        
           |  |  | 145 |     public function test_auto_key_generation(string $method): void {
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 |         // Allow automatic generation (default).
 | 
        
           |  |  | 148 |         $encrypted = encryption::encrypt('frogs', $method);
 | 
        
           |  |  | 149 |         $this->assertEquals('frogs', encryption::decrypt($encrypted));
 | 
        
           |  |  | 150 |     }
 | 
        
           |  |  | 151 |   | 
        
           |  |  | 152 |     /**
 | 
        
           |  |  | 153 |      * Checks that invalid key causes failures.
 | 
        
           |  |  | 154 |      *
 | 
        
           |  |  | 155 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 156 |      * @param string $method Encryption method
 | 
        
           |  |  | 157 |      */
 | 
        
           |  |  | 158 |     public function test_invalid_key(string $method): void {
 | 
        
           |  |  | 159 |         global $CFG;
 | 
        
           |  |  | 160 |   | 
        
           |  |  | 161 |         // Set the key to something bogus.
 | 
        
           |  |  | 162 |         $folder = $CFG->dataroot . '/secret/key';
 | 
        
           |  |  | 163 |         check_dir_exists($folder);
 | 
        
           |  |  | 164 |         file_put_contents(encryption::get_key_file($method), 'silly');
 | 
        
           |  |  | 165 |   | 
        
           |  |  | 166 |         switch ($method) {
 | 
        
           |  |  | 167 |             case encryption::METHOD_SODIUM:
 | 
        
           |  |  | 168 |                 $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/');
 | 
        
           |  |  | 169 |                 break;
 | 
        
           | 1441 | ariadna | 170 |         }
 | 
        
           | 1 | efrain | 171 |   | 
        
           |  |  | 172 |         encryption::encrypt('frogs', $method);
 | 
        
           |  |  | 173 |     }
 | 
        
           |  |  | 174 |   | 
        
           |  |  | 175 |     /**
 | 
        
           |  |  | 176 |      * Checks that modified data causes failures.
 | 
        
           |  |  | 177 |      *
 | 
        
           |  |  | 178 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 179 |      * @param string $method Encryption method
 | 
        
           |  |  | 180 |      */
 | 
        
           |  |  | 181 |     public function test_modified_data(string $method): void {
 | 
        
           |  |  | 182 |         $encrypted = encryption::encrypt('frogs', $method);
 | 
        
           |  |  | 183 |         $mainbit = base64_decode(substr($encrypted, strlen($method) + 1));
 | 
        
           |  |  | 184 |         $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16);
 | 
        
           |  |  | 185 |         $encrypted = $method . ':' . base64_encode($mainbit);
 | 
        
           |  |  | 186 |         $this->expectExceptionMessage('Integrity check failed');
 | 
        
           |  |  | 187 |         encryption::decrypt($encrypted);
 | 
        
           |  |  | 188 |     }
 | 
        
           |  |  | 189 |   | 
        
           |  |  | 190 |     /**
 | 
        
           |  |  | 191 |      * Tests encryption and decryption for real.
 | 
        
           |  |  | 192 |      *
 | 
        
           |  |  | 193 |      * @dataProvider encryption_method_provider
 | 
        
           |  |  | 194 |      * @param string $method Encryption method
 | 
        
           |  |  | 195 |      */
 | 
        
           |  |  | 196 |     public function test_encrypt_and_decrypt_realdata(string $method): void {
 | 
        
           |  |  | 197 |   | 
        
           |  |  | 198 |         // Encrypt short string.
 | 
        
           |  |  | 199 |         $encrypted = encryption::encrypt('frogs', $method);
 | 
        
           |  |  | 200 |         $this->assertNotEquals('frogs', $encrypted);
 | 
        
           |  |  | 201 |         $this->assertEquals('frogs', encryption::decrypt($encrypted));
 | 
        
           |  |  | 202 |   | 
        
           |  |  | 203 |         // Encrypt really long string (1 MB).
 | 
        
           |  |  | 204 |         $long = str_repeat('X', 1024 * 1024);
 | 
        
           |  |  | 205 |         $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method)));
 | 
        
           |  |  | 206 |     }
 | 
        
           |  |  | 207 | }
 |