Commit | Line | Data |
---|---|---|
3fef2e21 TO |
1 | <?php |
2 | ||
3 | /* | |
4 | +--------------------------------------------------------------------+ | |
5 | | Copyright CiviCRM LLC. All rights reserved. | | |
6 | | | | |
7 | | This work is published under the GNU AGPLv3 license with some | | |
8 | | permitted exceptions and without any warranty. For full license | | |
9 | | and copyright information, see https://civicrm.org/licensing | | |
10 | +--------------------------------------------------------------------+ | |
11 | */ | |
12 | ||
13 | namespace Civi\Crypto; | |
14 | ||
15 | use Civi\Crypto\Exception\CryptoException; | |
16 | ||
17 | /** | |
18 | * This is an implementation of CipherSuiteInterface based on phpseclib 1.x/2.x. | |
19 | * | |
20 | * It supports multiple ciphers: | |
21 | * | |
22 | * - aes-cbc: AES-256 w/CBC, no authentication | |
23 | * - aes-ctr: AES-256 w/CTR, no authentication | |
24 | * - aes-cbc-hs: AES-256 w/CBC, HMAC-SHA256 authentication. Enc+auth use derived keys. | |
25 | * - aes-ctr-hs: AES-256 w/CTR, HMAC-SHA256 authentication. Enc+auth use derived keys. | |
26 | * | |
27 | * @package CRM | |
28 | * @copyright CiviCRM LLC https://civicrm.org/licensing | |
29 | */ | |
30 | class PhpseclibCipherSuite implements CipherSuiteInterface { | |
31 | ||
32 | /** | |
33 | * List of phpseclib "Cipher" objects. These are template objects | |
34 | * which may be cloned for use with specific keys. | |
35 | * | |
36 | * @var array|null | |
37 | */ | |
38 | protected $ciphers = NULL; | |
39 | ||
40 | public function __construct() { | |
41 | $this->ciphers = []; | |
42 | if (class_exists('\phpseclib\Crypt\AES')) { | |
43 | // phpseclib v2 | |
44 | $this->ciphers['aes-cbc'] = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_CBC); | |
45 | $this->ciphers['aes-cbc']->setKeyLength(256); | |
46 | $this->ciphers['aes-ctr'] = new \phpseclib\Crypt\AES(\phpseclib\Crypt\AES::MODE_CTR); | |
47 | $this->ciphers['aes-ctr']->setKeyLength(256); | |
48 | } | |
49 | elseif (class_exists('Crypt_AES')) { | |
50 | // phpseclib v1 | |
d6b736d4 | 51 | $this->ciphers['aes-cbc'] = new \Crypt_AES(CRYPT_MODE_CBC); |
3fef2e21 | 52 | $this->ciphers['aes-cbc']->setKeyLength(256); |
d6b736d4 | 53 | $this->ciphers['aes-ctr'] = new \Crypt_AES(CRYPT_MODE_CBC); |
3fef2e21 TO |
54 | $this->ciphers['aes-ctr']->setKeyLength(256); |
55 | } | |
56 | else { | |
57 | throw new CryptoException("Failed to find phpseclib"); | |
58 | } | |
59 | } | |
60 | ||
61 | /** | |
62 | * @inheritdoc | |
63 | */ | |
64 | public function getSuites(): array { | |
65 | return ['aes-cbc', 'aes-ctr', 'aes-cbc-hs', 'aes-ctr-hs']; | |
66 | } | |
67 | ||
68 | /** | |
69 | * @inheritdoc | |
70 | */ | |
71 | public function encrypt(string $plainText, array $key): string { | |
72 | switch ($key['suite']) { | |
73 | case 'aes-cbc-hs': | |
74 | case 'aes-ctr-hs': | |
75 | return $this->encryptThenSign($plainText, substr($key['suite'], 0, -3), 'sha256', $key['key']); | |
76 | ||
77 | case 'aes-cbc': | |
78 | case 'aes-ctr': | |
79 | return $this->encryptOnly($plainText, $key['suite'], $key['key']); | |
80 | } | |
81 | } | |
82 | ||
83 | /** | |
84 | * @inheritdoc | |
85 | */ | |
86 | public function decrypt(string $cipherText, array $key): string { | |
87 | switch ($key['suite']) { | |
88 | case 'aes-cbc-hs': | |
89 | case 'aes-ctr-hs': | |
90 | return $this->authenticateThenDecrypt($cipherText, substr($key['suite'], 0, -3), 'sha256', $key['key']); | |
91 | ||
92 | case 'aes-cbc': | |
93 | case 'aes-ctr': | |
94 | return $this->decryptOnly($cipherText, $key['suite'], $key['key']); | |
95 | } | |
96 | } | |
97 | ||
98 | /** | |
99 | * Given an master key, derive a pair of encryption+authentication keys. | |
100 | * | |
101 | * @param string $masterKey | |
102 | * @return array | |
103 | */ | |
104 | protected function createEncAuthKeys($masterKey) { | |
105 | return [ | |
106 | hash_hmac('sha256', 'enc', $masterKey, TRUE), | |
107 | hash_hmac('sha256', 'auth', $masterKey, TRUE), | |
108 | ]; | |
109 | } | |
110 | ||
111 | protected function encryptOnly($plainText, $suite, $key) { | |
112 | $cipher = $this->createCipher($suite, $key); | |
113 | $blockBytes = $cipher->getBlockLength() >> 3; | |
114 | $iv = random_bytes($blockBytes); | |
115 | $cipher->setIV($iv); | |
116 | return $iv . $cipher->encrypt($plainText); | |
117 | } | |
118 | ||
119 | protected function decryptOnly(string $cipherText, $suite, $key) { | |
120 | $cipher = $this->createCipher($suite, $key); | |
121 | $blockBytes = $cipher->getBlockLength() >> 3; | |
122 | $iv = substr($cipherText, 0, $blockBytes); | |
123 | $cipher->setIV($iv); | |
124 | return $cipher->decrypt(substr($cipherText, $blockBytes)); | |
125 | } | |
126 | ||
127 | /** | |
128 | * @param string $plainText | |
129 | * @param string $suite | |
130 | * The encryption algorithms | |
131 | * Ex: aes-cbc, aes-ctr | |
132 | * @param string $digest | |
133 | * The authentication algorithm | |
134 | * Ex: sha256 | |
135 | * @param string $masterKey | |
136 | * Binary representation of the key | |
137 | * | |
138 | * @return string | |
139 | * The concatenation of IV, ciphertext, signature | |
140 | */ | |
141 | protected function encryptThenSign($plainText, $suite, $digest, $masterKey) { | |
142 | list ($encKey, $authKey) = $this->createEncAuthKeys($masterKey); | |
143 | $cipher = $this->createCipher($suite, $encKey); | |
144 | $blockBytes = $cipher->getBlockLength() >> 3; | |
145 | $iv = random_bytes($blockBytes); | |
146 | $cipher->setIV($iv); | |
147 | $ivText = $iv . $cipher->encrypt($plainText); | |
148 | $sig = hash_hmac($digest, $ivText, $authKey, TRUE); | |
149 | $this->assertLen($this->getDigestBytes($digest), $sig); | |
150 | return $ivText . $sig; | |
151 | } | |
152 | ||
153 | /** | |
154 | * @param string $cipherText | |
155 | * Combined ciphertext (IV + encrypted text + signature) | |
156 | * @param string $suite | |
157 | * The encryption algorithms | |
158 | * Ex: aes-cbc, aes-ctr | |
159 | * @param string $digest | |
160 | * The authentication algorithm | |
161 | * Ex: sha256 | |
162 | * @param string $masterKey | |
163 | * Binary representation of the key | |
164 | * | |
165 | * @return string | |
166 | * Decrypted text | |
167 | * @throws CryptoException | |
168 | * Throws an exception if authentication fails. | |
169 | */ | |
170 | protected function authenticateThenDecrypt($cipherText, $suite, $digest, $masterKey) { | |
171 | list ($encKey, $authKey) = $this->createEncAuthKeys($masterKey); | |
172 | $cipher = $this->createCipher($suite, $encKey); | |
173 | $blockBytes = $cipher->getBlockLength() >> 3; | |
174 | $digestBytes = $this->getDigestBytes($digest); | |
175 | $sigExpect = substr($cipherText, -1 * $digestBytes); | |
176 | $sigActual = hash_hmac($digest, substr($cipherText, 0, -1 * $digestBytes), $authKey, TRUE); | |
177 | if (!hash_equals($sigActual, $sigExpect)) { | |
178 | throw new CryptoException("Failed to decrypt token. Invalid digest."); | |
179 | } | |
180 | $cipher->setIV(substr($cipherText, 0, $blockBytes)); | |
181 | return $cipher->decrypt(substr($cipherText, $blockBytes, -1 * $digestBytes)); | |
182 | } | |
183 | ||
184 | /** | |
185 | * @param $suite | |
186 | * @param $key | |
187 | * @return \phpseclib\Crypt\Base|\Crypt_Base | |
188 | */ | |
189 | protected function createCipher($suite, $key) { | |
190 | if (!isset($this->ciphers[$suite])) { | |
191 | throw new \RuntimeException("Cipher suite does not support " . $suite); | |
192 | } | |
193 | ||
194 | $cipher = clone $this->ciphers[$suite]; | |
195 | $this->assertLen($cipher->getKeyLength() >> 3, $key); | |
196 | $cipher->setKey($key); | |
197 | return $cipher; | |
198 | } | |
199 | ||
200 | protected function getDigestBytes($digest) { | |
201 | if ($digest === 'sha256') { | |
202 | return 32; | |
203 | } | |
204 | throw new \RuntimeException('Unrecognized digest'); | |
205 | } | |
206 | ||
207 | private function assertLen($bytes, $value) { | |
208 | if ($bytes != strlen($value)) { | |
209 | throw new \InvalidArgumentException("Malformed AES key"); | |
210 | } | |
211 | } | |
212 | ||
213 | } |