Commit | Line | Data |
---|---|---|
8d3452c4 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
4 | | Copyright CiviCRM LLC. All rights reserved. | | |
5 | | | | |
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 | +--------------------------------------------------------------------+ | |
10 | */ | |
11 | ||
12 | namespace Civi\Crypto; | |
13 | ||
14 | use Civi\Crypto\Exception\CryptoException; | |
15 | ||
16 | /** | |
17 | * The "Crypto Token" service supports a token format suitable for | |
18 | * storing specific values in the database with encryption. Characteristics: | |
19 | * | |
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. | |
27 | * | |
28 | * USAGE: The "encrypt()" and "decrypt()" methods are the primary interfaces. | |
29 | * | |
30 | * $encrypted = Civi::service('crypto.token')->encrypt('my-mail-password, 'KEY_ID_OR_TAG'); | |
31 | * $decrypted = Civi::service('crypto.token')->decrypt($encrypted, '*'); | |
32 | * | |
33 | * FORMAT: An encoded token may be in either of these formats: | |
34 | * | |
35 | * - Plain text: Any string which does not begin with chr(2) | |
36 | * - Encrypted text: A string in the format: | |
5b394c8f TO |
37 | * TOKEN := DLM + FMT + QUERY |
38 | * DLM := ASCII char #2 | |
39 | * FMT := String, 4-digit, alphanumeric (as in "CTK?") | |
40 | * QUERY := String, URL-encoded key-value pairs, | |
41 | * "k", the key ID (alphanumeric and symbols "_-.,:;=+/\") | |
42 | * "t", the text (base64-encoded ciphertext) | |
8d3452c4 TO |
43 | * |
44 | * @package Civi\Crypto | |
45 | */ | |
46 | class CryptoToken { | |
47 | ||
5b394c8f TO |
48 | /** |
49 | * Format identification code | |
50 | */ | |
51 | const FMT_QUERY = 'CTK?'; | |
8d3452c4 | 52 | |
5b394c8f TO |
53 | /** |
54 | * @var string | |
55 | */ | |
8d3452c4 TO |
56 | protected $delim; |
57 | ||
58 | /** | |
59 | * CryptoToken constructor. | |
8d3452c4 | 60 | */ |
7c5110c3 | 61 | public function __construct() { |
8d3452c4 | 62 | $this->delim = chr(2); |
8d3452c4 TO |
63 | } |
64 | ||
65 | /** | |
66 | * Determine if a string looks like plain-text. | |
67 | * | |
68 | * @param string $plainText | |
69 | * @return bool | |
70 | */ | |
71 | public function isPlainText($plainText) { | |
72 | return is_string($plainText) && ($plainText === '' || $plainText{0} !== $this->delim); | |
73 | } | |
74 | ||
75 | /** | |
76 | * Create an encrypted token (given the plaintext). | |
77 | * | |
78 | * @param string $plainText | |
79 | * The secret value to encode (e.g. plain-text password). | |
80 | * @param string|string[] $keyIdOrTag | |
81 | * List of key IDs or key tags to check. First available match wins. | |
82 | * @return string | |
83 | * A token | |
84 | * @throws \Civi\Crypto\Exception\CryptoException | |
85 | */ | |
86 | public function encrypt($plainText, $keyIdOrTag) { | |
7c5110c3 TO |
87 | /** @var CryptoRegistry $registry */ |
88 | $registry = \Civi::service('crypto.registry'); | |
89 | ||
90 | $key = $registry->findKey($keyIdOrTag); | |
8d3452c4 TO |
91 | if ($key['suite'] === 'plain') { |
92 | if (!$this->isPlainText($plainText)) { | |
93 | throw new CryptoException("Cannot use plaintext encoding for data with reserved delimiter."); | |
94 | } | |
95 | return $plainText; | |
96 | } | |
97 | ||
98 | /** @var \Civi\Crypto\CipherSuiteInterface $cipherSuite */ | |
7c5110c3 | 99 | $cipherSuite = $registry->findSuite($key['suite']); |
8d3452c4 | 100 | $cipherText = $cipherSuite->encrypt($plainText, $key); |
5b394c8f TO |
101 | |
102 | return $this->delim . self::FMT_QUERY . \http_build_query([ | |
103 | 'k' => $key['id'], | |
104 | 't' => \CRM_Utils_String::base64UrlEncode($cipherText), | |
105 | ]); | |
8d3452c4 TO |
106 | } |
107 | ||
108 | /** | |
109 | * Get the plaintext (given an encrypted token). | |
110 | * | |
111 | * @param string $token | |
112 | * @param string|string[] $keyIdOrTag | |
113 | * Whitelist of acceptable keys. Wildcard '*' will allow it to use | |
114 | * any/all available means to decode the token. | |
115 | * @return string | |
116 | * @throws \Civi\Crypto\Exception\CryptoException | |
117 | */ | |
118 | public function decrypt($token, $keyIdOrTag = '*') { | |
119 | $keyIdOrTag = (array) $keyIdOrTag; | |
120 | ||
121 | if ($this->isPlainText($token)) { | |
122 | if (in_array('*', $keyIdOrTag) || in_array('plain', $keyIdOrTag)) { | |
123 | return $token; | |
124 | } | |
125 | else { | |
126 | throw new CryptoException("Cannot decrypt token. Unexpected key: plain"); | |
127 | } | |
128 | } | |
129 | ||
7c5110c3 TO |
130 | /** @var CryptoRegistry $registry */ |
131 | $registry = \Civi::service('crypto.registry'); | |
132 | ||
5b394c8f TO |
133 | $fmt = substr($token, 1, 4); |
134 | switch ($fmt) { | |
135 | case self::FMT_QUERY: | |
136 | parse_str(substr($token, 5), $tokenData); | |
137 | $keyId = $tokenData['k']; | |
138 | $cipherText = \CRM_Utils_String::base64UrlDecode($tokenData['t']); | |
139 | break; | |
140 | ||
141 | default: | |
142 | throw new CryptoException("Cannot decrypt token. Invalid format."); | |
8d3452c4 | 143 | } |
8d3452c4 | 144 | |
7c5110c3 | 145 | $key = $registry->findKey($keyId); |
8d3452c4 | 146 | if (!in_array('*', $keyIdOrTag) && !in_array($keyId, $keyIdOrTag) && empty(array_intersect($keyIdOrTag, $key['tags']))) { |
5b394c8f | 147 | throw new CryptoException("Cannot decrypt token. Unexpected key: {$keyId}"); |
8d3452c4 TO |
148 | } |
149 | ||
150 | /** @var \Civi\Crypto\CipherSuiteInterface $cipherSuite */ | |
7c5110c3 | 151 | $cipherSuite = $registry->findSuite($key['suite']); |
8d3452c4 TO |
152 | $plainText = $cipherSuite->decrypt($cipherText, $key); |
153 | return $plainText; | |
154 | } | |
155 | ||
156 | } |