PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.1
6.3.0 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 All 141 releases
defender-security / src / component / class-crypt.php

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

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