// 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;
+ }
}
/*
$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
*/