Skip to content

Commit

Permalink
MDL-65818 Security: Encryption API and admin setting for secure data
Browse files Browse the repository at this point in the history
  • Loading branch information
sammarshallou committed Dec 4, 2020
1 parent 7fa836c commit ddbafce
Show file tree
Hide file tree
Showing 15 changed files with 957 additions and 1 deletion.
77 changes: 77 additions & 0 deletions admin/cli/generate_key.php
@@ -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";
64 changes: 64 additions & 0 deletions admin/templates/setting_encryptedpassword.mustache
@@ -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}}
11 changes: 10 additions & 1 deletion admin/tests/behat/behat_admin.php
Expand Up @@ -89,7 +89,7 @@ public function i_set_the_following_administration_settings_values(TableNode $ta
}

/**
* 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
Expand All @@ -103,11 +103,20 @@ public function the_following_config_values_are_set_as_admin(TableNode $table) {
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);
}
}
Expand Down
5 changes: 5 additions & 0 deletions admin/upgrade.txt
@@ -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:
Expand Down
16 changes: 16 additions & 0 deletions config-dist.php
Expand Up @@ -727,6 +727,22 @@
//
// $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!!!
//=========================================================================
Expand Down
2 changes: 2 additions & 0 deletions lang/en/admin.php
Expand Up @@ -575,6 +575,8 @@
$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';
Expand Down
6 changes: 6 additions & 0 deletions lang/en/error.php
Expand Up @@ -236,6 +236,12 @@
$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.';
Expand Down
52 changes: 52 additions & 0 deletions lib/adminlib.php
Expand Up @@ -2724,6 +2724,58 @@ public function __construct($name, $visiblename, $description, $defaultsetting)
}
}

/**
* 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.
Expand Down

0 comments on commit ddbafce

Please sign in to comment.