PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.31
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.31
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Encryption / Encrypter.php

Encrypter.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.31, at vendor/wpfluent/framework/src/WPFluent/Encryption/Encrypter.php

366 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Encryption;
4
5 use RuntimeException;
6 use FluentBoards\Framework\Foundation\App;
7 use FluentBoards\Framework\Encryption\EncryptException;
8 use FluentBoards\Framework\Encryption\DecryptException;
9
10 class Encrypter
11 {
12 /**
13 * The encryption key.
14 *
15 * @var string
16 */
17 protected $key;
18
19 /**
20 * The algorithm used for encryption.
21 *
22 * @var string
23 */
24 protected $cipher;
25
26 /**
27 * The supported cipher algorithms and their properties.
28 *
29 * @var array
30 */
31 private static $supportedCiphers = [
32 'aes-128-cbc' => ['size' => 16, 'aead' => false],
33 'aes-256-cbc' => ['size' => 32, 'aead' => false],
34 'aes-128-gcm' => ['size' => 16, 'aead' => true],
35 'aes-256-gcm' => ['size' => 32, 'aead' => true],
36 ];
37
38 /**
39 * Create a new encrypter instance.
40 *
41 * @param string $key
42 * @param string $cipher
43 * @return void
44 *
45 * @throws \RuntimeException
46 */
47 public function __construct($key = null, $cipher = 'aes-128-cbc')
48 {
49 $this->cipher = $cipher;
50
51 $this->slug = $this->getSlug();
52
53 $key = $this->getKey();
54
55 if (!static::supported($key, $this->cipher)) {
56 $ciphers = implode(', ', array_keys(self::$supportedCiphers));
57
58 throw new RuntimeException("Unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}.");
59 }
60
61 $this->key = $key;
62 }
63
64 /**
65 * Determine if the given key and cipher combination is valid.
66 *
67 * @param string $key
68 * @param string $cipher
69 * @return bool
70 */
71 public static function supported($key, $cipher)
72 {
73 if (!isset(self::$supportedCiphers[strtolower($cipher)])) {
74 return false;
75 }
76
77 return mb_strlen(
78 $key, '8bit'
79 ) === self::$supportedCiphers[strtolower($cipher)]['size'];
80 }
81
82 /**
83 * Create a new encryption key for the given cipher.
84 *
85 * @param string $cipher
86 * @return string
87 */
88 public static function generateKey($cipher)
89 {
90 return random_bytes(
91 self::$supportedCiphers[strtolower($cipher)]['size'] ?? 32
92 );
93 }
94
95 /**
96 * Encrypt the given value.
97 *
98 * @param mixed $value
99 * @param bool $serialize
100 * @return string
101 *
102 * @throws \FluentBoards\Framework\Encryption\EncryptException
103 */
104 public function encrypt($value, $serialize = true)
105 {
106 $iv = random_bytes(openssl_cipher_iv_length(strtolower($this->cipher)));
107
108 $value = \openssl_encrypt(
109 $serialize ? serialize($value) : $value,
110 strtolower($this->cipher), $this->key, 0, $iv, $tag
111 );
112
113 if ($value === false) {
114 throw new EncryptException('Could not encrypt the data.');
115 }
116
117 $iv = base64_encode($iv);
118 $tag = base64_encode($tag ?? '');
119
120 $mac = self::$supportedCiphers[strtolower($this->cipher)]['aead']
121 ? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt...
122 : $this->hash($iv, $value, $this->key);
123
124 $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);
125
126 if (json_last_error() !== JSON_ERROR_NONE) {
127 throw new EncryptException('Could not encrypt the data.');
128 }
129
130 return base64_encode($json);
131 }
132
133 /**
134 * Encrypt a string without serialization.
135 *
136 * @param string $value
137 * @return string
138 *
139 * @throws \FluentBoards\Framework\Encryption\EncryptException
140 */
141 public function encryptString($value)
142 {
143 return $this->encrypt($value, false);
144 }
145
146 /**
147 * Decrypt the given value.
148 *
149 * @param string $payload
150 * @param bool $unserialize
151 * @return mixed
152 *
153 * @throws \FluentBoards\Framework\Encryption\DecryptException
154 */
155 public function decrypt($payload, $unserialize = true)
156 {
157 $payload = $this->getJsonPayload($payload);
158
159 $iv = base64_decode($payload['iv']);
160
161 $this->ensureTagIsValid(
162 $tag = empty($payload['tag']) ? null : base64_decode($payload['tag'])
163 );
164
165 $foundValidMac = false;
166
167 // Here we will decrypt the value. If we are able to successfully decrypt it
168 // we will then unserialize it and return it out to the caller. If we are
169 // unable to decrypt this value we will throw out an exception message.
170 foreach ($this->getAllKeys() as $key) {
171 if (
172 $this->shouldValidateMac() &&
173 !($foundValidMac = $foundValidMac || $this->validMacForKey($payload, $key))
174 ) {
175 continue;
176 }
177
178 $decrypted = \openssl_decrypt(
179 $payload['value'], strtolower($this->cipher), $key, 0, $iv, $tag ?? ''
180 );
181
182 if ($decrypted !== false) {
183 break;
184 }
185 }
186
187 if ($this->shouldValidateMac() && !$foundValidMac) {
188 throw new DecryptException('The MAC is invalid.');
189 }
190
191 if (($decrypted ?? false) === false) {
192 throw new DecryptException('Could not decrypt the data.');
193 }
194
195 return $unserialize ? unserialize($decrypted) : $decrypted;
196 }
197
198 /**
199 * Decrypt the given string without unserialization.
200 *
201 * @param string $payload
202 * @return string
203 *
204 * @throws \FluentBoards\Framework\Encryption\DecryptException
205 */
206 public function decryptString($payload)
207 {
208 return $this->decrypt($payload, false);
209 }
210
211 /**
212 * Create a MAC for the given value.
213 *
214 * @param string $iv
215 * @param mixed $value
216 * @param string $key
217 * @return string
218 */
219 protected function hash($iv, $value, $key)
220 {
221 return hash_hmac('sha256', $iv.$value, $key);
222 }
223
224 /**
225 * Get the JSON array from the given payload.
226 *
227 * @param string $payload
228 * @return array
229 *
230 * @throws \FluentBoards\Framework\Encryption\DecryptException
231 */
232 protected function getJsonPayload($payload)
233 {
234 if (!is_string($payload)) {
235 throw new DecryptException('The payload is invalid.');
236 }
237
238 $payload = json_decode(base64_decode($payload), true);
239
240 // If the payload is not valid JSON or does not have the proper keys set we will
241 // assume it is invalid and bail out of the routine since we will not be able
242 // to decrypt the given value. We'll also check the MAC for this encryption.
243 if (!$this->validPayload($payload)) {
244 throw new DecryptException('The payload is invalid.');
245 }
246
247 return $payload;
248 }
249
250 /**
251 * Verify that the encryption payload is valid.
252 *
253 * @param mixed $payload
254 * @return bool
255 */
256 protected function validPayload($payload)
257 {
258 if (!is_array($payload)) {
259 return false;
260 }
261
262 foreach (['iv', 'value', 'mac'] as $item) {
263 if (!isset($payload[$item]) || !is_string($payload[$item])) {
264 return false;
265 }
266 }
267
268 if (isset($payload['tag']) && !is_string($payload['tag'])) {
269 return false;
270 }
271
272 return strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length(strtolower($this->cipher));
273 }
274
275 /**
276 * Determine if the MAC for the given payload is valid for the primary key.
277 *
278 * @param array $payload
279 * @return bool
280 */
281 protected function validMac(array $payload)
282 {
283 return $this->validMacForKey($payload, $this->key);
284 }
285
286 /**
287 * Determine if the MAC is valid for the given payload and key.
288 *
289 * @param array $payload
290 * @param string $key
291 * @return bool
292 */
293 protected function validMacForKey($payload, $key)
294 {
295 return hash_equals(
296 $this->hash($payload['iv'], $payload['value'], $key), $payload['mac']
297 );
298 }
299
300 /**
301 * Ensure the given tag is a valid tag given the selected cipher.
302 *
303 * @param string $tag
304 * @return void
305 */
306 protected function ensureTagIsValid($tag)
307 {
308 if (self::$supportedCiphers[strtolower($this->cipher)]['aead'] && strlen($tag) !== 16) {
309 throw new DecryptException('Could not decrypt the data.');
310 }
311
312 if (!self::$supportedCiphers[strtolower($this->cipher)]['aead'] && is_string($tag)) {
313 throw new DecryptException('Unable to use tag because the cipher algorithm does not support AEAD.');
314 }
315 }
316
317 /**
318 * Determine if we should validate the MAC while decrypting.
319 *
320 * @return bool
321 */
322 protected function shouldValidateMac()
323 {
324 return !self::$supportedCiphers[strtolower($this->cipher)]['aead'];
325 }
326
327 /**
328 * Get the application encryption key.
329 *
330 * @return string
331 */
332 public function getSlug()
333 {
334 return App::config()->get('app.slug') . '_enc_key';
335 }
336
337 /**
338 * Get the encryption key that the encrypter is currently using.
339 *
340 * @return string
341 */
342 public function getKey()
343 {
344 if (!$key = get_option($this->slug)) {
345
346 add_option($this->slug, base64_encode(
347 $this->generateKey($this->cipher)
348 ));
349
350 $key = get_option($this->slug);
351 }
352
353 return base64_decode($key);
354 }
355
356 /**
357 * Get the current encryption key and all previous encryption keys.
358 *
359 * @return array
360 */
361 public function getAllKeys()
362 {
363 return [$this->key];
364 }
365 }
366