MDL-65818 Security: Encryption API and admin setting for secure data

This commit is contained in:
sam marshall 2019-06-03 11:39:29 +01:00
parent 7fa836cf36
commit ddbafce0e0
15 changed files with 957 additions and 1 deletions

View File

@ -0,0 +1,77 @@
<?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/>.
/**
* Generates a secure key for the current server (presuming it does not already exist).
*
* @package core_admin
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use \core\encryption;
define('CLI_SCRIPT', true);
require(__DIR__ . '/../../config.php');
require_once($CFG->libdir . '/clilib.php');
// Get cli options.
[$options, $unrecognized] = cli_get_params(
['help' => false, 'method' => null],
['h' => 'help']);
if ($unrecognized) {
$unrecognized = implode("\n ", $unrecognized);
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
}
if ($options['help']) {
echo "Generate secure key
This script manually creates a secure key within the secret data root folder (configured in
config.php as \$CFG->secretdataroot). You must run it using an account with access to write
to that folder.
In normal use Moodle automatically creates the key; this script is intended when setting up
a new Moodle system, for cases where the secure folder is not on shared storage and the key
may be manually installed on multiple servers.
Options:
-h, --help Print out this help
--method <method> Generate key for specified encryption method instead of default.
* sodium
* openssl-aes-256-ctr
Example:
php admin/cli/generate_key.php
";
exit;
}
$method = $options['method'];
if (encryption::key_exists($method)) {
echo 'Key already exists: ' . encryption::get_key_file($method) . "\n";
exit;
}
// Creates key with default permissions (no chmod).
echo "Generating key...\n";
encryption::create_key($method, false);
echo "\nKey created: " . encryption::get_key_file($method) . "\n\n";
echo "If the key folder is not shared storage, then key files should be copied to all servers.\n";

View File

@ -0,0 +1,64 @@
{{!
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/>.
}}
{{!
@template core_admin/admin_setting_encryptedpassword
Admin encrypted password template.
Context variables required for this template:
* name - form element name
* set - whether it is set or empty
* id - element id
Example context (json):
{
"name": "test",
"id": "test0",
"set": true
}
}}
<div class="core_admin_encryptedpassword" data-encryptedpasswordid="{{ id }}"
{{#novalue}}data-novalue="y"{{/novalue}}>
{{#set}}
<span>{{# str }} encryptedpassword_set, admin {{/ str }}</span>
{{/set}}
{{^set}}
<a href="#" title="{{# str }} encryptedpassword_edit, admin {{/ str }}">
<span>{{# str }} novalueclicktoset, form {{/ str }}</span>
{{# pix }} t/passwordunmask-edit, core, {{# str }} passwordunmaskedithint, form {{/ str }}{{/ pix }}
</a>
{{/set}}
<input style="display: none" type="password" name="{{name}}" disabled>
{{!
Using buttons instead of links here allows them to be connected to the label, so the button
works if you click the label.
}}
{{#set}}
<button type="button" id="{{id}}" title="{{# str }} encryptedpassword_edit, admin {{/ str }}" class="btn btn-link" data-editbutton>
{{# pix }} t/passwordunmask-edit, core, {{/ pix }}
</button>
{{/set}}
<button type="button" style="display: none" title="{{# str }} cancel {{/ str }}" class="btn btn-link" data-cancelbutton>
<i class="icon fa fa-times"></i>
</button>
</div>
{{#js}}
require(['core_form/encryptedpassword'], function(encryptedpassword) {
new encryptedpassword.EncryptedPassword("{{ id }}");
});
{{/js}}

View File

@ -89,7 +89,7 @@ class behat_admin extends behat_base {
}
/**
* Sets the specified site settings. A table with | config | value | (optional)plugin | is expected.
* Sets the specified site settings. A table with | config | value | (optional)plugin | (optional)encrypted | is expected.
*
* @Given /^the following config values are set as admin:$/
* @param TableNode $table
@ -103,11 +103,20 @@ class behat_admin extends behat_base {
foreach ($data as $config => $value) {
// Default plugin value is null.
$plugin = null;
$encrypted = false;
if (is_array($value)) {
$plugin = $value[1];
if (array_key_exists(2, $value)) {
$encrypted = $value[2] === 'encrypted';
}
$value = $value[0];
}
if ($encrypted) {
$value = \core\encryption::encrypt($value);
}
set_config($config, $value, $plugin);
}
}

View File

@ -1,5 +1,10 @@
This files describes API changes in /admin/*.
=== 3.11 ===
* New admin setting admin_setting_encryptedpassword allows passwords in admin settings to be
encrypted (with the new \core\encryption API) so that even the admin cannot read them.
=== 3.9 ===
* The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info:

View File

@ -727,6 +727,22 @@ $CFG->admin = 'admin';
//
// $CFG->maxcoursesincategory = 10000;
//
// Admin setting encryption
//
// $CFG->secretdataroot = '/var/www/my_secret_folder';
//
// Location to store encryption keys. By default this is $CFG->dataroot/secret; set this if
// you want to use a different location for increased security (e.g. if too many people have access
// to the main dataroot, or if you want to avoid using shared storage). Your web server user needs
// read access to this location, and write access unless you manually create the keys.
//
// $CFG->nokeygeneration = false;
//
// If you change this to true then the server will give an error if keys don't exist, instead of
// automatically generating them. This is only needed if you want to ensure that keys are consistent
// across a cluster when not using shared storage. If you stop the server generating keys, you will
// need to manually generate them by running 'php admin/cli/generate_key.php'.
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================

View File

@ -575,6 +575,8 @@ $string['enableuserfeedback'] = 'Enable feedback about this software';
$string['enableuserfeedback_desc'] = 'If enabled, a \'Give feedback about this software\' link is displayed in the footer for users to give feedback about the Moodle software to Moodle HQ. If the \'Next feedback reminder\' option is set, the user is also shown a reminder on the Dashboard at the specified interval. Setting \'Next feedback reminder\' to \'Never\' disables the Dashboard reminder, while leaving the \'Give feedback about this software\' link in the footer.';
$string['enablewebservices'] = 'Enable web services';
$string['enablewsdocumentation'] = 'Web services documentation';
$string['encryptedpassword_set'] = '(Set and encrypted)';
$string['encryptedpassword_edit'] = 'Enter new value';
$string['enrolinstancedefaults'] = 'Enrolment instance defaults';
$string['enrolinstancedefaults_desc'] = 'Default enrolment settings in new courses.';
$string['enrolmultipleusers'] = 'Enrol the users';

View File

@ -236,6 +236,12 @@ $string['duplicaterolename'] = 'There is already a role with this name!';
$string['duplicateroleshortname'] = 'There is already a role with this short name!';
$string['duplicateusername'] = 'Duplicate username - skipping record';
$string['emailfail'] = 'Emailing failed';
$string['encryption_encryptfailed'] = 'Encryption failed';
$string['encryption_decryptfailed'] = 'Decryption failed';
$string['encryption_invalidkey'] = 'Invalid key';
$string['encryption_keyalreadyexists'] = 'Key already exists';
$string['encryption_nokey'] = 'Key not found';
$string['encryption_wrongmethod'] = 'Data does not match a supported encryption method';
$string['enddatebeforestartdate'] = 'The course end date must be after the start date.';
$string['error'] = 'Error occurred';
$string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';

View File

@ -2724,6 +2724,58 @@ class admin_setting_configpasswordunmask_with_advanced extends admin_setting_con
}
}
/**
* Admin setting class for encrypted values using secure encryption.
*
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_encryptedpassword extends admin_setting {
/**
* Constructor. Same as parent except that the default value is always an empty string.
*
* @param string $name Internal name used in config table
* @param string $visiblename Name shown on form
* @param string $description Description that appears below field
*/
public function __construct(string $name, string $visiblename, string $description) {
parent::__construct($name, $visiblename, $description, '');
}
public function get_setting() {
return $this->config_read($this->name);
}
public function write_setting($data) {
$data = trim($data);
if ($data === '') {
// Value can really be set to nothing.
$savedata = '';
} else {
// Encrypt value before saving it.
$savedata = \core\encryption::encrypt($data);
}
return ($this->config_write($this->name, $savedata) ? '' : get_string('errorsetting', 'admin'));
}
public function output_html($data, $query='') {
global $OUTPUT;
$default = $this->get_defaultsetting();
$context = (object) [
'id' => $this->get_id(),
'name' => $this->get_full_name(),
'set' => $data !== '',
'novalue' => $this->get_setting() === null
];
$element = $OUTPUT->render_from_template('core_admin/setting_encryptedpassword', $context);
return format_admin_setting($this, $this->visiblename, $element, $this->description,
true, '', $default, $query);
}
}
/**
* Empty setting used to allow flags (advanced) on settings that can have no sensible default.
* Note: Only advanced makes sense right now - locked does not.

318
lib/classes/encryption.php Normal file
View File

@ -0,0 +1,318 @@
<?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/>.
/**
* Class used to encrypt or decrypt data.
*
* @package core
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core;
/**
* Class used to encrypt or decrypt data.
*
* @package core
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class encryption {
/** @var string Encryption method: Sodium */
const METHOD_SODIUM = 'sodium';
/** @var string Encryption method: hand-coded OpenSSL (less safe) */
const METHOD_OPENSSL = 'openssl-aes-256-ctr';
/** @var string OpenSSL cipher method */
const OPENSSL_CIPHER = 'AES-256-CTR';
/**
* Checks if Sodium is installed.
*
* @return bool True if the Sodium extension is available
*/
public static function is_sodium_installed(): bool {
return extension_loaded('sodium');
}
/**
* Gets the encryption method to use. We use the Sodium extension if it is installed, or
* otherwise, OpenSSL.
*
* @return string Current encryption method
*/
protected static function get_encryption_method(): string {
if (self::is_sodium_installed()) {
return self::METHOD_SODIUM;
} else {
return self::METHOD_OPENSSL;
}
}
/**
* Creates a key for the server.
*
* @param string|null $method Encryption method (only if you want to create a non-default key)
* @param bool $chmod If true, restricts the file access of the key
* @throws \moodle_exception If the server already has a key, or there is an error
*/
public static function create_key(?string $method = null, bool $chmod = true): void {
if ($method === null) {
$method = self::get_encryption_method();
}
if (self::key_exists($method)) {
throw new \moodle_exception('encryption_keyalreadyexists', 'error');
}
// Don't make it read-only in Behat or it will fail to clear for future runs.
if (defined('BEHAT_SITE_RUNNING')) {
$chmod = false;
}
// Generate the key.
switch ($method) {
case self::METHOD_SODIUM:
$key = sodium_crypto_secretbox_keygen();
break;
case self::METHOD_OPENSSL:
$key = openssl_random_pseudo_bytes(32);
break;
default:
throw new \coding_exception('Unknown method: ' . $method);
}
// Store the key, making it readable only by server.
$folder = self::get_key_folder();
check_dir_exists($folder);
$keyfile = self::get_key_file($method);
file_put_contents($keyfile, $key);
if ($chmod) {
chmod($keyfile, 0400);
}
}
/**
* Gets the folder used to store the secret key.
*
* @return string Folder path
*/
protected static function get_key_folder(): string {
global $CFG;
return ($CFG->secretdataroot ?? $CFG->dataroot . '/secret') . '/key';
}
/**
* Gets the file path used to store the secret key. The filename contains the cipher method,
* so that if necessary to transition in future it would be possible to have multiple.
*
* @param string|null $method Encryption method (only if you want to get a non-default key)
* @return string Full path to file
*/
public static function get_key_file(?string $method = null): string {
if ($method === null) {
$method = self::get_encryption_method();
}
return self::get_key_folder() . '/' . $method . '.key';
}
/**
* Checks if there is a key file.
*
* @param string|null $method Encryption method (only if you want to check a non-default key)
* @return bool True if there is a key file
*/
public static function key_exists(?string $method = null): bool {
if ($method === null) {
$method = self::get_encryption_method();
}
return file_exists(self::get_key_file($method));
}
/**
* Gets the current key, automatically creating it if there isn't one yet.
*
* @param string|null $method Encryption method (only if you want to get a non-default key)
* @return string The key (binary)
* @throws \moodle_exception If there isn't one already (and creation is disabled)
*/
protected static function get_key(?string $method = null): string {
global $CFG;
if ($method === null) {
$method = self::get_encryption_method();
}
$keyfile = self::get_key_file($method);
if (!file_exists($keyfile) && empty($CFG->nokeygeneration)) {
self::create_key($method);
}
$result = @file_get_contents($keyfile);
if ($result === false) {
throw new \moodle_exception('encryption_nokey', 'error');
}
return $result;
}
/**
* Gets the length in bytes of the initial values data required.
*
* @param string $method Crypto method
* @return int Length in bytes
*/
protected static function get_iv_length(string $method): int {
switch ($method) {
case self::METHOD_SODIUM:
return SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
case self::METHOD_OPENSSL:
return openssl_cipher_iv_length(self::OPENSSL_CIPHER);
default:
throw new \coding_exception('Unknown method: ' . $method);
}
}
/**
* Encrypts data using the server's key.
*
* Note there is a special case - the empty string is not encrypted.
*
* @param string $data Data to encrypt, or empty string for no data
* @param string|null $method Encryption method (only if you want to use a non-default method)
* @return string Encrypted data, or empty string for no data
* @throws \moodle_exception If the key doesn't exist, or the string is too long
*/
public static function encrypt(string $data, ?string $method = null): string {
if ($data === '') {
return '';
} else {
if ($method === null) {
$method = self::get_encryption_method();
}
// Create IV.
$iv = random_bytes(self::get_iv_length($method));
// Encrypt data.
switch($method) {
case self::METHOD_SODIUM:
try {
$encrypted = sodium_crypto_secretbox($data, $iv, self::get_key($method));
} catch (\SodiumException $e) {
throw new \moodle_exception('encryption_encryptfailed', 'error', '', null, $e->getMessage());
}
break;
case self::METHOD_OPENSSL:
// This may not be a secure authenticated encryption implementation;
// administrators should enable the Sodium extension.
$key = self::get_key($method);
if (strlen($key) !== 32) {
throw new \moodle_exception('encryption_invalidkey', 'error');
}
$encrypted = @openssl_encrypt($data, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv);
if ($encrypted === false) {
throw new \moodle_exception('encryption_encryptfailed', 'error',
'', null, openssl_error_string());
}
$hmac = hash_hmac('sha256', $iv . $encrypted, $key, true);
$encrypted .= $hmac;
break;
default:
throw new \coding_exception('Unknown method: ' . $method);
}
// Encrypted data is cipher method plus IV plus encrypted data.
return $method . ':' . base64_encode($iv . $encrypted);
}
}
/**
* Decrypts data using the server's key. The decryption works with either supported method.
*
* @param string $data Data to decrypt
* @return string Decrypted data
*/
public static function decrypt(string $data): string {
if ($data === '') {
return '';
} else {
if (preg_match('~^(' . self::METHOD_OPENSSL . '|' . self::METHOD_SODIUM . '):~', $data, $matches)) {
$method = $matches[1];
} else {
throw new \moodle_exception('encryption_wrongmethod', 'error');
}
$realdata = base64_decode(substr($data, strlen($method) + 1), true);
if ($realdata === false) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, 'Invalid base64 data');
}
$ivlength = self::get_iv_length($method);
if (strlen($realdata) < $ivlength + 1) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, 'Insufficient data');
}
$iv = substr($realdata, 0, $ivlength);
$encrypted = substr($realdata, $ivlength);
switch ($method) {
case self::METHOD_SODIUM:
try {
$decrypted = sodium_crypto_secretbox_open($encrypted, $iv, self::get_key($method));
} catch (\SodiumException $e) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, $e->getMessage());
}
// Sodium returns false if decryption fails because data is invalid.
if ($decrypted === false) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, 'Integrity check failed');
}
break;
case self::METHOD_OPENSSL:
if (strlen($encrypted) < 33) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, 'Insufficient data');
}
$hmac = substr($encrypted, -32);
$encrypted = substr($encrypted, 0, -32);
$key = self::get_key($method);
$expectedhmac = hash_hmac('sha256', $iv . $encrypted, $key, true);
if ($hmac !== $expectedhmac) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, 'Integrity check failed');
}
$decrypted = @openssl_decrypt($encrypted, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv);
if ($decrypted === false) {
throw new \moodle_exception('encryption_decryptfailed', 'error',
'', null, openssl_error_string());
}
break;
default:
throw new \coding_exception('Unknown method: ' . $method);
}
return $decrypted;
}
}
}

