3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
12 namespace Civi\Crypto
;
14 use Civi\Crypto\Exception\CryptoException
;
17 * The "Crypto Token" service supports a token format suitable for
18 * storing specific values in the database with encryption. Characteristics:
20 * - Primarily designed to defend confidentiality in case of data-leaks
21 * (SQL injections, lost backups, etc).
22 * - NOT appropriate for securing data-transmission. Data-transmission
23 * requires more protections (eg mandatory TTLs + signatures). If you need
24 * that, consider adding a JWT/JWS/JWE implementation.
25 * - Data-format allows phase-in/phase-out. If you have a datum that was written
26 * with an old key or with no key, it will still be readable.
28 * USAGE: The "encrypt()" and "decrypt()" methods are the primary interfaces.
30 * $encrypted = Civi::service('crypto.token')->encrypt('my-mail-password, 'KEY_ID_OR_TAG');
31 * $decrypted = Civi::service('crypto.token')->decrypt($encrypted, '*');
33 * FORMAT: An encoded token may be in either of these formats:
35 * - Plain text: Any string which does not begin with chr(2)
36 * - Encrypted text: A string in the format:
37 * TOKEN := DLM + VERSION + DLM + KEY_ID + DLM + CIPHERTEXT
38 * DLM := ASCII CHAR #2
39 * VERSION := String, 4-digit, alphanumeric (as in "CTK0")
40 * KEY_ID := String, alphanumeric and symbols "_-.,:;=+/\"
42 * @package Civi\Crypto
46 const VERSION_1
= 'CTK0';
51 * CryptoToken constructor.
53 public function __construct() {
54 $this->delim
= chr(2);
58 * Determine if a string looks like plain-text.
60 * @param string $plainText
63 public function isPlainText($plainText) {
64 return is_string($plainText) && ($plainText === '' ||
$plainText{0} !== $this->delim
);
68 * Create an encrypted token (given the plaintext).
70 * @param string $plainText
71 * The secret value to encode (e.g. plain-text password).
72 * @param string|string[] $keyIdOrTag
73 * List of key IDs or key tags to check. First available match wins.
76 * @throws \Civi\Crypto\Exception\CryptoException
78 public function encrypt($plainText, $keyIdOrTag) {
79 /** @var CryptoRegistry $registry */
80 $registry = \Civi
::service('crypto.registry');
82 $key = $registry->findKey($keyIdOrTag);
83 if ($key['suite'] === 'plain') {
84 if (!$this->isPlainText($plainText)) {
85 throw new CryptoException("Cannot use plaintext encoding for data with reserved delimiter.");
90 /** @var \Civi\Crypto\CipherSuiteInterface $cipherSuite */
91 $cipherSuite = $registry->findSuite($key['suite']);
92 $cipherText = $cipherSuite->encrypt($plainText, $key);
93 return $this->delim
. self
::VERSION_1
. $this->delim
. $key['id'] . $this->delim
. base64_encode($cipherText);
97 * Get the plaintext (given an encrypted token).
99 * @param string $token
100 * @param string|string[] $keyIdOrTag
101 * Whitelist of acceptable keys. Wildcard '*' will allow it to use
102 * any/all available means to decode the token.
104 * @throws \Civi\Crypto\Exception\CryptoException
106 public function decrypt($token, $keyIdOrTag = '*') {
107 $keyIdOrTag = (array) $keyIdOrTag;
109 if ($this->isPlainText($token)) {
110 if (in_array('*', $keyIdOrTag) ||
in_array('plain', $keyIdOrTag)) {
114 throw new CryptoException("Cannot decrypt token. Unexpected key: plain");
118 /** @var CryptoRegistry $registry */
119 $registry = \Civi
::service('crypto.registry');
121 $parts = explode($this->delim
, $token);
122 if ($parts[1] !== self
::VERSION_1
) {
123 throw new CryptoException("Unrecognized encoding");
126 $cipherText = base64_decode($parts[3]);
128 $key = $registry->findKey($keyId);
129 if (!in_array('*', $keyIdOrTag) && !in_array($keyId, $keyIdOrTag) && empty(array_intersect($keyIdOrTag, $key['tags']))) {
130 throw new CryptoException("Cannot decrypt token. Unexpected key: $keyId");
133 /** @var \Civi\Crypto\CipherSuiteInterface $cipherSuite */
134 $cipherSuite = $registry->findSuite($key['suite']);
135 $plainText = $cipherSuite->decrypt($cipherText, $key);