PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.8
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.8
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / myyoast-client / infrastructure / crypto / encryption.php

encryption.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.8, at src/myyoast-client/infrastructure/crypto/encryption.php

123 lines 4.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3
4 namespace Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto;
5
6 use Exception;
7 use SensitiveParameter;
8 use SodiumException;
9
10 /**
11 * Provides symmetric encryption and decryption using libsodium.
12 *
13 * Derives per-purpose 256-bit encryption keys from the WordPress AUTH_KEY
14 * constant via HKDF-SHA256. The encryption key is never stored — it is
15 * derived on every call.
16 */
17 class Encryption {
18
19 /**
20 * Encrypts a plaintext string using sodium_crypto_secretbox (XSalsa20-Poly1305).
21 *
22 * @param string $plaintext The data to encrypt.
23 * @param string $context A unique context string for key derivation (e.g. "yoast-myyoast-dpop-key").
24 *
25 * @return string Base64-encoded nonce + ciphertext.
26 *
27 * @throws Encryption_Exception If encryption fails.
28 */
29 public function encrypt(
30 // phpcs:ignore PHPCompatibility.Attributes.NewAttributes.PHPNativeAttributeFound -- No-op on PHP < 8.2; redacts parameter from stack traces on PHP 8.2+.
31 #[SensitiveParameter]
32 string $plaintext,
33 string $context
34 ): string {
35 $key = $this->derive_key( $context );
36
37 try {
38 $nonce = \random_bytes( \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
39 $ciphertext = \sodium_crypto_secretbox( $plaintext, $nonce, $key );
40
41 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Binary ciphertext needs safe encoding.
42 return \base64_encode( $nonce . $ciphertext );
43 } catch ( Exception $e ) {
44 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
45 throw new Encryption_Exception( 'Encryption failed: ' . $e->getMessage(), 0, $e );
46 }
47 finally {
48 // Securely wipe the derived key from memory to prevent leakage via memory dumps or core files.
49 try {
50 \sodium_memzero( $key );
51 } catch ( SodiumException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Best-effort cleanup.
52 // Best-effort: key will be freed when $key goes out of scope.
53 }
54 }
55 }
56
57 /**
58 * Decrypts a previously encrypted string.
59 *
60 * @param string $encrypted Base64-encoded nonce + ciphertext from encrypt().
61 * @param string $context The same context string used during encryption.
62 *
63 * @return string The decrypted plaintext.
64 *
65 * @throws Encryption_Exception If decryption fails (corrupt data, wrong key, tampered ciphertext).
66 */
67 public function decrypt( string $encrypted, string $context ): string {
68 $key = $this->derive_key( $context );
69
70 try {
71 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding our own base64-encoded ciphertext.
72 $decoded = \base64_decode( $encrypted, true );
73 if ( $decoded === false ) {
74 throw new Encryption_Exception( 'Decryption failed: invalid base64 encoding.' );
75 }
76
77 if ( \strlen( $decoded ) < ( \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + \SODIUM_CRYPTO_SECRETBOX_MACBYTES ) ) {
78 throw new Encryption_Exception( 'Decryption failed: ciphertext too short.' );
79 }
80
81 $nonce = \substr( $decoded, 0, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
82 $ciphertext = \substr( $decoded, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
83 $plaintext = \sodium_crypto_secretbox_open( $ciphertext, $nonce, $key );
84
85 if ( $plaintext === false ) {
86 throw new Encryption_Exception( 'Decryption failed: authentication tag verification failed.' );
87 }
88
89 return $plaintext;
90 }
91 finally {
92 // Securely wipe the derived key from memory to prevent leakage via memory dumps or core files.
93 try {
94 \sodium_memzero( $key );
95 }
96 catch ( SodiumException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Best-effort cleanup.
97 // Best-effort: key will be freed when $key goes out of scope.
98 }
99 }
100 }
101
102 /**
103 * Derives a 256-bit encryption key from AUTH_KEY using HKDF-SHA256.
104 *
105 * @param string $context A unique context string for key derivation.
106 *
107 * @return string A 32-byte derived key.
108 *
109 * @throws Encryption_Exception If AUTH_KEY is not defined or sodium is unavailable.
110 */
111 private function derive_key( string $context ): string {
112 if ( ! \function_exists( 'sodium_crypto_secretbox' ) ) {
113 throw new Encryption_Exception( 'The sodium PHP extension is required but not available.' );
114 }
115
116 if ( ! \defined( 'AUTH_KEY' ) || \AUTH_KEY === '' || \AUTH_KEY === 'put your unique phrase here' ) {
117 throw new Encryption_Exception( 'AUTH_KEY is not configured. Please set a unique AUTH_KEY in wp-config.php.' );
118 }
119
120 return \hash_hkdf( 'sha256', \AUTH_KEY, \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, $context );
121 }
122 }
123