View File

@ -0,0 +1,2 @@
define ("core_form/encryptedpassword",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.EncryptedPassword=void 0;var b=function(a){var b=this,c=document.querySelector("div[data-encryptedpasswordid=\""+a+"\"]");this.spanOrLink=c.querySelector("span, a");this.input=c.querySelector("input");this.editButtonOrLink=c.querySelector("button[data-editbutton], a");this.cancelButton=c.querySelector("button[data-cancelbutton]");var d=function(a){a.stopImmediatePropagation();a.preventDefault();b.startEditing(!0)};this.editButtonOrLink.addEventListener("click",d);if("A"===this.editButtonOrLink.nodeName){c.parentElement.previousElementSibling.querySelector("label").addEventListener("click",d)}this.cancelButton.addEventListener("click",function(a){a.stopImmediatePropagation();a.preventDefault();b.cancelEditing()});if("y"===c.dataset.novalue){this.startEditing(!1);this.cancelButton.style.display="none"}};a.EncryptedPassword=b;b.prototype.startEditing=function(a){this.input.style.display="inline";this.input.disabled=!1;this.spanOrLink.style.display="none";this.editButtonOrLink.style.display="none";this.cancelButton.style.display="inline";var b=this.editButtonOrLink.id;this.editButtonOrLink.removeAttribute("id");this.input.id=b;if(a){this.input.focus()}};b.prototype.cancelEditing=function(){this.input.style.display="none";this.input.value="";this.input.disabled=!0;this.spanOrLink.style.display="inline";this.editButtonOrLink.style.display="inline";this.cancelButton.style.display="none";var a=this.input.id;this.input.removeAttribute("id");this.editButtonOrLink.id=a}});
//# sourceMappingURL=encryptedpassword.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,104 @@
// 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/>.
/**
* Encrypted password functionality.
*
* @module core_form/encryptedpassword
* @package core_form
* @class encryptedpassword
* @copyright 2019 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Constructor for EncryptedPassword.
*
* @param {String} elementId The element to apply the encrypted password JS to
*/
export const EncryptedPassword = function(elementId) {
const wrapper = document.querySelector('div[data-encryptedpasswordid="' + elementId + '"]');
this.spanOrLink = wrapper.querySelector('span, a');
this.input = wrapper.querySelector('input');
this.editButtonOrLink = wrapper.querySelector('button[data-editbutton], a');
this.cancelButton = wrapper.querySelector('button[data-cancelbutton]');
// Edit button action.
var editHandler = (e) => {
e.stopImmediatePropagation();
e.preventDefault();
this.startEditing(true);
};
this.editButtonOrLink.addEventListener('click', editHandler);
// When it's a link, do some magic to make the label work as well.
if (this.editButtonOrLink.nodeName === 'A') {
wrapper.parentElement.previousElementSibling.querySelector('label').addEventListener('click', editHandler);
}
// Cancel button action.
this.cancelButton.addEventListener('click', (e) => {
e.stopImmediatePropagation();
e.preventDefault();
this.cancelEditing();
});
// If the value is not set yet, start editing and remove the cancel option - so that
// it saves something in the config table and doesn't keep repeat showing it as a new
// admin setting...
if (wrapper.dataset.novalue === 'y') {
this.startEditing(false);
this.cancelButton.style.display = 'none';
}
};
/**
* Starts editing.
*
* @param {Boolean} moveFocus If true, sets focus to the edit box
*/
EncryptedPassword.prototype.startEditing = function(moveFocus) {
this.input.style.display = 'inline';
this.input.disabled = false;
this.spanOrLink.style.display = 'none';
this.editButtonOrLink.style.display = 'none';
this.cancelButton.style.display = 'inline';
// Move the id around, which changes what happens when you click the label.
const id = this.editButtonOrLink.id;
this.editButtonOrLink.removeAttribute('id');
this.input.id = id;
if (moveFocus) {
this.input.focus();
}
};
/**
* Cancels editing.
*/
EncryptedPassword.prototype.cancelEditing = function() {
this.input.style.display = 'none';
this.input.value = '';
this.input.disabled = true;
this.spanOrLink.style.display = 'inline';
this.editButtonOrLink.style.display = 'inline';
this.cancelButton.style.display = 'none';
// Move the id around, which changes what happens when you click the label.
const id = this.input.id;
this.input.removeAttribute('id');
this.editButtonOrLink.id = id;
};

