PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.0.1
Defender Security – Malware Scanner, Login Security & Firewall v3.0.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 / two-fa.php

two-fa.php in Defender Security – Malware Scanner, Login Security & Firewall 3.0.1, at src/component/two-fa.php

559 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Component;
4
5 use Calotes\Base\Component;
6 use WP_Defender\Extra\Base2n;
7 use Calotes\Helper\Array_Cache;
8 use WP_Defender\Component\Two_Factor\Providers\Totp;
9 use WP_Defender\Component\Two_Factor\Providers\Backup_Codes;
10 use WP_Defender\Component\Two_Factor\Providers\Fallback_Email;
11 use WP_Defender\Component\Two_Factor\Providers\Webauthn;
12
13 class Two_Fa extends Component {
14 /**
15 * The user meta key for the default provider.
16 *
17 * @type string
18 */
19 public const DEFAULT_PROVIDER_USER_KEY = 'wd_2fa_default_provider';
20
21 /**
22 * The user meta key for enabled providers.
23 *
24 * @type string
25 */
26 public const ENABLED_PROVIDERS_USER_KEY = 'wd_2fa_enabled_providers';
27
28 /**
29 * @type int
30 */
31 public const TOTP_DIGIT_COUNT = 6;
32
33 /**
34 * @type int
35 */
36 public const TOTP_TIME_STEP_SEC = 30;
37
38 /**
39 * @type int
40 */
41 public const TOTP_LENGTH = 16;
42
43 /**
44 * RFC 4648 base32 alphabet.
45 * @type string
46 */
47 public const TOTP_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
48
49 /**
50 * @type string
51 */
52 public const DEFAULT_CRYPTO = 'sha1';
53
54 /**
55 * @param int $user_id
56 */
57 public function enable_otp( $user_id ) {
58 update_user_meta( $user_id, 'defenderAuthOn', 1 );
59 update_user_meta( $user_id, 'defenderForceAuth', 0 );
60 }
61
62 /**
63 * Gradually move on to using the method is_enabled_otp_for_user(). Used only by the Forminator plugin.
64 *
65 * @param int $user_id
66 *
67 * @return mixed
68 */
69 public function is_user_enabled_otp( $user_id ) {
70 return get_user_meta( $user_id, 'defenderAuthOn', true );
71 }
72
73 /**
74 * @param $user_id
75 * @param $roles
76 *
77 * @return bool
78 */
79 public function is_force_auth_enable_for( $user_id, $roles ) {
80 $user = get_user_by( 'id', $user_id );
81 if ( ! is_object( $user ) ) {
82 return false;
83 }
84
85 $check = array_intersect( $user->roles, $roles );
86
87 return count( $check ) > 0;
88 }
89
90 /**
91 * Verify the OTP of beyond & after TOTP_TIME_STEP_SEC seconds windows. Forminator uses it.
92 *
93 * @param string $user_code
94 * @param null|\WP_User $user
95 *
96 * @return bool
97 */
98 public function verify_otp( $user_code, $user = null ) {
99 if ( strlen( $user_code ) < self::TOTP_DIGIT_COUNT ) {
100 return false;
101 }
102 for ( $i = - 30; $i <= self::TOTP_TIME_STEP_SEC; $i ++ ) {
103 $counter = 0 === $i ? null : $i * self::TOTP_TIME_STEP_SEC + time();
104 $code = self::generate_otp( $counter, $user );
105 if ( hash_equals( $user_code, $code ) ) {
106 return true;
107 }
108 }
109
110 return false;
111 }
112
113 /**
114 * Generate an OTP code base on current time.
115 *
116 * @param null $counter
117 * @param null|\WP_User $user
118 *
119 * @return int|string
120 */
121 public static function generate_otp( $counter = null, $user = null ) {
122 include_once defender_path( 'src/extra/binary-to-text-php/Base2n.php' );
123 $base32 = new Base2n( 5, self::TOTP_CHARACTERS, false, true, true );
124 $secret = $base32->decode( self::get_user_secret( $user ) );
125 if ( is_null( $counter ) ) {
126 $counter = time();
127 }
128 $input = floor( $counter / self::TOTP_TIME_STEP_SEC );
129 // According to https://tools.ietf.org/html/rfc4226#section-5.3, should be a 8 bytes value.
130 $time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $input );
131 $hmac = hash_hmac( self::DEFAULT_CRYPTO, $time, $secret, true );
132 // Now we have 20 bytes of DEFAULT_CRYPTO, need to short it down. Getting last byte of the hmac.
133 $offset = ord( substr( $hmac, - 1 ) ) & 0x0F;
134 $four_bytes = substr( $hmac, $offset, 4 );
135 // Now convert it into INT.
136 $value = unpack( 'N', $four_bytes );
137 $value = $value[1];
138 // Make sure it always actual like 32 bits.
139 $value = $value & 0x7FFFFFFF;
140 // Close.
141 $code = $value % 10 ** self::TOTP_DIGIT_COUNT;
142 // In some case we have the 0 before, so it becomes lesser than TOTP_DIGIT_COUNT, make sure it always right.
143 return str_pad( $code, self::TOTP_DIGIT_COUNT, '0', STR_PAD_LEFT );
144 }
145
146 /**
147 * @param $user
148 *
149 * @return mixed|string
150 */
151 public static function get_user_secret( $user = null ) {
152 // This should only use in testing.
153 if ( is_object( $user ) ) {
154 $user_id = $user->ID;
155 } else {
156 $user_id = get_current_user_id();
157 }
158 $secret = get_user_meta( $user_id, 'defenderAuthSecret', true );
159 if ( ! empty( $secret ) ) {
160 return $secret;
161 }
162 $secret = defender_generate_random_string( self::TOTP_LENGTH, self::TOTP_CHARACTERS );
163 update_user_meta( $user_id, 'defenderAuthSecret', $secret );
164
165 return $secret;
166 }
167
168 /**
169 * Count the total of users, who enables any 2FA method.
170 *
171 * @return int
172 */
173 public function count_users_with_enabled_2fa() {
174 $slugs = array_keys( $this->get_providers() );
175 $query = new \WP_User_Query(
176 array(
177 // Look over the network.
178 'blog_id' => 0,
179 'meta_key' => self::DEFAULT_PROVIDER_USER_KEY,
180 'meta_value' => $slugs,
181 'meta_compare' => 'IN',
182 )
183 );
184
185 return $query->get_total();
186 }
187
188 /**
189 * @return bool
190 */
191 public function is_jetpack_sso() {
192 $settings = new \WP_Defender\Model\Setting\Two_Fa();
193 if ( is_plugin_active_for_network( 'jetpack/jetpack.php' ) ) {
194 // Loop through all sites.
195 $is_conflict = $settings->is_conflict( 'jetpack/jetpack.php' );
196 if ( 0 === $is_conflict ) {
197 // No data, init.
198 global $wpdb;
199 $sql = "SELECT blog_id FROM `{$wpdb->base_prefix}blogs`";
200 $blogs = $wpdb->get_col( $sql );
201 foreach ( $blogs as $id ) {
202 $options = get_blog_option( $id, 'jetpack_active_modules', array() );
203 if ( array_search( 'sso', $options ) ) {
204 $settings->mark_as_conflict( 'jetpack/jetpack.php' );
205
206 return true;
207 }
208 }
209 } else {
210 // Get the data from cache.
211 return $is_conflict;
212 }
213
214 } elseif ( is_plugin_active( 'jetpack/jetpack.php' ) ) {
215 $is_conflict = $settings->is_conflict( 'jetpack/jetpack.php' );
216 if ( 0 === $is_conflict ) {
217 $options = get_option( 'jetpack_active_modules', array() );
218 if ( array_search( 'sso', $options ) ) {
219 $settings->mark_as_conflict( 'jetpack/jetpack.php' );
220
221 return true;
222 }
223 } else {
224 return $is_conflict;
225 }
226
227 }
228
229 return false;
230 }
231
232 /**
233 * @return bool
234 */
235 public function is_tml() {
236 if (
237 is_plugin_active( 'theme-my-login/theme-my-login.php' )
238 || is_plugin_active_for_network( 'theme-my-login/theme-my-login.php' )
239 ) {
240 $settings = new \WP_Defender\Model\Setting\Two_Fa();
241 $settings->mark_as_conflict( 'theme-my-login/theme-my-login.php' );
242
243 return true;
244 }
245
246 return false;
247 }
248
249 /**
250 * @param string $url
251 *
252 * @return string
253 */
254 public function get_custom_graphic_url( $url = '' ) {
255 if ( empty( $url ) ) {
256 // Nothing here, surely it will cause broken, fall back to default.
257 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
258 } else {
259 // Image should be under wp-content/.., so we catch that part.
260 if ( preg_match( '/(\/wp-content\/.+)/', $url, $matches ) ) {
261 $rel_path = $matches[1];
262 $rel_path = ltrim( $rel_path, '/' );
263 $abs_path = ABSPATH . $rel_path;
264 if ( ! file_exists( $abs_path ) ) {
265 // Fallback.
266 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
267 } else {
268 // Should replace with our site url.
269 return get_site_url( null, $rel_path );
270 }
271 }
272
273 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
274 }
275 }
276
277 /**
278 * @param WP_User $user
279 *
280 * @return bool
281 */
282 public function is_enable_for_current_role( $user ) {
283 if ( 0 === (is_array($user->roles) || $user->roles instanceof \Countable ? count( $user->roles ) : 0) ) {
284 return true;
285 }
286
287 $settings = new \WP_Defender\Model\Setting\Two_Fa();
288 if ( ! is_multisite() ) {
289 $allowed_for_this_role = array_intersect( $settings->user_roles, $user->roles );
290 if ( ! is_array( $allowed_for_this_role ) ) {
291 $allowed_for_this_role = [];
292 }
293
294 return count( $allowed_for_this_role ) > 0;
295 } else {
296 $blogs = get_blogs_of_user( $user->ID );
297 $user_roles = array();
298 foreach ( $blogs as $blog ) {
299 // Get user roles for this blog.
300 $u = new \WP_User( $user->ID, '', $blog->userblog_id );
301 $user_roles = array_merge( $u->roles, $user_roles );
302 }
303 $allowed_for_this_role = array_intersect( $settings->user_roles, $user_roles );
304
305 return count( $allowed_for_this_role ) > 0;
306 }
307 }
308
309 /**
310 * @param $user_value
311 *
312 * @return bool
313 */
314 public function is_enabled_otp_for_user( $user_value ) {
315 if ( is_numeric( $user_value ) ) {
316 $user = get_user_by( 'id', $user_value );
317 } elseif ( $user_value instanceof \WP_User ) {
318 $user = $user_value;
319 } else{
320 return false;
321 }
322
323 if ( ! $this->is_enable_for_current_role( $user ) ) {
324 return false;
325 }
326
327 return (bool) get_user_meta( $user->ID, 'defenderAuthOn', true );
328 }
329
330 /**
331 * @return bool
332 */
333 public function is_intersected_arrays( $current_user_roles, $plugin_user_roles ) {
334 return ! empty( array_intersect( $current_user_roles, $plugin_user_roles ) );
335 }
336
337 /**
338 * @since 2.8.0
339 * @return array
340 */
341 public function get_providers() {
342 $providers = Array_Cache::get( 'providers', 'providers' );
343 if ( ! is_array( $providers ) ) {
344 $classes = array(
345 Totp::class,
346 Backup_Codes::class,
347 Fallback_Email::class,
348 Webauthn::class,
349 );
350 /**
351 * Filter the supplied providers.
352 *
353 * @param array $classes
354 * @since 2.8.0
355 */
356 $classes = apply_filters( 'wd_2fa_providers', $classes );
357 foreach ( $classes as $class ) {
358 $providers[ $class::$slug ] = new $class();
359 }
360 Array_Cache::set( 'providers', $providers, 'providers' );
361 }
362
363 return $providers;
364 }
365
366 /**
367 * Get all 2FA Auth providers that are enabled for the specified|current user.
368 *
369 * @param WP_User|null $user
370 *
371 * @return array
372 */
373 public function get_enabled_providers_for_user( $user = null ) {
374 if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
375 $user = wp_get_current_user();
376 }
377
378 $providers = $this->get_providers();
379 $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, true );
380 if ( empty( $enabled_providers ) ) {
381 $enabled_providers = array();
382 }
383 $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
384 /**
385 * Filter the enabled 2FA providers for this user.
386 *
387 * @param array $enabled_providers The enabled providers.
388 * @param int $user_id The user ID.
389 * @since 2.8.0
390 */
391 return apply_filters( 'wd_2fa_enabled_providers_for_user', $enabled_providers, $user->ID );
392 }
393
394 /**
395 * @return array
396 */
397 public function get_available_providers_for_user( $user = null ) {
398 if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
399 $user = wp_get_current_user();
400 }
401
402 $providers = $this->get_providers();
403 $enabled_providers = $this->get_enabled_providers_for_user( $user );
404 $configured_providers = array();
405
406 foreach ( $providers as $slug => $provider ) {
407 if ( in_array( $slug, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) {
408 $configured_providers[ $slug ] = $provider;
409 }
410 }
411
412 return $configured_providers;
413 }
414
415 /**
416 * Gets the 2FA provider's slug for the specified or current user.
417 *
418 * @param int $user_id Optional. User ID. Default is 'null'.
419 *
420 * @return string|null
421 */
422 public function get_default_provider_slug_for_user( $user_id = null ) {
423 if ( empty( $user_id ) || ! is_numeric( $user_id ) ) {
424 $user_id = get_current_user_id();
425 }
426
427 $available_providers = $this->get_available_providers_for_user( get_userdata( $user_id ) );
428 // If there's only one available provider, force that to be the primary.
429 if ( empty( $available_providers ) ) {
430 return null;
431 } elseif ( 1 === count( $available_providers ) ) {
432 $provider_slug = key( $available_providers );
433 } else {
434 $provider_slug = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
435
436 // If the provider specified isn't enabled, just grab the first one that is.
437 if ( ! isset( $available_providers[ $provider_slug ] ) ) {
438 $provider_slug = key( $available_providers );
439 }
440 }
441
442 /**
443 * Filter the 2FA provider slug used for this user.
444 *
445 * @param string $provider_slug The provider slug currently being used.
446 * @param int $user_id The user ID.
447 */
448 return apply_filters( 'wd_2fa_default_provider_for_user', $provider_slug, $user_id );
449 }
450
451 /**
452 * @param \WP_User $user
453 * @param string $slug
454 *
455 * @retun bool
456 */
457 public function is_checked_enabled_provider_by_slug( $user, $slug ) {
458 $enabled_providers = $this->get_enabled_providers_for_user( $user );
459
460 return in_array( $slug, $enabled_providers, true );
461 }
462
463 /**
464 * Send emergency email to users.
465 *
466 * @param string $login_token This will be generated randomly on frontend each time user refresh, an internal OTP.
467 *
468 * @return boolean|\WP_Error
469 */
470 public function send_otp_to_email( $login_token ) {
471 $settings = new \WP_Defender\Model\Setting\Two_Fa();
472 $query = new \WP_User_Query( [
473 'blog_id' => 0,
474 'meta_key' => 'defender_two_fa_token',
475 'meta_value' => $login_token
476 ] );
477 if ( 0 === $query->get_total() ) {
478 return new \WP_Error( Error_Code::INVALID, __( 'Your token is invalid.', 'wpdef' ) );
479 }
480
481 $user = $query->get_results()[0];
482 $code = wp_generate_password( 20, false );
483 update_user_meta( $user->ID, 'defenderBackupCode', array(
484 'code' => $code,
485 'time' => time(),
486 ) );
487 $params = array(
488 'display_name' => $user->display_name,
489 'passcode' => $code,
490 );
491 $two_fa = wd_di()->get( \WP_Defender\Controller\Two_Factor::class );
492 $body = $two_fa->render_partial( 'email/2fa-lost-phone', [
493 'body' => nl2br( $settings->email_body ),
494 ], false );
495
496 foreach ( $params as $key => $val ) {
497 $body = str_replace( '{{' . $key . '}}', $val, $body );
498 }
499 // Main email template.
500 $body = $two_fa->render_partial(
501 'email/index',
502 array(
503 'title' => __( 'Two-Factor Authentication', 'wpdef' ),
504 'content_body' => $body,
505 ),
506 false
507 );
508 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
509 $from_email = get_bloginfo( 'admin_email' );
510 $headers[] = sprintf( 'From: %s <%s>', $settings->email_sender, $from_email );
511
512 return wp_mail( Fallback_Email::get_backup_email( $user->ID ), $settings->email_subject, $body, $headers );
513 }
514
515 /**
516 * @return object|\WP_Error
517 */
518 public function get_provider_by_slug( $slug ) {
519 foreach ( $this->get_providers() as $key => $provider ) {
520 if ( $slug === $key ) {
521 return $provider;
522 }
523 }
524
525 return new \WP_Error( 'opt_fail', __( 'ERROR: Cheatin&#8217; uh?', 'wpdef' ) );
526 }
527
528 /**
529 * Remove enabled provider for the specified|current user.
530 *
531 * @param string $provider A provider which needs to be removed.
532 * @param WP_User|null $user
533 *
534 * @since 3.0.0
535 * @return void
536 */
537 public function remove_enabled_provider_for_user( $provider, $user = null ) {
538 if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
539 $user = wp_get_current_user();
540 }
541
542 $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, true );
543 if ( empty( $enabled_providers ) || ! is_array( $enabled_providers ) ) {
544 return;
545 }
546
547 $pos = array_search( $provider, $enabled_providers );
548 if ( false !== $pos ) {
549 unset( $enabled_providers[ $pos ] );
550 update_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
551
552 $default_provider = get_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY, true );
553 if ( $provider === $default_provider ) {
554 delete_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY );
555 }
556 }
557 }
558 }
559