MDL-36580 backup: General support for decrypted contents in restore
- Uses site generated (on backup) key. - Can be applied potentially everywhere in the restore process. - Covered with unit tests. - Authentication / integrity aware so can be used between any 2 servers (just requires matching key).
This commit is contained in:
@@ -94,6 +94,66 @@ abstract class restore_step extends base_step {
|
||||
// Return the passed value with cached offset applied.
|
||||
return $value + $cache[$this->get_restoreid()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns symmetric-key AES-256 decryption of base64 encoded contents.
|
||||
*
|
||||
* This method is used in restore operations to decrypt contents encrypted with
|
||||
* {@link encrypted_final_element} automatically decoding (base64) and decrypting
|
||||
* contents using the key stored in backup_encryptkey config.
|
||||
*
|
||||
* Requires openssl, cipher availability, and key existence (backup
|
||||
* automatically sets it if missing). Integrity is provided via HMAC.
|
||||
*
|
||||
* @param string $value {@link encrypted_final_element} value to decode and decrypt.
|
||||
* @return string|null decoded and decrypted value or null if the operation can not be performed.
|
||||
*/
|
||||
public function decrypt($value) {
|
||||
|
||||
// No openssl available, skip this field completely.
|
||||
if (!function_exists('openssl_encrypt')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// No hash available, skip this field completely.
|
||||
if (!function_exists('hash_hmac')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cypher not available, skip this field completely.
|
||||
if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the decrypt key. Skip if missing.
|
||||
$key = get_config('backup', 'backup_encryptkey');
|
||||
if ($key === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// And decode it.
|
||||
$key = base64_decode($key);
|
||||
|
||||
// Arrived here, let's proceed with authentication (provides integrity).
|
||||
$hmaclen = 32; // SHA256 is 32 bytes.
|
||||
$ivlen = openssl_cipher_iv_length(backup::CIPHER);
|
||||
list($hmac, $iv, $text) = array_values(unpack("a{$hmaclen}hmac/a{$ivlen}iv/a*text", base64_decode($value)));
|
||||
|
||||
// Verify HMAC matches expectations, skip if not (integrity failed).
|
||||
if (!hash_equals($hmac, hash_hmac('sha256', $iv . $text, $key, true))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Arrived here, integrity is ok, let's decrypt.
|
||||
$result = openssl_decrypt($text, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
// For some reason decrypt failed (strange, HMAC check should have deteted it), skip this field completely.
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -91,6 +91,52 @@ class backup_step_testcase extends advanced_testcase {
|
||||
$bc->destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* test restore_step class, decrypt method
|
||||
*/
|
||||
public function test_restore_step_decrypt() {
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
if (!function_exists('openssl_encrypt')) {
|
||||
$this->markTestSkipped('OpenSSL extension is not loaded.');
|
||||
|
||||
} else if (!function_exists('hash_hmac')) {
|
||||
$this->markTestSkipped('Hash extension is not loaded.');
|
||||
|
||||
} else if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) {
|
||||
$this->markTestSkipped('Expected cipher not available: ' . backup::CIPHER);
|
||||
}
|
||||
|
||||
$bt = new mock_restore_task_basepath('taskname');
|
||||
$bs = new mock_restore_structure_step('steptest', null, $bt);
|
||||
$this->assertTrue(method_exists($bs, 'decrypt'));
|
||||
|
||||
// Let's prepare a string for being decrypted.
|
||||
$secret = 'This is a secret message that nobody else will be able to read but me 💩 ';
|
||||
$key = hash('md5', 'Moodle rocks and this is not secure key, who cares, it is a test');
|
||||
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(backup::CIPHER));
|
||||
$message = $iv . openssl_encrypt($secret, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
|
||||
$hmac = hash_hmac('sha256', $message, $key, true);
|
||||
$crypt = base64_encode($hmac . $message);
|
||||
|
||||
// Running it without a key configured, returns null.
|
||||
$this->assertNull($bs->decrypt($crypt));
|
||||
|
||||
// Store the key into config.
|
||||
set_config('backup_encryptkey', base64_encode($key), 'backup');
|
||||
|
||||
// Verify decrypt works and returns original.
|
||||
$this->assertSame($secret, $bs->decrypt($crypt));
|
||||
|
||||
// Finally, test the integrity failure detection is working.
|
||||
// (this can be caused by changed hmac, key or message, in
|
||||
// this case we are just forcing it via changed hmac).
|
||||
$hmac = md5($message);
|
||||
$crypt = base64_encode($hmac . $message);
|
||||
$this->assertNull($bs->decrypt($crypt));
|
||||
}
|
||||
|
||||
/**
|
||||
* test backup_structure_step class
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user