View File

@ -0,0 +1,265 @@
<?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;
/**
* Test encryption.
*
* @package core
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class encryption_testcase extends \basic_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]];
}
/**
* 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);
}
/**
* Tests encryption and decryption with empty strings.
*
* @throws \moodle_exception
*/
public function test_encrypt_and_decrypt_empty(): void {
$this->assertEquals('', encryption::encrypt(''));
$this->assertEquals('', encryption::decrypt(''));
}
/**
* Tests encryption when the keys weren't created yet.
*
* @param string $method Encryption method
* @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);
}
/**
* Tests decryption when the data has a different encryption method
*/
public function test_decrypt_wrongmethod(): void {
$this->expectExceptionMessage('Data does not match a supported encryption method');
encryption::decrypt('FAKE-CIPHER-METHOD:xx');
}
/**
* 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 automatic generation of keys when needed.
*
* @dataProvider encryption_method_provider
* @param string $method Encryption method
*/
public function test_auto_key_generation(string $method): void {
$this->require_sodium($method);
// Allow automatic generation (default).
$encrypted = encryption::encrypt('frogs', $method);
$this->assertEquals('frogs', encryption::decrypt($encrypted));
}
/**
* Checks that invalid key causes failures.
*
* @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->expectExceptionMessage('key size should be');
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)));
}
}

View File

@ -0,0 +1,31 @@
<?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/>.
namespace core;
/**
* Testable version of the encryption class - just makes it possible to unit-test protected
* function.
*
* @package core
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_encryption extends encryption {
public static function get_key(?string $method = null): string {
return parent::get_key($method);
}
}

View File

@ -7,6 +7,10 @@ information provided here is intended especially for developers.
* Final deprecation i_dock_block() in behat_deprecated.php
* Final deprecation of get_courses_page. Function has been removed and core_course_category::get_courses() should be
used instead.
* New encryption API in \core\encryption allows secure encryption and decryption of data. By
default the key is stored in moodledata but admins can configure a different, more secure
location in config.php if required. To get the best possible security for this feature, we
recommend enabling the Sodium PHP extension.
=== 3.10 ===
* PHPUnit has been upgraded to 8.5. That comes with a few changes: