PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.2
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.2
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / vendor / wpfluent / framework / src / WPFluent / Encryption / Encrypter.php

Encrypter.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.2, at vendor/wpfluent/framework/src/WPFluent/Encryption/Encrypter.php

440 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\Framework\Encryption;
4
5 use RuntimeException;
6 use FluentForm\Framework\Foundation\App;
7 use FluentForm\Framework\Encryption\EncryptException;
8 use FluentForm\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 plugin slug key.
28 *
29 * @var string
30 */
31 protected $slug;
32
33 /**
34 * The supported cipher algorithms and their properties.
35 *
36 * @var array
37 */
38 private static $supportedCiphers = [
39 'aes-128-cbc' => ['size' => 16, 'aead' => false],
40 'aes-256-cbc' => ['size' => 32, 'aead' => false],
41 'aes-128-gcm' => ['size' => 16, 'aead' => true],
42 'aes-256-gcm' => ['size' => 32, 'aead' => true],
43 ];
44
45 /**
46 * Create a new encrypter instance.
47 *
48 * @param string $key
49 * @param string $cipher
50 * @return void
51 *
52 * @throws \RuntimeException
53 */
54 public function __construct($key = null, $cipher = 'aes-128-cbc')
55 {
56 $this->cipher = $cipher;
57
58 $this->slug = $this->getSlug();
59
60 $key = $key ?: $this->getKey();
61
62 if (!static::supported($key, $this->cipher)) {
63 $ciphers = implode(', ', array_keys(self::$supportedCiphers));
64
65 throw new RuntimeException("Unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}.");
66 }
67
68 $this->key = $key;
69 }
70
71 /**
72 * Determine if the given key and cipher combination is valid.
73 *
74 * @param string $key
75 * @param string $cipher
76 * @return bool
77 */
78 public static function supported($key, $cipher)
79 {
80 if (!isset(self::$supportedCiphers[strtolower($cipher)])) {
81 return false;
82 }
83
84 return mb_strlen(
85 $key, '8bit'
86 ) === self::$supportedCiphers[strtolower($cipher)]['size'];
87 }
88
89 /**
90 * Create a new encryption key for the given cipher.
91 *
92 * @param string $cipher
93 * @return string
94 */
95 public static function generateKey($cipher)
96 {
97 return random_bytes(
98 self::$supportedCiphers[strtolower($cipher)]['size'] ?? 32
99 );
100 }
101
102 /**
103 * Encrypt the given value.
104 *
105 * @param mixed $value
106 * @param bool $serialize
107 * @return string
108 *
109 * @throws \FluentForm\Framework\Encryption\EncryptException
110 */
111 public function encrypt($value, $serialize = true)
112 {
113 $iv = random_bytes(openssl_cipher_iv_length(strtolower($this->cipher)));
114
115 $value = \openssl_encrypt(
116 $serialize ? serialize($value) : $value,
117 strtolower($this->cipher), $this->key, 0, $iv, $tag
118 );
119
120 if ($value === false) {
121 throw new EncryptException('Could not encrypt the data.');
122 }
123
124 $iv = base64_encode($iv);
125 $tag = base64_encode($tag ?? '');
126
127 $mac = self::$supportedCiphers[strtolower($this->cipher)]['aead']
128 ? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt...
129 : $this->hash($iv, $value, $this->key);
130
131 $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);
132
133 if (json_last_error() !== JSON_ERROR_NONE) {
134 throw new EncryptException('Could not encrypt the data.');
135 }
136
137 return base64_encode($json);
138 }
139
140 /**
141 * Encrypt a string without serialization.
142 *
143 * @param string $value
144 * @return string
145 *
146 * @throws \FluentForm\Framework\Encryption\EncryptException
147 */
148 public function encryptString($value)
149 {
150 return $this->encrypt($value, false);
151 }
152
153 /**
154 * Decrypt the given value.
155 *
156 * @param string $payload
157 * @param bool $unserialize
158 * @return mixed
159 *
160 * @throws \FluentForm\Framework\Encryption\DecryptException
161 */
162 public function decrypt($payload, $unserialize = true)
163 {
164 $payload = $this->getJsonPayload($payload);
165
166 $iv = base64_decode($payload['iv']);
167
168 $this->ensureTagIsValid(
169 $tag = empty($payload['tag']) ? null : base64_decode($payload['tag'])
170 );
171
172 $foundValidMac = false;
173
174 // Here we will decrypt the value. If we are able to successfully decrypt it
175 // we will then unserialize it and return it out to the caller. If we are
176 // unable to decrypt this value we will throw out an exception message.
177 foreach ($this->getAllKeys() as $key) {
178 if (
179 $this->shouldValidateMac() &&
180 !($foundValidMac = $foundValidMac || $this->validMacForKey($payload, $key))
181 ) {
182 continue;
183 }
184
185 $decrypted = \openssl_decrypt(
186 $payload['value'], strtolower($this->cipher), $key, 0, $iv, $tag ?? ''
187 );
188
189 if ($decrypted !== false) {
190 break;
191 }
192 }
193
194 if ($this->shouldValidateMac() && !$foundValidMac) {
195 throw new DecryptException('The MAC is invalid.');
196 }
197
198 if (($decrypted ?? false) === false) {
199 throw new DecryptException('Could not decrypt the data.');
200 }
201
202 return $unserialize ? unserialize($decrypted) : $decrypted;
203 }
204
205 /**
206 * Decrypt the given string without unserialization.
207 *
208 * @param string $payload
209 * @return string
210 *
211 * @throws \FluentForm\Framework\Encryption\DecryptException
212 */
213 public function decryptString($payload)
214 {
215 return $this->decrypt($payload, false);
216 }
217
218 /**
219 * Create a MAC for the given value.
220 *
221 * @param string $iv
222 * @param mixed $value
223 * @param string $key
224 * @return string
225 */
226 protected function hash($iv, $value, $key)
227 {
228 return hash_hmac('sha256', $iv.$value, $key);
229 }
230
231 /**
232 * Get the JSON array from the given payload.
233 *
234 * @param string $payload
235 * @return array
236 *
237 * @throws \FluentForm\Framework\Encryption\DecryptException
238 */
239 protected function getJsonPayload($payload)
240 {
241 if (!is_string($payload)) {
242 throw new DecryptException('The payload is invalid.');
243 }
244
245 $payload = json_decode(base64_decode($payload), true);
246
247 // If the payload is not valid JSON or does not have the proper keys set we will
248 // assume it is invalid and bail out of the routine since we will not be able
249 // to decrypt the given value. We'll also check the MAC for this encryption.
250 if (!$this->validPayload($payload)) {
251 throw new DecryptException('The payload is invalid.');
252 }
253
254 return $payload;
255 }
256
257 /**
258 * Verify that the encryption payload is valid.
259 *
260 * @param mixed $payload
261 * @return bool
262 */
263 protected function validPayload($payload)
264 {
265 if (!is_array($payload)) {
266 return false;
267 }
268
269 foreach (['iv', 'value', 'mac'] as $item) {
270 if (!isset($payload[$item]) || !is_string($payload[$item])) {
271 return false;
272 }
273 }
274
275 if (isset($payload['tag']) && !is_string($payload['tag'])) {
276 return false;
277 }
278
279 return strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length(strtolower($this->cipher));
280 }
281
282 /**
283 * Determine if the MAC for the given payload is valid for the primary key.
284 *
285 * @param array $payload
286 * @return bool
287 */
288 protected function validMac(array $payload)
289 {
290 return $this->validMacForKey($payload, $this->key);
291 }
292
293 /**
294 * Determine if the MAC is valid for the given payload and key.
295 *
296 * @param array $payload
297 * @param string $key
298 * @return bool
299 */
300 protected function validMacForKey($payload, $key)
301 {
302 return hash_equals(
303 $this->hash($payload['iv'], $payload['value'], $key), $payload['mac']
304 );
305 }
306
307 /**
308 * Ensure the given tag is a valid tag given the selected cipher.
309 *
310 * @param string $tag
311 * @return void
312 */
313 protected function ensureTagIsValid($tag)
314 {
315 if (self::$supportedCiphers[strtolower($this->cipher)]['aead'] && strlen($tag) !== 16) {
316 throw new DecryptException('Could not decrypt the data.');
317 }
318
319 if (!self::$supportedCiphers[strtolower($this->cipher)]['aead'] && is_string($tag)) {
320 throw new DecryptException('Unable to use tag because the cipher algorithm does not support AEAD.');
321 }
322 }
323
324 /**
325 * Determine if we should validate the MAC while decrypting.
326 *
327 * @return bool
328 */
329 protected function shouldValidateMac()
330 {
331 return !self::$supportedCiphers[strtolower($this->cipher)]['aead'];
332 }
333
334 /**
335 * Get the application encryption key.
336 *
337 * Developers may override the database key name using the filter:
338 * `{slug}.encryption.option_key`
339 *
340 * @return string
341 */
342 public function getSlug()
343 {
344 $slug = App::config()->get('app.slug');
345
346 $default = $slug . '_enc_key';
347
348 /**
349 * Allow developer to override the encryption key option name.
350 *
351 * @param string $default Default option key name.
352 */
353 return App::applyFilters($slug . '.encryption.option_key', $default);
354 }
355
356 /**
357 * Get the encryption key that the encrypter is currently using.
358 *
359 * @return string
360 */
361 public function getKey()
362 {
363 if (!$key = get_option($this->slug)) {
364
365 add_option($this->slug, base64_encode(
366 $this->generateKey($this->cipher)
367 ));
368
369 $key = get_option($this->slug);
370 }
371
372 return base64_decode($key);
373 }
374
375 /**
376 * Get the current encryption key and all previous encryption keys.
377 *
378 * This is useful for key rotation, allowing decryption of data
379 * encrypted with older keys.
380 *
381 * @return array Array of binary encryption keys.
382 */
383 public function getAllKeys()
384 {
385 $keys = [$this->key];
386
387 // Get the old keys option name, allowing override via filter
388 $oldKeysOption = App::applyFilters(
389 $this->slug . '.encryption.old_keys_option',
390 $this->slug . '_old_enc_key'
391 );
392
393 $oldKeys = get_option($oldKeysOption, []);
394
395 foreach ($oldKeys as $encodedKey) {
396 $decoded = base64_decode($encodedKey, true);
397 if ($decoded !== false) {
398 $keys[] = $decoded;
399 }
400 }
401
402 return $keys;
403 }
404
405 /**
406 * Rotate the encryption key.
407 *
408 * This method archives the current key in the old keys list,
409 * generates a new encryption key, stores it in the database,
410 * and updates the encrypter instance with the new key.
411 *
412 * @return void
413 */
414 public function rotateKey()
415 {
416 $currentEncoded = base64_encode($this->key);
417
418 // Get the old keys option name, allowing override via filter
419 $oldKeysOption = App::applyFilters(
420 $this->slug . '.encryption.old_keys_option',
421 $this->slug . '_old_enc_key'
422 );
423
424 $oldKeys = get_option($oldKeysOption, []);
425
426 // Add current key to old list if not already present
427 if (!in_array($currentEncoded, $oldKeys, true)) {
428 $oldKeys[] = $currentEncoded;
429 update_option($oldKeysOption, $oldKeys);
430 }
431
432 // Generate and store new key
433 $newKey = base64_encode(static::generateKey($this->cipher));
434 update_option($this->slug, $newKey);
435
436 // Update instance property with decoded new key
437 $this->key = base64_decode($newKey);
438 }
439 }
440