PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Encryption/Encrypter.php +102 -26 1.3.26 → 1.6.5 View file →
@@ -90,14 +90,24 @@
90 90 * Create a new encryption key for the given cipher.
91 91 *
92 92 * @param string $cipher
93 93 * @return string
94 + *
95 + * @throws \RuntimeException If the cipher is not in the supported list.
94 96 */
95 97 public static function generateKey($cipher)
96 98 {
97 - return random_bytes(
98 - self::$supportedCiphers[strtolower($cipher)]['size'] ?? 32
99 - );
99 + $cipher = strtolower($cipher);
100 +
101 + if (!isset(self::$supportedCiphers[$cipher])) {
102 + $ciphers = implode(', ', array_keys(self::$supportedCiphers));
103 +
104 + throw new RuntimeException(
105 + "Unsupported cipher '{$cipher}'. Supported ciphers are: {$ciphers}."
106 + );
107 + }
108 +
109 + return random_bytes(self::$supportedCiphers[$cipher]['size']);
100 110 }
101 111
102 112 /**
103 113 * Encrypt the given value.
@@ -170,19 +180,19 @@
170 180 );
171 181
172 182 $foundValidMac = false;
173 183
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.
184 + // Try each key (current + rotated) in turn. For non-AEAD ciphers,
185 + // the MAC must be validated against THIS specific key — not a
186 + // previous iteration's. Once a key passes its MAC check, attempt
187 + // decryption with the same key.
177 188 foreach ($this->getAllKeys() as $key) {
178 - if (
179 - $this->shouldValidateMac() &&
180 - !($foundValidMac = $foundValidMac || $this->validMacForKey($payload, $key))
181 - ) {
189 + if ($this->shouldValidateMac() && !$this->validMacForKey($payload, $key)) {
182 190 continue;
183 191 }
184 192
193 + $foundValidMac = true;
194 +
185 195 $decrypted = \openssl_decrypt(
186 196 $payload['value'], strtolower($this->cipher), $key, 0, $iv, $tag ?? ''
187 197 );
188 198
@@ -340,10 +350,19 @@
340 350 * @return string
341 351 */
342 352 public function getSlug()
343 353 {
344 - $slug = App::config()->get('app.slug');
354 + // Defensive: framework not bootstrapped (bare PHP, plugin
355 + // activation pre-init, early CLI). Fall back to a generic slug
356 + // so the constructor can still build a working encrypter.
357 + $app = App::getInstance();
345 358
359 + if (!$app) {
360 + return 'wpfluent_enc_key';
361 + }
362 +
363 + $slug = $app->config->get('app.slug');
364 +
346 365 $default = $slug . '_enc_key';
347 366
348 367 /**
349 368 * Allow developer to override the encryption key option name.
@@ -349,9 +368,9 @@
349 368 * Allow developer to override the encryption key option name.
350 369 *
351 370 * @param string $default Default option key name.
352 371 */
353 - return App::applyFilters($slug . '.encryption.option_key', $default);
372 + return $app->applyFilters($slug . '.encryption.option_key', $default);
354 373 }
355 374
356 375 /**
357 376 * Get the encryption key that the encrypter is currently using.
@@ -383,27 +402,52 @@
383 402 public function getAllKeys()
384 403 {
385 404 $keys = [$this->key];
386 405
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 - );
406 + $oldKeysOption = $this->oldKeysOptionName();
392 407
393 408 $oldKeys = get_option($oldKeysOption, []);
394 409
395 410 foreach ($oldKeys as $encodedKey) {
396 411 $decoded = base64_decode($encodedKey, true);
397 - if ($decoded !== false) {
398 - $keys[] = $decoded;
412 +
413 + // Skip corrupted base64 and any key whose length doesn't match
414 + // the active cipher (otherwise it'd silently fail in the
415 + // decrypt loop, hiding the real cause).
416 + if ($decoded === false || !static::supported($decoded, $this->cipher)) {
417 + continue;
399 418 }
419 +
420 + $keys[] = $decoded;
400 421 }
401 -
422 +
402 423 return $keys;
403 424 }
404 425
405 426 /**
427 + * Resolve the option name that stores rotated old keys.
428 + * Honors the {slug}.encryption.old_keys_option filter when the
429 + * framework App is bootstrapped; otherwise uses the default.
430 + *
431 + * @return string
432 + */
433 + protected function oldKeysOptionName()
434 + {
435 + $default = $this->slug . '_old_enc_key';
436 +
437 + $app = App::getInstance();
438 +
439 + if (!$app) {
440 + return $default;
441 + }
442 +
443 + return $app->applyFilters(
444 + $this->slug . '.encryption.old_keys_option',
445 + $default
446 + );
447 + }
448 +
449 + /**
406 450 * Rotate the encryption key.
407 451 *
408 452 * This method archives the current key in the old keys list,
409 453 * generates a new encryption key, stores it in the database,
@@ -414,13 +458,9 @@
414 458 public function rotateKey()
415 459 {
416 460 $currentEncoded = base64_encode($this->key);
417 461
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 - );
462 + $oldKeysOption = $this->oldKeysOptionName();
423 463
424 464 $oldKeys = get_option($oldKeysOption, []);
425 465
426 466 // Add current key to old list if not already present
@@ -425,11 +465,21 @@
425 465
426 466 // Add current key to old list if not already present
427 467 if (!in_array($currentEncoded, $oldKeys, true)) {
428 468 $oldKeys[] = $currentEncoded;
429 - update_option($oldKeysOption, $oldKeys);
430 469 }
431 470
471 + // Cap retention so the array doesn't grow unbounded across many
472 + // rotations. Keeps the MOST RECENT $cap entries; trimmed-out keys
473 + // mean any data still encrypted under them becomes undecryptable —
474 + // that's the documented contract of any retention policy.
475 + $cap = $this->oldKeysMax();
476 + if (count($oldKeys) > $cap) {
477 + $oldKeys = array_values(array_slice($oldKeys, -$cap));
478 + }
479 +
480 + update_option($oldKeysOption, $oldKeys);
481 +
432 482 // Generate and store new key
433 483 $newKey = base64_encode(static::generateKey($this->cipher));
434 484 update_option($this->slug, $newKey);
435 485
@@ -434,6 +484,32 @@
434 484 update_option($this->slug, $newKey);
435 485
436 486 // Update instance property with decoded new key
437 487 $this->key = base64_decode($newKey);
488 + }
489 +
490 + /**
491 + * Maximum number of rotated keys to retain.
492 + *
493 + * Honors the {slug}.encryption.old_keys_max filter when the framework
494 + * App is bootstrapped. Default is 10, which covers ~10 weeks of weekly
495 + * rotation or ~10 years of annual rotation; bump the filter to keep
496 + * more historical keys around.
497 + *
498 + * @return int
499 + */
500 + protected function oldKeysMax()
501 + {
502 + $default = 10;
503 +
504 + $app = App::getInstance();
505 +
506 + if (!$app) {
507 + return $default;
508 + }
509 +
510 + return (int) $app->applyFilters(
511 + $this->slug . '.encryption.old_keys_max',
512 + $default
513 + );
438 514 }
439 515 }