PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / trunk
Defender Security – Malware Scanner, Login Security & Firewall vtrunk
6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 3.1.1 All 140 releases
defender-security / src / component / class-crypt.php

class-crypt.php in Defender Security – Malware Scanner, Login Security & Firewall trunk, at src/component/class-crypt.php

297 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Methods for generating cryptographically secure pseudo-random bytes and integers, comparing strings securely,
4 * and encrypting/decrypting data.
5 *
6 * @package WP_Defender\Component
7 */
8
9 namespace WP_Defender\Component;
10
11 use WP_Error;
12 use Exception;
13 use SodiumException;
14 use RuntimeException;
15 use WP_Defender\Traits\IO;
16 use Calotes\Base\Component;
17 use WP_Filesystem_Base;
18
19 /**
20 * Methods for generating cryptographically secure pseudo-random bytes and integers, comparing strings securely,
21 * and encrypting/decrypting data.
22 *
23 * @since 3.3.1
24 */
25 class Crypt extends Component {
26
27 use IO;
28
29 /**
30 * Generates cryptographically secure pseudo-random bytes.
31 *
32 * @param int $bytes The number of bytes to generate.
33 *
34 * @return string
35 */
36 public static function random_bytes( int $bytes ): string {
37 // Try with random_bytes.
38 if ( function_exists( 'random_bytes' ) ) {
39 try {
40 $rand = random_bytes( $bytes );
41 if ( is_string( $rand ) && strlen( $rand ) === $bytes ) {
42 return $rand;
43 }
44 } catch ( Exception $e ) {
45 $_this = new self();
46 $_this->log( $e->getMessage(), wd_internal_log() );
47 }
48 }
49 // Try with openssl_random_pseudo_bytes.
50 if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
51 $rand = openssl_random_pseudo_bytes( $bytes, $strong );
52 if ( is_string( $rand ) && strlen( $rand ) === $bytes ) {
53 return $rand;
54 }
55 }
56 // Not safe. Use in extreme cases.
57 $return = '';
58 for ( $i = 0; $i < $bytes; $i++ ) {
59 $return .= chr( wp_rand( 0, 255 ) );
60 }
61
62 return $return;
63 }
64
65 /**
66 * Generates cryptographically secure pseudo-random integers.
67 *
68 * @param int $min The minimum value of the generated integer (inclusive).
69 * @param int $max The maximum value of the generated integer (inclusive).
70 *
71 * @return int
72 * @throws RuntimeException On failure.
73 */
74 public static function random_int( $min = 0, $max = 0x7FFFFFFF ): int {
75 if ( function_exists( 'random_int' ) ) {
76 try {
77 return random_int( $min, $max );
78 } catch ( Exception $e ) {
79 $_this = new self();
80 $_this->log( $e->getMessage(), wd_internal_log() );
81 }
82 }
83 $diff = $max - $min;
84 $bytes = self::random_bytes( 4 );
85 if ( 4 !== strlen( $bytes ) ) {
86 throw new RuntimeException( 'Unable to get 4 bytes' );
87 }
88 $val = unpack( 'nint', $bytes );
89 $val = $val['int'] & 0x7FFFFFFF;
90 // Convert to [0,1].
91 $fp = (float) $val / 2147483647.0;
92
93 return (int) ( round( $fp * $diff ) + $min );
94 }
95
96 /**
97 * Compare two strings to avoid timing attacks.
98 *
99 * @param string $expected The expected string.
100 * @param string $actual The actual string to compare against expected.
101 *
102 * @return bool
103 */
104 public static function compare_lines( $expected, $actual ): bool {
105 if ( function_exists( 'hash_equals' ) ) {
106 return hash_equals( $expected, $actual );
107 }
108
109 $len_expected = mb_strlen( $expected, '8bit' );
110 $len_actual = mb_strlen( $actual, '8bit' );
111 $len = min( $len_expected, $len_actual );
112
113 $result = 0;
114 for ( $i = 0; $i < $len; $i++ ) {
115 $result |= ord( $expected[ $i ] ) ^ ord( $actual[ $i ] );
116 }
117 $result |= $len_expected ^ $len_actual;
118
119 return 0 === $result;
120 }
121
122 /**
123 * Encrypts a given string using a specified key.
124 *
125 * @param string $value The plaintext value to encrypt.
126 * @param string $key The encryption key.
127 *
128 * @return string|WP_Error Returns the encrypted string or WP_Error on failure.
129 * @throws SodiumException Throws an exception if encryption fails.
130 */
131 private static function encrypt( $value, $key ) {
132 // This is not obfuscation. Just decode a base64-encoded string.
133 $key = base64_decode( $key, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
134 if ( SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen( $key, '8bit' ) ) {
135 return new WP_Error(
136 Error_Code::ENCRYPT_ERROR,
137 esc_html__( 'The issue with Sodium library.', 'defender-security' )
138 );
139 }
140 $nonce = self::random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
141 $ciphertext = sodium_crypto_secretbox( $value, $nonce, $key );
142 // This is not obfuscation. Just encode the resulting string.
143 return base64_encode( $nonce . $ciphertext ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
144 }
145
146 /**
147 * Decrypts an encrypted string using a specified key.
148 *
149 * @param string $encoded_value The encrypted data to decrypt.
150 * @param string $key The decryption key.
151 *
152 * @return string|WP_Error Returns the decrypted string or WP_Error on failure.
153 * @throws SodiumException Throws an exception if decryption fails.
154 */
155 private static function decrypt( $encoded_value, $key ) {
156 if ( ! $encoded_value || '' === $key ) {
157 return new WP_Error(
158 Error_Code::DECRYPT_ERROR,
159 esc_html__( 'Please re-setup 2FA TOTP method again.', 'defender-security' )
160 );
161 }
162 // No obfuscation. Just decode base64-encoded $key and $encoded_value strings.
163 $key = base64_decode( $key, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
164 $decoded = base64_decode( $encoded_value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
165 $nonce = mb_substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit' );
166 $ciphertext = mb_substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit' );
167
168 $decrypted = sodium_crypto_secretbox_open( $ciphertext, $nonce, $key );
169 if ( false === $decrypted ) {
170 return new WP_Error(
171 Error_Code::DECRYPT_ERROR,
172 esc_html__( 'Please re-setup 2FA TOTP method again.', 'defender-security' )
173 );
174 }
175
176 return $decrypted;
177 }
178
179 /**
180 * Get the path to a file with a random key. This is used for 2FA TOTP.
181 *
182 * @return string
183 */
184 public static function get_path_to_key_file() {
185 return wp_normalize_path( WP_CONTENT_DIR ) . DIRECTORY_SEPARATOR . 'wp-defender-secrets.php';
186 }
187
188 /**
189 * Decrypts data using a stored random key.
190 *
191 * @param string $data The encrypted data to decrypt.
192 *
193 * @return string|WP_Error Returns the decrypted data or WP_Error on failure.
194 * @throws SodiumException Throws an exception if decryption fails.
195 */
196 public static function get_decrypted_data( $data ) {
197 $key = self::get_random_key();
198 if ( is_wp_error( $key ) ) {
199 return $key;
200 }
201
202 return self::decrypt( $data, $key );
203 }
204
205 /**
206 * Encrypts data using a stored random key.
207 *
208 * @param string $data The plaintext data to encrypt.
209 *
210 * @return string|WP_Error Returns the encrypted data or WP_Error on failure.
211 * @throws SodiumException Throws an exception if encryption fails.
212 */
213 public static function get_encrypted_data( $data ) {
214 $key = self::get_random_key();
215 if ( is_wp_error( $key ) ) {
216 return $key;
217 }
218
219 return self::encrypt( $data, $key );
220 }
221
222 /**
223 * Retrieves a random cryptographic key from a file.
224 *
225 * @return string|WP_Error Returns the cryptographic key or WP_Error if the key file is not found or invalid.
226 * @throws SodiumException Throws an exception if key retrieval fails.
227 */
228 private static function get_random_key() {
229 $file = self::get_path_to_key_file();
230 if ( ! file_exists( $file ) ) {
231 return new WP_Error(
232 Error_Code::IS_EMPTY,
233 esc_html__( 'The Defender file with the random key does not exist.', 'defender-security' )
234 );
235 }
236
237 if ( ! defined( 'WP_DEFENDER_TOTP_KEY' ) ) {
238 require_once $file;
239 }
240
241 if ( '{{__REPLACE_CODE__}}' !== constant( 'WP_DEFENDER_TOTP_KEY' ) ) {
242 return WP_DEFENDER_TOTP_KEY;
243 } else {
244 return new WP_Error(
245 Error_Code::INVALID,
246 esc_html__( 'The Defender file with the random key is incorrect.', 'defender-security' )
247 );
248 }
249 }
250
251 /**
252 * Generate a random key.
253 *
254 * @return string
255 * @throws Exception On failure.
256 */
257 protected function generate_random_key(): string {
258 // This is not obfuscation. Just encode the binary key into a base64 string.
259 return base64_encode( sodium_crypto_secretbox_keygen() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
260 }
261
262 /**
263 * Create a file with a random key.
264 *
265 * @return bool
266 * @throws Exception On failure.
267 */
268 public function create_key_file(): bool {
269 global $wp_filesystem;
270 // Initialize the WP filesystem, no more using 'file-put-contents' function.
271 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
272 require_once ABSPATH . '/wp-admin/includes/file.php';
273 WP_Filesystem();
274 }
275 $to = self::get_path_to_key_file();
276 if ( ! file_exists( $to ) ) {
277 // Move a template file to WP_CONTENT and replace the file content.
278 $template_file = WP_DEFENDER_DIR . 'src' . DIRECTORY_SEPARATOR . 'component' . DIRECTORY_SEPARATOR
279 . 'wp-defender-sample.php';
280 if ( copy( $template_file, $to ) ) {
281 $content = $wp_filesystem->get_contents( $to );
282 if ( false !== strpos( $content, '{{__REPLACE_CODE__}}' ) ) {
283 $new_content = str_replace( '{{__REPLACE_CODE__}}', $this->generate_random_key(), $content );
284
285 return (bool) file_put_contents( $to, $new_content, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
286 }
287 }
288
289 // The file was not copied.
290 return false;
291 }
292
293 // Everything is fine. The file exists.
294 return true;
295 }
296 }
297