Merge pull request #20139 from eileenmcnaughton/home_url
[civicrm-core.git] / Civi / Crypto / PhpseclibCipherSuite.php
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
51 $this->ciphers['aes-cbc'] = new \Crypt_AES(CRYPT_MODE_CBC);
52 $this->ciphers['aes-cbc']->setKeyLength(256);
53 $this->ciphers['aes-ctr'] = new \Crypt_AES(CRYPT_MODE_CBC);
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 }