← 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 | } |