• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 8dbb17dd027bdca8154ad419c37e397e58556d21

12 May 2026 10:00AM UTC coverage: 51.862%. First build
8dbb17dd027bdca8154ad419c37e397e58556d21

push

github

vraja-pro
Merge branch 'trunk' into feature/editor-focus-keyphrase-redesign

1899 of 3333 branches covered (56.98%)

Branch coverage included in aggregate %.

1142 of 2108 new or added lines in 60 files covered. (54.17%)

6040 of 11975 relevant lines covered (50.44%)

251201.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.67
/src/myyoast-client/infrastructure/crypto/encryption.php
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(
10✔
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 );
10✔
36

37
                try {
38
                        $nonce      = \random_bytes( \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
10✔
39
                        $ciphertext = \sodium_crypto_secretbox( $plaintext, $nonce, $key );
10✔
40

41
                        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Binary ciphertext needs safe encoding.
42
                        return \base64_encode( $nonce . $ciphertext );
10✔
NEW
43
                } catch ( Exception $e ) {
×
44
                        // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
NEW
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 );
10✔
51
                        } catch ( SodiumException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Best-effort cleanup.
10✔
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 {
10✔
68
                $key = $this->derive_key( $context );
10✔
69

70
                try {
71
                        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding our own base64-encoded ciphertext.
72
                        $decoded = \base64_decode( $encrypted, true );
10✔
73
                        if ( $decoded === false ) {
10✔
74
                                throw new Encryption_Exception( 'Decryption failed: invalid base64 encoding.' );
2✔
75
                        }
76

77
                        if ( \strlen( $decoded ) < ( \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + \SODIUM_CRYPTO_SECRETBOX_MACBYTES ) ) {
8✔
78
                                throw new Encryption_Exception( 'Decryption failed: ciphertext too short.' );
2✔
79
                        }
80

81
                        $nonce      = \substr( $decoded, 0, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
6✔
82
                        $ciphertext = \substr( $decoded, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
6✔
83
                        $plaintext  = \sodium_crypto_secretbox_open( $ciphertext, $nonce, $key );
6✔
84

85
                        if ( $plaintext === false ) {
6✔
86
                                throw new Encryption_Exception( 'Decryption failed: authentication tag verification failed.' );
2✔
87
                        }
88

89
                        return $plaintext;
4✔
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 );
10✔
95
                        }
96
                        catch ( SodiumException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Best-effort cleanup.
10✔
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 {
6✔
112
                if ( ! \function_exists( 'sodium_crypto_secretbox' ) ) {
6✔
NEW
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' ) {
6✔
NEW
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 );
6✔
121
        }
122
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc