diff --git a/Civi/Crypto/CryptoToken.php b/Civi/Crypto/CryptoToken.php index e45f962b9fa5..038f405ce2d2 100644 --- a/Civi/Crypto/CryptoToken.php +++ b/Civi/Crypto/CryptoToken.php @@ -34,17 +34,25 @@ * * - Plain text: Any string which does not begin with chr(2) * - Encrypted text: A string in the format: - * TOKEN := DLM + VERSION + DLM + KEY_ID + DLM + CIPHERTEXT - * DLM := ASCII CHAR #2 - * VERSION := String, 4-digit, alphanumeric (as in "CTK0") - * KEY_ID := String, alphanumeric and symbols "_-.,:;=+/\" + * TOKEN := DLM + FMT + QUERY + * DLM := ASCII char #2 + * FMT := String, 4-digit, alphanumeric (as in "CTK?") + * QUERY := String, URL-encoded key-value pairs, + * "k", the key ID (alphanumeric and symbols "_-.,:;=+/\") + * "t", the text (base64-encoded ciphertext) * * @package Civi\Crypto */ class CryptoToken { - const VERSION_1 = 'CTK0'; + /** + * Format identification code + */ + const FMT_QUERY = 'CTK?'; + /** + * @var string + */ protected $delim; /** @@ -90,7 +98,11 @@ public function encrypt($plainText, $keyIdOrTag) { /** @var \Civi\Crypto\CipherSuiteInterface $cipherSuite */ $cipherSuite = $registry->findSuite($key['suite']); $cipherText = $cipherSuite->encrypt($plainText, $key); - return $this->delim . self::VERSION_1 . $this->delim . $key['id'] . $this->delim . base64_encode($cipherText); + + return $this->delim . self::FMT_QUERY . \http_build_query([ + 'k' => $key['id'], + 't' => \CRM_Utils_String::base64UrlEncode($cipherText), + ]); } /** @@ -118,16 +130,21 @@ public function decrypt($token, $keyIdOrTag = '*') { /** @var CryptoRegistry $registry */ $registry = \Civi::service('crypto.registry'); - $parts = explode($this->delim, $token, 4); - if (count($parts) !== 4 || $parts[1] !== self::VERSION_1) { - throw new CryptoException("Cannot decrypt token. Invalid format."); + $fmt = substr($token, 1, 4); + switch ($fmt) { + case self::FMT_QUERY: + parse_str(substr($token, 5), $tokenData); + $keyId = $tokenData['k']; + $cipherText = \CRM_Utils_String::base64UrlDecode($tokenData['t']); + break; + + default: + throw new CryptoException("Cannot decrypt token. Invalid format."); } - $keyId = $parts[2]; - $cipherText = base64_decode($parts[3]); $key = $registry->findKey($keyId); if (!in_array('*', $keyIdOrTag) && !in_array($keyId, $keyIdOrTag) && empty(array_intersect($keyIdOrTag, $key['tags']))) { - throw new CryptoException("Cannot decrypt token. Unexpected key: $keyId"); + throw new CryptoException("Cannot decrypt token. Unexpected key: {$keyId}"); } /** @var \Civi\Crypto\CipherSuiteInterface $cipherSuite */ diff --git a/tests/phpunit/Civi/Crypto/CryptoTokenTest.php b/tests/phpunit/Civi/Crypto/CryptoTokenTest.php index fa949ca5cdda..d626698df5db 100644 --- a/tests/phpunit/Civi/Crypto/CryptoTokenTest.php +++ b/tests/phpunit/Civi/Crypto/CryptoTokenTest.php @@ -51,7 +51,8 @@ public function testDecryptInvalid() { $this->assertEquals('mess with me', $cryptoToken->decrypt($goodExample)); try { - $badExample = preg_replace(';CTK0;', 'ctk9', $goodExample); + $badExample = preg_replace(';CTK\?;', 'ctk9', $goodExample); + $this->assertTrue($badExample !== $goodExample); $cryptoToken->decrypt($badExample); $this->fail("Expected CryptoException"); } @@ -64,11 +65,11 @@ public function getExampleTokens() { return [ // [ 'Plain text', 'Encryption Key ID', 'expectTokenRegex', 'expectTokenLen', 'expectPlain' ] ['hello world. can you see me', 'plain', '/^hello world. can you see me/', 27, TRUE], - ['hello world. i am secret.', 'UNIT-TEST', '/^.CTK0.asdf-key-1./', 81, FALSE], - ['hello world. we b secret.', 'asdf-key-0', '/^.CTK0.asdf-key-0./', 81, FALSE], - ['hello world. u ur secret.', 'asdf-key-1', '/^.CTK0.asdf-key-1./', 81, FALSE], - ['hello world. he z secret.', 'asdf-key-2', '/^.CTK0.asdf-key-2./', 73, FALSE], - ['hello world. whos secret.', 'asdf-key-3', '/^.CTK0.asdf-key-3./', 125, FALSE], + ['hello world. i am secret.', 'UNIT-TEST', '/^.CTK\?k=asdf-key-1&/', 84, FALSE], + ['hello world. we b secret.', 'asdf-key-0', '/^.CTK\?k=asdf-key-0&/', 84, FALSE], + ['hello world. u ur secret.', 'asdf-key-1', '/^.CTK\?k=asdf-key-1&/', 84, FALSE], + ['hello world. he z secret.', 'asdf-key-2', '/^.CTK\?k=asdf-key-2&/', 75, FALSE], + ['hello world. whos secret.', 'asdf-key-3', '/^.CTK\?k=asdf-key-3&/', 127, FALSE], ]; }