PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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-factor / providers / class-totp.php

class-totp.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/component/two-factor/providers/class-totp.php

451 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for handling the Time-based One-Time Password (TOTP) authentication.
4 *
5 * @package WP_Defender\Component\Two_Factor\Providers
6 */
7
8 namespace WP_Defender\Component\Two_Factor\Providers;
9
10 use WP_User;
11 use WP_Error;
12 use SodiumException;
13 use Calotes\Helper\HTTP;
14 use WP_Defender\Traits\IO;
15 use WP_Defender\Extra\Base2n;
16 use WP_Defender\Component\Crypt;
17 use WP_Defender\Model\Setting\Two_Fa;
18 use WP_Defender\Component\Two_Fa as Two_Fa_Component;
19 use WP_Defender\Component\Two_Factor\Two_Factor_Provider;
20
21 /**
22 * Manage Time-based One-Time Password (TOTP) authentication.
23 * Note: key 'defenderAuthOn' only for TOTP method.
24 *
25 * @since 2.8.0
26 */
27 class Totp extends Two_Factor_Provider {
28
29 use IO;
30
31 /**
32 * 2fa provider slug.
33 *
34 * @var string
35 */
36 public static $slug = 'totp';
37
38 /**
39 * Meta key to check if TOTP is enabled for a user.
40 *
41 * @var string
42 */
43 public const TOTP_AUTH_KEY = 'defenderAuthOn';
44
45 /**
46 * Used def.key before v3.4.0.
47 *
48 * @var string
49 */
50 public const TOTP_SECRET_KEY = 'defenderAuthSecret';
51
52 /**
53 * Use Sodium library since v3.4.0.
54 *
55 * @var string
56 */
57 public const TOTP_SODIUM_SECRET_KEY = 'defenderAuthSodiumSecret';
58
59 /**
60 * Meta key to force TOTP authentication.
61 *
62 * @var string
63 */
64 public const TOTP_FORCE_KEY = 'defenderForceAuth';
65
66 /**
67 * Number of digits in the TOTP code.
68 *
69 * @var int
70 */
71 public const TOTP_DIGIT_COUNT = 6;
72
73 /**
74 * Time step in seconds for TOTP code validity.
75 *
76 * @var int
77 */
78 public const TOTP_TIME_STEP_SEC = 30;
79
80 /**
81 * Length of the TOTP secret key.
82 *
83 * @var int
84 */
85 public const TOTP_LENGTH = 16;
86
87 /**
88 * RFC 4648 base32 alphabet.
89 *
90 * @var string
91 */
92 public const TOTP_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
93
94 /**
95 * Default cryptographic method used for TOTP.
96 *
97 * @var string
98 */
99 public const DEFAULT_CRYPTO = 'sha1';
100
101 /**
102 * 1 = 30 second range for authenticator.
103 *
104 * @var int
105 */
106 public const TOTP_TIME_STEP_ALLOWANCE = 1;
107 /**
108 * Label for the TOTP method.
109 *
110 * @var string
111 */
112 protected $label;
113 /**
114 * Description for the TOTP method.
115 *
116 * @var string
117 */
118 protected $description;
119
120 /**
121 * Constructor to add actions for initializing and managing user options for TOTP.
122 */
123 public function __construct() {
124 add_action( 'wd_2fa_init_provider_' . self::$slug, array( $this, 'init_provider' ) );
125 add_action( 'wd_2fa_user_options_' . self::$slug, array( $this, 'user_options' ) );
126 }
127
128 /**
129 * Get the name of the provider.
130 *
131 * @return string
132 */
133 public function get_label(): string {
134 return esc_html__( 'TOTP Authenticator App', 'defender-security' ) . $this->label;
135 }
136
137 /**
138 * Returns the login label for the TOTP authentication method.
139 *
140 * @return string The login label for the TOTP authentication method.
141 */
142 public function get_login_label(): string {
143 return esc_html__( 'TOTP Authentication', 'defender-security' );
144 }
145
146 /**
147 * Returns the user label for the TOTP authentication method.
148 *
149 * @return string The user label for the TOTP authentication method.
150 */
151 public function get_user_label(): string {
152 return esc_html__( 'TOTP', 'defender-security' );
153 }
154
155 /**
156 * Get the desc of the provider.
157 *
158 * @return string
159 */
160 public function get_description(): string {
161 return $this->description;
162 }
163
164 /**
165 * Renders the authentication form for TOTP.
166 */
167 public function authentication_form() {
168 ?>
169 <p class="wpdef-2fa-label"><?php echo $this->get_login_label(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
170 <p class="wpdef-2fa-text def-otp-text"><?php echo esc_html( $this->get_model()->app_text ); ?></p>
171 <input type="text" autofocus value="" autocomplete="off" name="otp"/>
172 <button class="button button-primary float-r" type="submit">
173 <?php
174 esc_attr_e(
175 'Authenticate',
176 'defender-security'
177 );
178 ?>
179 </button>
180 <?php
181 }
182
183 /**
184 * Returns a list of supported authentication apps.
185 *
186 * @return array List of supported authentication apps.
187 */
188 public function get_auth_apps(): array {
189 return array(
190 'google-authenticator' => 'Google Authenticator',
191 'microsoft-authenticator' => 'Microsoft Authenticator',
192 'authy' => 'Authy',
193 );
194 }
195
196 /**
197 * Initializes the provider for a specific user.
198 *
199 * @param WP_User $user The user object.
200 */
201 public function init_provider( WP_User $user ) {
202 $is_on = $this->is_available_for_user( $user );
203 // $this->label will be escaped at src\view\two-fa\user-options.php.
204 $this->label = $is_on
205 ? sprintf(
206 '<button type="button" class="button reset-totp-keys button-secondary hide-if-no-js" %s>%s</button>',
207 $this->get_component()->is_checked_enabled_provider_by_slug( $user, self::$slug ) ? '' : ' disabled',
208 esc_html__( 'Reset Keys', 'defender-security' )
209 )
210 : '';
211 $this->description = $is_on
212 ? esc_html__( 'TOTP Authentication method is active for this site', 'defender-security' )
213 : esc_html__( 'Use an authenticator app to sign in with a separate passcode.', 'defender-security' );
214 }
215
216 /**
217 * Display auth method.
218 *
219 * @param WP_User $user The user object.
220 *
221 * @throws SodiumException Exceptions thrown by the sodium functions.
222 */
223 public function user_options( WP_User $user ) {
224 // Enqueue scripts.
225 if ( ! wp_script_is( 'clipboard', 'enqueued' ) ) {
226 wp_enqueue_script( 'clipboard' );
227 }
228 wp_enqueue_script(
229 'def-qrcode',
230 defender_asset_url( '/assets/js/jquery-qrcode.min.js' ),
231 array( 'jquery' ),
232 '0.18.0',
233 true
234 );
235
236 $model = $this->get_model();
237 $service = $this->get_component();
238 $default_values = $model->get_default_values();
239 $is_on = $this->is_available_for_user( $user );
240 if ( $is_on ) {
241 $this->get_controller()->render_partial(
242 'two-fa/providers/totp-enabled',
243 array(
244 'url' => $this->get_url( 'disable_totp' ),
245 )
246 );
247 } else {
248 $is_success = true;
249 $result = self::get_user_secret( $user->ID );
250 if ( is_wp_error( $result ) ) {
251 $secret = $result->get_error_message();
252 $is_success = false;
253 } elseif ( is_bool( $result ) ) {
254 // Sometimes we can get a boolean value due to errors with writing to the database. In this case, we need to reset the value.
255 delete_user_meta( $user->ID, self::TOTP_SECRET_KEY );
256 // Also for new key.
257 delete_user_meta( $user->ID, self::TOTP_SODIUM_SECRET_KEY );
258 $secret = self::get_user_secret( $user->ID );
259 } else {
260 $secret = $result;
261 }
262 $this->get_controller()->render_partial(
263 'two-fa/providers/totp-disabled',
264 array(
265 'url' => $this->get_url( 'verify_otp_for_enabling' ),
266 'default_message' => $default_values['message'],
267 'auth_apps' => $this->get_auth_apps(),
268 'user' => $user,
269 'secret_key' => $secret,
270 'class' => $service->is_checked_enabled_provider_by_slug(
271 $user,
272 self::$slug
273 ) ? '' : 'hidden',
274 'is_success' => $is_success,
275 )
276 );
277 }
278 }
279
280 /**
281 * Generate a QR code for apps can use. Apps from get_auth_apps().
282 *
283 * @param string $secret_key The secret key for the TOTP.
284 *
285 * @retun string
286 */
287 public static function generate_qr_code( $secret_key ) {
288 $settings = new Two_Fa();
289 $issuer = rawurlencode( $settings->app_title );
290 $user = wp_get_current_user();
291
292 return 'otpauth://totp/' . $issuer . ':' . rawurlencode( $user->user_email )
293 . '?secret=' . $secret_key . '&issuer=' . $issuer;
294 }
295
296 /**
297 * Whether this 2FA provider is configured and available for the user specified.
298 *
299 * @param WP_User $user WP_User object of the logged-in user.
300 *
301 * @return boolean
302 */
303 public function is_available_for_user( WP_User $user ) {
304 return (bool) get_user_meta( $user->ID, self::TOTP_AUTH_KEY, true );
305 }
306
307 /**
308 * Validates the TOTP authentication for a user.
309 *
310 * @param WP_User $user The user to validate.
311 *
312 * @return bool|WP_Error True if validation is successful, WP_Error otherwise.
313 * @throws SodiumException Exceptions thrown by the sodium functions.
314 */
315 public function validate_authentication( WP_User $user ) {
316 $otp = HTTP::post( 'otp' );
317 if ( empty( $otp ) ) {
318 $lockout_message = $this->get_component()->verify_attempt( $user->ID, self::$slug );
319
320 return new WP_Error(
321 'opt_fail',
322 empty( $lockout_message )
323 ? esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' )
324 : $lockout_message
325 );
326 }
327 $setup_key = self::get_user_secret( $user->ID );
328 if ( is_wp_error( $setup_key ) ) {
329 return new WP_Error(
330 'opt_fail',
331 esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' )
332 );
333 }
334
335 return self::verify_otp( $otp, $user->ID, $setup_key );
336 }
337
338 /**
339 * Retrieves the TOTP secret for a user.
340 *
341 * @param int $user_id The user ID.
342 *
343 * @return string|WP_Error The TOTP secret, or WP_Error on failure.
344 * @throws SodiumException Exceptions thrown by the sodium functions.
345 */
346 private static function get_user_secret( $user_id ) {
347 // First, we check the new 'TOTP_SODIUM_SECRET_KEY' key.
348 $data = get_user_meta( $user_id, self::TOTP_SODIUM_SECRET_KEY, true );
349 if ( ! empty( $data ) ) {
350 return Crypt::get_decrypted_data( $data );
351 }
352 // Then check the old 'TOTP_SECRET_KEY' key.
353 if ( ( new Two_Fa_Component() )->maybe_update( $user_id ) ) {
354 // Check a new key again.
355 $data = get_user_meta( $user_id, self::TOTP_SODIUM_SECRET_KEY, true );
356 if ( ! empty( $data ) && is_string( $data ) ) {
357 return Crypt::get_decrypted_data( $data );
358 }
359 }
360
361 // Finally, generate a new one.
362 return defender_generate_random_string( self::TOTP_LENGTH, self::TOTP_CHARACTERS );
363 }
364
365 /**
366 * Saves the TOTP setup key for a user.
367 *
368 * @param int $user_id The user ID.
369 * @param string $plaintext The plaintext TOTP key.
370 *
371 * @return bool|WP_Error True on success, WP_Error on failure.
372 * @throws SodiumException Exceptions thrown by the sodium functions.
373 */
374 public static function save_setup_key( $user_id, $plaintext ) {
375 $secret = Crypt::get_encrypted_data( $plaintext );
376 if ( is_wp_error( $secret ) ) {
377 return $secret;
378 }
379 update_user_meta( $user_id, self::TOTP_SODIUM_SECRET_KEY, $secret );
380
381 return true;
382 }
383
384 /**
385 * Generate an OTP code base on current time.
386 *
387 * @param int $counter The time counter.
388 * @param int $user_id The user ID.
389 * @param string $setup_key The TOTP setup key.
390 *
391 * @return string|WP_Error
392 * @throws SodiumException Exceptions thrown by the sodium functions.
393 */
394 private static function generate_otp( $counter, $user_id, $setup_key ) {
395 if ( empty( $setup_key ) || ! is_string( $setup_key ) ) {
396 $setup_key = self::get_user_secret( $user_id );
397 if ( is_wp_error( $setup_key ) ) {
398 return $setup_key;
399 }
400 }
401 include_once defender_path( 'src/extra/binary-to-text-php/Base2n.php' );
402 $base32 = new Base2n( 5, self::TOTP_CHARACTERS, false, true, true );
403 $secret = $base32->decode( $setup_key );
404 $input = floor( $counter / self::TOTP_TIME_STEP_SEC );
405 // According to https://tools.ietf.org/html/rfc4226#section-5.3, should be 8 bytes value.
406 $time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $input );
407 $hmac = hash_hmac( self::DEFAULT_CRYPTO, $time, $secret, true );
408 // Now we have 20 bytes of DEFAULT_CRYPTO, need to short it down. Getting last byte of the hmac.
409 $offset = ord( substr( $hmac, - 1 ) ) & 0x0F;
410 $four_bytes = substr( $hmac, $offset, 4 );
411 // Now convert it into INT.
412 $value = unpack( 'N', $four_bytes );
413 $value = $value[1];
414 // Make sure it always actual like 32 bits.
415 $value = $value & 0x7FFFFFFF;
416 // Close.
417 $code = $value % 10 ** self::TOTP_DIGIT_COUNT;
418
419 // In some case we have the 0 before, so it becomes lesser than TOTP_DIGIT_COUNT, make sure it always right.
420 return str_pad( (string) $code, self::TOTP_DIGIT_COUNT, '0', STR_PAD_LEFT );
421 }
422
423 /**
424 * Verify the OTP of beyond & after TOTP_TIME_STEP_SEC seconds windows.
425 *
426 * @param string $user_code The TOTP code entered by the user.
427 * @param int $user_id The user ID.
428 * @param string $setup_key The TOTP setup key.
429 *
430 * @return bool|WP_Error
431 * @throws SodiumException Exceptions thrown by the sodium functions.
432 */
433 public static function verify_otp( string $user_code, int $user_id, $setup_key ) {
434 if ( strlen( $user_code ) < self::TOTP_DIGIT_COUNT ) {
435 return false;
436 }
437 for ( $i = - self::TOTP_TIME_STEP_ALLOWANCE; $i <= self::TOTP_TIME_STEP_ALLOWANCE; $i++ ) {
438 $counter = 0 === $i ? time() : $i * self::TOTP_TIME_STEP_SEC + time();
439 $code = self::generate_otp( $counter, $user_id, $setup_key );
440 if ( is_wp_error( $code ) ) {
441 return $code;
442 }
443 if ( Crypt::compare_lines( $user_code, $code ) ) {
444 return true;
445 }
446 }
447
448 return false;
449 }
450 }
451