Skip to content

Commit

Permalink
(dev/core#2258) Add System.rotateKey API
Browse files Browse the repository at this point in the history
  • Loading branch information
totten committed Dec 30, 2020
1 parent fe4d2bd commit b124f74
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 0 deletions.
41 changes: 41 additions & 0 deletions CRM/Utils/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,47 @@ public static function permission_check($permission, &$granted, $contactId) {
);
}

/**
* 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
Expand Down
81 changes: 81 additions & 0 deletions Civi/Api4/Action/System/RotateKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?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);
}

}
10 changes: 10 additions & 0 deletions Civi/Api4/System.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ public static function check($checkPermissions = TRUE) {
->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
Expand Down
61 changes: 61 additions & 0 deletions tests/phpunit/api/v4/Entity/SystemRotateKeyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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,
]);
}

}

0 comments on commit b124f74

Please sign in to comment.