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.

Differences Between: [Versions 401 and 403]

   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  /**
  18   * Test encryption.
  19   *
  20   * @package core
  21   * @copyright 2020 The Open University
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core;
  26  
  27  /**
  28   * Test encryption.
  29   *
  30   * @package core
  31   * @copyright 2020 The Open University
  32   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class encryption_test extends \basic_testcase {
  35  
  36      /**
  37       * Clear junk created by tests.
  38       */
  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);
  44          }
  45          $keyfile = encryption::get_key_file(encryption::METHOD_SODIUM);
  46          if (file_exists($keyfile)) {
  47              chmod($keyfile, 0700);
  48          }
  49          remove_dir($CFG->dataroot . '/secret');
  50          unset($CFG->nokeygeneration);
  51      }
  52  
  53      protected function setUp(): void {
  54          $this->tearDown();
  55  
  56          require_once (__DIR__ . '/fixtures/testable_encryption.php');
  57      }
  58  
  59      /**
  60       * Tests using Sodium need to check the extension is available.
  61       *
  62       * @param string $method Encryption method
  63       */
  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');
  68              }
  69          }
  70      }
  71  
  72      /**
  73       * Many of the tests work with both encryption methods.
  74       *
  75       * @return array[] Array of method options for test
  76       */
  77      public function encryption_method_provider(): array {
  78          return ['Sodium' => [encryption::METHOD_SODIUM], 'OpenSSL' => [encryption::METHOD_OPENSSL]];
  79      }
  80  
  81      /**
  82       * Tests the create_keys and get_key functions.
  83       *
  84       * @param string $method Encryption method
  85       * @dataProvider encryption_method_provider
  86       */
  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);
  91  
  92          // Conveniently, both encryption methods have the same key length.
  93          $this->assertEquals(32, strlen($key));
  94  
  95          $this->expectExceptionMessage('Key already exists');
  96          encryption::create_key($method);
  97      }
  98  
  99      /**
 100       * Tests encryption and decryption with empty strings.
 101       *
 102       * @throws \moodle_exception
 103       */
 104      public function test_encrypt_and_decrypt_empty(): void {
 105          $this->assertEquals('', encryption::encrypt(''));
 106          $this->assertEquals('', encryption::decrypt(''));
 107      }
 108  
 109      /**
 110       * Tests encryption when the keys weren't created yet.
 111       *
 112       * @param string $method Encryption method
 113       * @dataProvider encryption_method_provider
 114       */
 115      public function test_encrypt_nokeys(string $method): void {
 116          global $CFG;
 117          $this->require_sodium($method);
 118  
 119          // Prevent automatic generation of keys.
 120          $CFG->nokeygeneration = true;
 121          $this->expectExceptionMessage('Key not found');
 122          encryption::encrypt('frogs', $method);
 123      }
 124  
 125      /**
 126       * Tests decryption when the data has a different encryption method
 127       */
 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');
 131      }
 132  
 133      /**
 134       * Tests decryption when not enough data is supplied to get the IV and some data.
 135       *
 136       * @dataProvider encryption_method_provider
 137       * @param string $method Encryption method
 138       */
 139      public function test_decrypt_tooshort(string $method): void {
 140          $this->require_sodium($method);
 141  
 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;
 153          }
 154  
 155          encryption::decrypt($method . ':' .base64_encode($justtooshort));
 156      }
 157  
 158      /**
 159       * Tests decryption when data is not valid base64.
 160       *
 161       * @dataProvider encryption_method_provider
 162       * @param string $method Encryption method
 163       */
 164      public function test_decrypt_notbase64(string $method): void {
 165          $this->require_sodium($method);
 166  
 167          $this->expectExceptionMessage('Invalid base64 data');
 168          encryption::decrypt($method . ':' . chr(160));
 169      }
 170  
 171      /**
 172       * Tests decryption when the keys weren't created yet.
 173       *
 174       * @dataProvider encryption_method_provider
 175       * @param string $method Encryption method
 176       */
 177      public function test_decrypt_nokeys(string $method): void {
 178          global $CFG;
 179          $this->require_sodium($method);
 180  
 181          // Prevent automatic generation of keys.
 182          $CFG->nokeygeneration = true;
 183          $this->expectExceptionMessage('Key not found');
 184          encryption::decrypt($method . ':' . base64_encode(
 185                  '0123456789abcdef0123456789abcdef0123456789abcdef0'));
 186      }
 187  
 188      /**
 189       * Test automatic generation of keys when needed.
 190       *
 191       * @dataProvider encryption_method_provider
 192       * @param string $method Encryption method
 193       */
 194      public function test_auto_key_generation(string $method): void {
 195          $this->require_sodium($method);
 196  
 197          // Allow automatic generation (default).
 198          $encrypted = encryption::encrypt('frogs', $method);
 199          $this->assertEquals('frogs', encryption::decrypt($encrypted));
 200      }
 201  
 202      /**
 203       * Checks that invalid key causes failures.
 204       *
 205       * @dataProvider encryption_method_provider
 206       * @param string $method Encryption method
 207       */
 208      public function test_invalid_key(string $method): void {
 209          global $CFG;
 210          $this->require_sodium($method);
 211  
 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');
 216  
 217          switch ($method) {
 218              case encryption::METHOD_SODIUM:
 219                  $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/');
 220                  break;
 221  
 222              case encryption::METHOD_OPENSSL:
 223                  $this->expectExceptionMessage('Invalid key');
 224                  break;
 225          }
 226          encryption::encrypt('frogs', $method);
 227      }
 228  
 229      /**
 230       * Checks that modified data causes failures.
 231       *
 232       * @dataProvider encryption_method_provider
 233       * @param string $method Encryption method
 234       */
 235      public function test_modified_data(string $method): void {
 236          $this->require_sodium($method);
 237  
 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);
 244      }
 245  
 246      /**
 247       * Tests encryption and decryption for real.
 248       *
 249       * @dataProvider encryption_method_provider
 250       * @param string $method Encryption method
 251       * @throws \moodle_exception
 252       */
 253      public function test_encrypt_and_decrypt_realdata(string $method): void {
 254          $this->require_sodium($method);
 255  
 256          // Encrypt short string.
 257          $encrypted = encryption::encrypt('frogs', $method);
 258          $this->assertNotEquals('frogs', $encrypted);
 259          $this->assertEquals('frogs', encryption::decrypt($encrypted));
 260  
 261          // Encrypt really long string (1 MB).
 262          $long = str_repeat('X', 1024 * 1024);
 263          $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method)));
 264      }
 265  }