);
}
+ /**
+ * Rotate the cryptographic key used in the database.
+ *
+ * The purpose of this hook is to visit any encrypted values in the database
+ * and re-encrypt the content.
+ *
+ * For values encoded via `CryptoToken`, you can use `CryptoToken::rekey($oldToken, $tag)`
+ *
+ * @param string $tag
+ * The type of crypto-key that is currently being rotated.
+ * The hook-implementer should use this to decide which (if any) fields to visit.
+ * Ex: 'CRED'
+ * @param \Psr\Log\LoggerInterface $log
+ * List of messages about re-keyed values.
+ *
+ * @code
+ * function example_civicrm_rekey($tag, &$log) {
+ * if ($tag !== 'CRED') return;
+ *
+ * $cryptoToken = Civi::service('crypto.token');
+ * $rows = sql('SELECT id, secret_column FROM some_table');
+ * foreach ($rows as $row) {
+ * $new = $cryptoToken->rekey($row['secret_column']);
+ * if ($new !== NULL) {
+ * sql('UPDATE some_table SET secret_column = %1 WHERE id = %2',
+ * $new, $row['id']);
+ * }
+ * }
+ * }
+ * @endCode
+ *
+ * @return null
+ * The return value is ignored
+ */
+ public static function cryptoRotateKey($tag, $log) {
+ return self::singleton()->invoke(['tag', 'log'], $tag, $log, self::$_nullObject,
+ self::$_nullObject, self::$_nullObject, self::$_nullObject,
+ 'civicrm_cryptoRotateKey'
+ );
+ }
+
/**
* @param CRM_Core_Exception $exception
* @param mixed $request
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\System;
+
+use Civi\Api4\Generic\AbstractAction;
+use Civi\Api4\Generic\Result;
+
+/**
+ * Rotate the keys used for encrypted database content.
+ *
+ * Crypto keys are loaded from the CryptoRegistry based on tag name. Each tag will
+ * have one preferred key and 0+ legacy keys. They rekey operation finds any
+ * old content (based on legacy keys) and rewrites it (using the preferred key).
+ *
+ * @method string getTag()
+ * @method $this setTag(string $tag)
+ */
+class RotateKey extends AbstractAction {
+
+ /**
+ * Tag name (e.g. "CRED")
+ *
+ * @var string
+ */
+ protected $tag;
+
+ /**
+ * @param \Civi\Api4\Generic\Result $result
+ *
+ * @throws \API_Exception
+ * @throws \Civi\Crypto\Exception\CryptoException
+ */
+ public function _run(Result $result) {
+ if (empty($this->tag)) {
+ throw new \API_Exception("Missing required argument: tag");
+ }
+
+ // Track log of changes in memory.
+ $logger = new class() extends \Psr\Log\AbstractLogger {
+
+ /**
+ * @var array
+ */
+ public $log = [];
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ */
+ public function log($level, $message, array $context = []) {
+ $evalVar = function($m) use ($context) {
+ return $context[$m[1]] ?? '';
+ };
+
+ $this->log[] = [
+ 'level' => $level,
+ 'message' => preg_replace_callback('/\{([a-zA-Z0-9\.]+)\}/', $evalVar, $message),
+ ];
+ }
+
+ };
+
+ \CRM_Utils_Hook::cryptoRotateKey($this->tag, $logger);
+
+ $result->exchangeArray($logger->log);
+ }
+
+}
->setCheckPermissions($checkPermissions);
}
+ /**
+ * @param bool $checkPermissions
+ *
+ * @return Action\System\RotateKey
+ */
+ public static function rotateKey($checkPermissions = TRUE) {
+ return (new Action\System\RotateKey(__CLASS__, __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
/**
* @param bool $checkPermissions
* @return Generic\BasicGetFieldsAction
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace api\v4\Entity;
+
+use api\v4\UnitTestCase;
+use Civi\Crypto\CryptoTestTrait;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @group headless
+ */
+class RotateKeyTest extends UnitTestCase {
+
+ use CryptoTestTrait;
+
+ /**
+ * Set up baseline for testing
+ */
+ public function setUp() {
+ parent::setUp();
+ \CRM_Utils_Hook::singleton()->setHook('civicrm_crypto', [$this, 'registerExampleKeys']);
+ \CRM_Utils_Hook::singleton()->setHook('civicrm_cryptoRotateKey', [$this, 'onRotateKey']);
+ }
+
+ public function testRekey() {
+ $result = \Civi\Api4\System::rotateKey(0)->setTag('UNIT-TEST')->execute();
+ $this->assertEquals(2, count($result));
+ $this->assertEquals('Updated field A using UNIT-TEST.', $result[0]['message']);
+ $this->assertEquals('info', $result[0]['level']);
+ $this->assertEquals('Updated field B using UNIT-TEST.', $result[1]['message']);
+ $this->assertEquals('info', $result[1]['level']);
+ }
+
+ public function onRotateKey(string $tag, LoggerInterface $log) {
+ $this->assertEquals('UNIT-TEST', $tag);
+ $log->info('Updated field A using {tag}.', [
+ 'tag' => $tag,
+ ]);
+ $log->info('Updated field B using {tag}.', [
+ 'tag' => $tag,
+ ]);
+ }
+
+}