PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 2.1.0
WP 2FA – Two-factor authentication for WordPress v2.1.0
1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Authenticator / Authentication.php
wp-2fa / includes / classes / Authenticator Last commit date
Authentication.php 4 years ago BackupCodes.php 4 years ago Login.php 4 years ago class-open-ssl.php 4 years ago index.php 5 years ago
Authentication.php
553 lines
1 <?php // phpcs:ignore
2 /**
3 * Class for handling general authentication tasks.
4 *
5 * @since 0.1-dev
6 *
7 * @package WP2FA
8 */
9
10 namespace WP2FA\Authenticator;
11
12 use Endroid\QrCode\QrCode;
13 use Endroid\QrCode\Writer\SvgWriter;
14 use WP2FA\Admin\Controllers\Login_Attempts;
15
16 /**
17 * Authenticator class
18 */
19 class Authentication {
20
21 const SECRET_META_KEY = 'wp_2fa_totp_key';
22 const NOTICES_META_KEY = 'wp_2fa_totp_notices';
23 const TOKEN_META_KEY = 'wp_2fa_email_token';
24 const DEFAULT_KEY_BIT_SIZE = 160;
25 const DEFAULT_CRYPTO = 'sha1';
26 const DEFAULT_DIGIT_COUNT = 6;
27 const DEFAULT_TIME_STEP_SEC = 30;
28 const DEFAULT_TIME_STEP_ALLOWANCE = 4;
29
30 /**
31 * Holds the name of the meta key for the allowed login attempts
32 *
33 * @var string
34 *
35 * @since 2.0.0
36 */
37 private static $login_num_meta_key = WP_2FA_PREFIX . 'email-login-attempts';
38
39 /**
40 * The login attempts class
41 *
42 * @var WP2FA\Extensions\Login_Attempts
43 *
44 * @since 2.0.0
45 */
46 private static $login_attempts = null;
47
48 /**
49 * String with the base32 characters
50 *
51 * @var string
52 */
53 private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
54
55 /**
56 * Gemerate QR code
57 *
58 * @param string $name Username.
59 * @param string $key Auth key.
60 * @param string $title Site title.
61 * @return string QR code URL.
62 */
63 public static function get_google_qr_code( $name, $key, $title = null ) {
64 // Encode to support spaces, question marks and other characters.
65 $name = rawurlencode( $name );
66
67 self::decrypt_key_if_needed( $key );
68
69 $target_url = ( 'otpauth://totp/' . $name . '?secret=' . $key );
70 if ( isset( $title ) ) {
71 $target_url .= ( '&issuer=' . rawurlencode( $title ) );
72 }
73
74 $qr = new QrCode( $target_url );
75 $qr->setWriterOptions(['exclude_xml_declaration'=>true]);
76 $writer = new SvgWriter();
77 $result = $writer->writeString( $qr );
78
79 return 'data:image/svg+xml;base64,'.base64_encode($result);
80 }
81
82 /**
83 * Generates key
84 *
85 * @param int $bitsize Nume of bits to use for key.
86 *
87 * @return string $bitsize long string composed of available base32 chars.
88 */
89 public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
90 $bytes = ceil( $bitsize / 8 );
91 $secret = wp_generate_password( $bytes, true, true );
92
93 $secret = Open_SSL::encrypt( self::base32_encode( $secret ) );
94
95 if ( Open_SSL::is_ssl_available() ) {
96 $secret = 'ssl_' . $secret;
97 }
98
99 return $secret;
100 }
101 /**
102 * Returns a base32 encoded string.
103 *
104 * @param string $string String to be encoded using base32.
105 *
106 * @return string base32 encoded string without padding.
107 */
108 public static function base32_encode( $string ) {
109 if ( empty( $string ) ) {
110 return '';
111 }
112
113 $binary_string = '';
114
115 foreach ( str_split( $string ) as $character ) {
116 $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
117 }
118
119 $five_bit_sections = str_split( $binary_string, 5 );
120 $base32_string = '';
121
122 foreach ( $five_bit_sections as $five_bit_section ) {
123 $base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
124 }
125
126 return $base32_string;
127 }
128
129 /**
130 * Get the TOTP secret key for a user.
131 *
132 * @param int $user_id User ID.
133 *
134 * @return string
135 */
136 public static function get_user_totp_key( $user_id ) {
137
138 $key = (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
139
140 $test = $key;
141
142 self::decrypt_key_if_needed( $test );
143
144 if ( ! self::is_valid_key( $test ) ) {
145 $key = self::generate_key();
146 \update_user_meta( $user_id, self::SECRET_META_KEY, $key );
147 }
148
149 return $key;
150 }
151
152 /**
153 * Check if the TOTP secret key has a proper format.
154 *
155 * @param string $key TOTP secret key.
156 *
157 * @return boolean
158 */
159 public static function is_valid_key( $key ) {
160 self::decrypt_key_if_needed( $key );
161
162 $check = sprintf( '/^[%s]+$/', self::$base_32_chars );
163
164 if ( 1 === preg_match( $check, $key ) ) {
165 return true;
166 }
167
168 return false;
169 }
170
171 /**
172 * Checks if a given code is valid for a given key, allowing for a certain amount of time drift
173 *
174 * @param string $key The share secret key to use.
175 * @param string $authcode The code to test.
176 *
177 * @return bool Whether the code is valid within the time frame
178 */
179 public static function is_valid_authcode( $key, $authcode ) {
180
181 self::decrypt_key_if_needed( $key );
182
183 $max_ticks = apply_filters( 'wp_2fa_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
184
185 // Array of all ticks to allow, sorted using absolute value to test closest match first.
186 $ticks = range( - $max_ticks, $max_ticks );
187 usort( $ticks, array( __CLASS__, 'abssort' ) );
188
189 $time = time() / self::DEFAULT_TIME_STEP_SEC;
190 foreach ( $ticks as $offset ) {
191 $log_time = $time + $offset;
192 $calculdated = (string) self::calc_totp( $key, $log_time );
193 if ( $calculdated === $authcode ) {
194 return true;
195 }
196 }
197 return false;
198 }
199
200 /**
201 * Calculate a valid code given the shared secret key
202 *
203 * @param string $key The shared secret key to use for calculating code.
204 * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
205 * @param int $digits The number of digits in the returned code.
206 * @param string $hash The hash used to calculate the code.
207 * @param int $time_step The size of the time step.
208 *
209 * @return string The totp code
210 */
211 public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
212
213 $secret = self::base32_decode( $key );
214
215 if ( false === $step_count ) {
216 $step_count = floor( time() / $time_step );
217 }
218
219 $timestamp = self::pack64( $step_count );
220
221 $hash = hash_hmac( $hash, $timestamp, $secret, true );
222
223 $offset = ord( $hash[19] ) & 0xf;
224
225 $code = (
226 ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
227 ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
228 ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
229 ( ord( $hash[ $offset + 3 ] ) & 0xff )
230 ) % pow( 10, $digits );
231
232 return str_pad( $code, $digits, '0', STR_PAD_LEFT );
233 }
234
235 /**
236 * Decode a base32 string and return a binary representation
237 *
238 * @param string $base32_string The base 32 string to decode.
239 *
240 * @throws \Exception If string contains non-base32 characters.
241 *
242 * @return string Binary representation of decoded string
243 */
244 public static function base32_decode( $base32_string ) {
245
246 $base32_string = strtoupper( $base32_string );
247
248 if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
249 throw new \Exception( 'Invalid characters in the base32 string.' );
250 }
251
252 $l = strlen( $base32_string );
253 $n = 0;
254 $j = 0;
255 $binary = '';
256
257 for ( $i = 0; $i < $l; $i++ ) {
258
259 $n = $n << 5; // Move buffer left by 5 to make room.
260 $n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
261 $j += 5; // Keep track of number of bits in buffer.
262
263 if ( $j >= 8 ) {
264 $j -= 8;
265 $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
266 }
267 }
268
269 return $binary;
270 }
271
272 /**
273 * Used with usort to sort an array by distance from 0
274 *
275 * @param int $a First array element.
276 * @param int $b Second array element.
277 *
278 * @return int -1, 0, or 1 as needed by usort
279 */
280 private static function abssort( $a, $b ) {
281 $a = abs( $a );
282 $b = abs( $b );
283 if ( $a === $b ) {
284 return 0;
285 }
286 return ( $a < $b ) ? -1 : 1;
287 }
288
289 /**
290 * Pack stuff
291 *
292 * @param string $value The value to be packed.
293 *
294 * @return string Binary packed string.
295 */
296 public static function pack64( $value ) {
297 // 64bit mode (PHP_INT_SIZE == 8).
298 if ( PHP_INT_SIZE >= 8 ) {
299 // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
300 if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
301 return pack( 'J', $value );
302 }
303 $highmap = 0xffffffff << 32;
304 $higher = ( $value & $highmap ) >> 32;
305 } else {
306 /*
307 * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
308 * and not pack anything beyond it's limits.
309 */
310 $higher = 0;
311 }
312
313 $lowmap = 0xffffffff;
314 $lower = $value & $lowmap;
315
316 return pack( 'NN', $higher, $lower );
317 }
318
319 /**
320 * Generate a random eight-digit string to send out as an auth code.
321 *
322 * @since 0.1-dev
323 *
324 * @param int $length The code length.
325 * @param string|array $chars Valid auth code characters.
326 * @return string
327 */
328 public static function get_code( $length = 8, $chars = '1234567890' ) {
329 $code = '';
330 if ( is_array( $chars ) ) {
331 $chars = implode( '', $chars );
332 }
333 for ( $i = 0; $i < $length; $i++ ) {
334 $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
335 }
336 return $code;
337 }
338
339 /**
340 * Generate the user token.
341 *
342 * @since 0.1-dev
343 *
344 * @param int $user_id User ID.
345 * @return string
346 */
347 public static function generate_token( $user_id ) {
348 $token = self::get_code();
349 update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
350 return $token;
351 }
352
353 /**
354 * Validate the user token.
355 *
356 * @since 0.1-dev
357 *
358 * @param \WP_User $user User ID.
359 * @param string $token User token.
360 * @return boolean
361 */
362 public static function validate_token( $user, $token ) {
363 $user_id = $user->ID;
364 $hashed_token = self::get_user_token( $user_id );
365 // Bail if token is empty or it doesn't match.
366 if ( empty( $hashed_token ) || ( wp_hash( $token ) !== $hashed_token ) ) {
367 self::get_login_attempts_instance()->increase_login_attempts( $user );
368 return false;
369 }
370
371
372 // Ensure that the token can't be re-used.
373 self::delete_token( $user_id );
374 self::get_login_attempts_instance()->clear_login_attempts( $user );
375
376 return true;
377 }
378
379 /**
380 * Delete the user token.
381 *
382 * @since 0.1-dev
383 *
384 * @param int $user_id User ID.
385 */
386 public static function delete_token( $user_id ) {
387 delete_user_meta( $user_id, self::TOKEN_META_KEY );
388 }
389
390 /**
391 * Check if user has a valid token already.
392 *
393 * @param int $user_id User ID.
394 * @return boolean If user has a valid email token.
395 */
396 public static function user_has_token( $user_id ) {
397 $hashed_token = self::get_user_token( $user_id );
398 if ( ! empty( $hashed_token ) ) {
399 return true;
400 } else {
401 return false;
402 }
403 }
404
405 /**
406 * Get the authentication token for the user.
407 *
408 * @param int $user_id User ID.
409 *
410 * @return string|boolean User token or `false` if no token found.
411 */
412 public static function get_user_token( $user_id ) {
413 $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
414
415 if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
416 return $hashed_token;
417 }
418
419 return false;
420 }
421
422 /**
423 * Delete the TOTP secret key for a user.
424 *
425 * @param int $user_id User ID.
426 *
427 * @return boolean If the key was deleted successfully.
428 */
429 public static function delete_user_totp_key( $user_id ) {
430 return delete_user_meta( $user_id, self::SECRET_META_KEY );
431 }
432
433 /**
434 * Returns list of all the auth apps and their properties
435 *
436 * @return array
437 */
438 public static function get_apps(): array {
439 return array(
440 'authy' => array(
441 'logo' => 'authy-logo.png',
442 'hash' => 'authy',
443 'name' => 'Authy',
444 ),
445 'google' => array(
446 'logo' => 'google-logo.png',
447 'hash' => 'google',
448 'name' => 'Google Authenticator',
449 ),
450 'microsoft' => array(
451 'logo' => 'microsoft-logo.png',
452 'hash' => 'microsoft',
453 'name' => 'Microsoft Authenticator',
454 ),
455 'duo' => array(
456 'logo' => 'duo-logo.png',
457 'hash' => 'duo',
458 'name' => 'Duo Security',
459 ),
460 'lastpass' => array(
461 'logo' => 'lastpass-logo.png',
462 'hash' => 'lastpass',
463 'name' => 'LastPass',
464 ),
465 'freeotp' => array(
466 'logo' => 'free-otp-logo.png',
467 'hash' => 'freeotp',
468 'name' => 'FreeOTP',
469 ),
470 'okta' => array(
471 'logo' => 'okta-logo.png',
472 'hash' => 'okta',
473 'name' => 'Okta',
474 ),
475 );
476 }
477
478 /**
479 * Getter for the base32 character set
480 *
481 * @return string
482 *
483 * @since 2.0.0
484 */
485 public static function get_base32_characters(): string {
486 return self::$base_32_chars;
487 }
488
489 /**
490 * Validates base32 encoded string
491 *
492 * @param string $text = The text to be validated.
493 *
494 * @return boolean
495 *
496 * @since 2.0.0
497 */
498 public static function validate_base32_string( string $text ): bool {
499 if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $text, $match ) ) {
500 return false;
501 }
502
503 return true;
504 }
505
506 /**
507 * Checks the given key and decrypts it if necessarily
508 *
509 * @param string $key - The key to check.
510 *
511 * @return string
512 *
513 * @since 2.0.0
514 */
515 public static function decrypt_key_if_needed( string &$key ): string {
516
517 if ( Open_SSL::is_ssl_available() && false !== \strpos( $key, 'ssl_' ) ) {
518 $key = Open_SSL::decrypt( substr( $key, 4 ) );
519 }
520
521 return $key;
522 }
523
524 /**
525 * Returns instance of the LoginAttempts class
526 *
527 * @return LoginAttempts
528 *
529 * @since 2.0.0
530 */
531 public static function get_login_attempts_instance() {
532 if ( null === self::$login_attempts ) {
533
534 self::$login_attempts = new Login_Attempts( self::$login_num_meta_key );
535
536 }
537 return self::$login_attempts;
538 }
539
540 /**
541 * Checks the number of login attempts
542 *
543 * @param \WP_User $user - The user we have to check for.
544 *
545 * @return boolean
546 *
547 * @since 2.0.0
548 */
549 public static function check_number_of_attempts( \WP_User $user ):bool {
550 return self::get_login_attempts_instance()->check_number_of_attempts( $user );
551 }
552 }
553