Skip to content

Commit

Permalink
(dev/core#2258) CryptoToken - Add rekey method
Browse files Browse the repository at this point in the history
  • Loading branch information
totten committed Dec 30, 2020
1 parent 88ccbca commit fe4d2bd
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 0 deletions.
34 changes: 34 additions & 0 deletions Civi/Crypto/CryptoToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ public function decrypt($token, $keyIdOrTag = '*') {
return $plainText;
}

/**
* Re-encrypt an existing token with a newer version of the key.
*
* @param string $oldToken
* @param string $keyTag
* Ex: 'CRED'
*
* @return string|null
* A re-encrypted version of $oldToken, or NULL if there should be no change.
* @throws \Civi\Crypto\Exception\CryptoException
*/
public function rekey($oldToken, $keyTag) {
/** @var \Civi\Crypto\CryptoRegistry $registry */
$registry = \Civi::service('crypto.registry');

$sourceKeys = $registry->findKeysByTag($keyTag);
$targetKey = array_shift($sourceKeys);

if ($this->isPlainText($oldToken)) {
if ($targetKey['suite'] === 'plain') {
return NULL;
}
}
else {
$tokenData = $this->parse($oldToken);
if ($tokenData['k'] === $targetKey['id'] || !isset($sourceKeys[$tokenData['k']])) {
return NULL;
}
}

$decrypted = $this->decrypt($oldToken);
return $this->encrypt($decrypted, $targetKey['id']);
}

/**
* Parse the content of a token (without decrypting it).
*
Expand Down
60 changes: 60 additions & 0 deletions tests/phpunit/Civi/Crypto/CryptoTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,66 @@ public function testRoundtrip($inputText, $inputKeyIdOrTag, $expectTokenRegex, $
$this->assertEquals($inputText, $actualText);
}

public function testRekeyCiphertext() {
/** @var \Civi\Crypto\CryptoRegistry $cryptoRegistry */
$cryptoRegistry = \Civi::service('crypto.registry');
/** @var \Civi\Crypto\CryptoToken $cryptoToken */
$cryptoToken = \Civi::service('crypto.token');

$first = $cryptoToken->encrypt("hello world", 'UNIT-TEST');
$this->assertRegExp(';k=asdf-key-1;', $first);
$this->assertEquals('hello world', $cryptoToken->decrypt($first));

// If the keys haven't changed yet, then rekey() is a null-op.
$second = $cryptoToken->rekey($first, 'UNIT-TEST');
$this->assertTrue($second === NULL);

// But if we add a newer key, then rekey() will yield new token.
$cryptoRegistry->addSymmetricKey($cryptoRegistry->parseKey('::foo') + [
'tags' => ['UNIT-TEST'],
'weight' => -100,
'id' => 'new-key',
]);
$third = $cryptoToken->rekey($first, 'UNIT-TEST');
$this->assertNotRegExp(';k=asdf-key-1;', $third);
$this->assertRegExp(';k=new-key;', $third);
$this->assertEquals('hello world', $cryptoToken->decrypt($third));
}

public function testRekeyUpgradeDowngradePlaintext() {
/** @var \Civi\Crypto\CryptoRegistry $cryptoRegistry */
$cryptoRegistry = \Civi::service('crypto.registry');
/** @var \Civi\Crypto\CryptoToken $cryptoToken */
$cryptoToken = \Civi::service('crypto.token');

// In the first pass, we have no real key.
$cryptoRegistry->addPlainText(['tags' => ['APPLE'], 'weight' => -1]);
$first = $cryptoToken->encrypt("hello world", 'APPLE');
$this->assertEquals('hello world', $first);
$this->assertEquals('hello world', $cryptoToken->decrypt($first));

// If the keys haven't changed yet, then rekey() is a null-op.
$second = $cryptoToken->rekey($first, 'APPLE');
$this->assertTrue($second === NULL);

// But if we add a key, then it takes precedence.
$cryptoRegistry->addSymmetricKey($cryptoRegistry->parseKey('::applepie') + [
'tags' => ['APPLE'],
'weight' => -3,
'id' => 'interim-key',
]);
$third = $cryptoToken->rekey($first, 'APPLE');
$this->assertRegExp(';k=interim-key;', $third);
$this->assertEquals('hello world', $cryptoToken->decrypt($third));

// But if we add another key with earlier priority,
$cryptoRegistry->addPlainText(['tags' => ['APPLE'], 'weight' => -4]);
$fourth = $cryptoToken->rekey($third, 'APPLE');
$this->assertEquals('hello world', $fourth);
$this->assertEquals('hello world', $cryptoToken->decrypt($fourth));

}

public function testReadPlainTextWithoutRegistry() {
// This is performance optimization - don't initialize crypto.registry unless
// you actually need it.
Expand Down

0 comments on commit fe4d2bd

Please sign in to comment.