PluginProbe
Blocks – Reusable Content, Shortcodes & Site Variables / trunk
Blocks – Reusable Content, Shortcodes & Site Variables vtrunk
26.09.03.15 26.08.31.21 26.08.22.20 26.08.22.22 26.08.22.17 26.08.22.13 26.08.21.19 26.08.07.23 26.07.19.14 26.07.13.21 26.07.13.17 26.07.12.13 026.07.07.21 026.07.05.18 026.06.26.20 026.06.26.21 026.06.08.20 026.05.13.14 026.04.29.10 trunk 026.02.22.22 026.03.16.23 026.04.23.13
blocks / src / Security / SecretStore.php

SecretStore.php in Blocks – Reusable Content, Shortcodes & Site Variables trunk, at src/Security/SecretStore.php

147 lines 4.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Authenticated storage for plugin secrets.
5 *
6 * @package Blocks
7 * @license GPL-2.0-or-later
8 */
9
10 declare(strict_types=1);
11
12 namespace RenzoJohnson\Blocks\Security;
13
14 \defined( 'ABSPATH' ) || exit;
15
16 final class SecretStore {
17
18 private const PREFIX = 'blocks-secret:v1:';
19 private const CIPHER = 'aes-256-gcm';
20 private const IV_LENGTH = 12;
21 private const TAG_LENGTH = 16;
22
23 public function is_encrypted( string $value ): bool {
24 return \str_starts_with( $value, self::PREFIX );
25 }
26
27 public function encrypt( string $option_name, string $plaintext ): string|\WP_Error {
28 if ( '' === $plaintext ) {
29 return '';
30 }
31
32 if ( ! \function_exists( 'openssl_encrypt' ) ) {
33 return new \WP_Error( 'blocks_secret_crypto_unavailable', \__( 'Secure encryption is unavailable on this server.', 'blocks' ) );
34 }
35
36 try {
37 $iv = \random_bytes( self::IV_LENGTH );
38 } catch ( \Throwable ) {
39 return new \WP_Error( 'blocks_secret_random_failed', \__( 'Secure random generation failed.', 'blocks' ) );
40 }
41
42 $tag = '';
43 $ciphertext = \openssl_encrypt(
44 $plaintext,
45 self::CIPHER,
46 $this->key(),
47 \OPENSSL_RAW_DATA,
48 $iv,
49 $tag,
50 $option_name,
51 self::TAG_LENGTH
52 );
53
54 if ( false === $ciphertext || self::TAG_LENGTH !== \strlen( $tag ) ) {
55 return new \WP_Error( 'blocks_secret_encrypt_failed', \__( 'The secret could not be encrypted.', 'blocks' ) );
56 }
57
58 // Binary ciphertext must be encoded for safe option storage.
59 return self::PREFIX . \base64_encode( $iv . $tag . $ciphertext ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
60 }
61
62 public function decrypt( string $option_name, string $stored_value ): string {
63 if ( ! $this->is_encrypted( $stored_value ) || ! \function_exists( 'openssl_decrypt' ) ) {
64 return '';
65 }
66
67 // Decode the binary authenticated-encryption payload stored by encrypt().
68 $payload = \base64_decode( \substr( $stored_value, \strlen( self::PREFIX ) ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
69 if ( ! \is_string( $payload ) || self::IV_LENGTH + self::TAG_LENGTH >= \strlen( $payload ) ) {
70 return '';
71 }
72
73 $iv = \substr( $payload, 0, self::IV_LENGTH );
74 $tag = \substr( $payload, self::IV_LENGTH, self::TAG_LENGTH );
75 $ciphertext = \substr( $payload, self::IV_LENGTH + self::TAG_LENGTH );
76 $plaintext = \openssl_decrypt(
77 $ciphertext,
78 self::CIPHER,
79 $this->key(),
80 \OPENSSL_RAW_DATA,
81 $iv,
82 $tag,
83 $option_name
84 );
85
86 return \is_string( $plaintext ) ? $plaintext : '';
87 }
88
89 public function get( string $option_name ): string {
90 $stored_value = \get_option( $option_name, '' );
91
92 return \is_string( $stored_value ) ? $this->decrypt( $option_name, $stored_value ) : '';
93 }
94
95 public function has( string $option_name ): bool {
96 return '' !== $this->get( $option_name );
97 }
98
99 public function encrypted_value( string $option_name ): string {
100 $stored_value = \get_option( $option_name, '' );
101 if ( ! \is_string( $stored_value ) || '' === $this->decrypt( $option_name, $stored_value ) ) {
102 return '';
103 }
104
105 return $stored_value;
106 }
107
108 public function migrate_option( string $option_name ): bool {
109 $stored_value = \get_option( $option_name, '' );
110 if ( ! \is_string( $stored_value ) || '' === $stored_value ) {
111 return true;
112 }
113
114 if ( $this->is_encrypted( $stored_value ) ) {
115 $normalized = $stored_value;
116 for ( $depth = 0; $depth < 4; ++$depth ) {
117 $decrypted = $this->decrypt( $option_name, $normalized );
118 if ( '' === $decrypted ) {
119 return false;
120 }
121
122 if ( ! $this->is_encrypted( $decrypted ) ) {
123 return $normalized === $stored_value || \update_option( $option_name, $normalized, false );
124 }
125
126 $normalized = $decrypted;
127 }
128
129 return false;
130 }
131
132 $encrypted = $this->encrypt( $option_name, $stored_value );
133 if ( \is_wp_error( $encrypted ) ) {
134 return false;
135 }
136
137 return \update_option( $option_name, $encrypted, false );
138 }
139
140 private function key(): string {
141 $key = \defined( 'AUTH_KEY' ) ? (string) \constant( 'AUTH_KEY' ) : '';
142 $salt = \defined( 'AUTH_SALT' ) ? (string) \constant( 'AUTH_SALT' ) : '';
143
144 return \hash_hkdf( 'sha256', $key . $salt, 32, 'renzojohnson/blocks/secret-store/v1' );
145 }
146 }
147