PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 1.4.1
WP 2FA – Two-factor authentication for WordPress v1.4.1
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 5 years ago BackupCodes.php 6 years ago Login.php 5 years ago
Authentication.php
549 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 \WP2FA\WP2FA as WP2FA;
13
14 /**
15 * Authenticator class
16 */
17 class Authentication {
18
19 const SECRET_META_KEY = 'wp_2fa_totp_key';
20 const NOTICES_META_KEY = 'wp_2fa_totp_notices';
21 const TOKEN_META_KEY = 'wp_2fa_email_token';
22 const DEFAULT_KEY_BIT_SIZE = 160;
23 const DEFAULT_CRYPTO = 'sha1';
24 const DEFAULT_DIGIT_COUNT = 6;
25 const DEFAULT_TIME_STEP_SEC = 30;
26 const DEFAULT_TIME_STEP_ALLOWANCE = 4;
27 private static $_base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
28
29 /**
30 * Constructor.
31 */
32 public function __construct() {
33
34 }
35
36 /**
37 * Gemerate QR code
38 *
39 * @param string $name Username.
40 * @param string $key Auth key.
41 * @param string $title Site title.
42 * @return string QR code URL.
43 */
44 public static function get_google_qr_code( $name, $key, $title = null ) {
45 // Encode to support spaces, question marks and other characters.
46 $name = rawurlencode( $name );
47 $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
48 if ( isset( $title ) ) {
49 $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
50 }
51 return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url;
52 }
53
54 /**
55 * Generates key
56 *
57 * @param int $bitsize Nume of bits to use for key.
58 *
59 * @return string $bitsize long string composed of available base32 chars.
60 */
61 public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
62 $bytes = ceil( $bitsize / 8 );
63 $secret = wp_generate_password( $bytes, true, true );
64
65 return self::base32_encode( $secret );
66 }
67 /**
68 * Returns a base32 encoded string.
69 *
70 * @param string $string String to be encoded using base32.
71 *
72 * @return string base32 encoded string without padding.
73 */
74 public static function base32_encode( $string ) {
75 if ( empty( $string ) ) {
76 return '';
77 }
78
79 $binary_string = '';
80
81 foreach ( str_split( $string ) as $character ) {
82 $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
83 }
84
85 $five_bit_sections = str_split( $binary_string, 5 );
86 $base32_string = '';
87
88 foreach ( $five_bit_sections as $five_bit_section ) {
89 $base32_string .= self::$_base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
90 }
91
92 return $base32_string;
93 }
94
95 /**
96 * Get the TOTP secret key for a user.
97 *
98 * @param int $user_id User ID.
99 *
100 * @return string
101 */
102 public static function get_user_totp_key( $user_id ) {
103 return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
104 }
105
106 /**
107 * Check if the TOTP secret key has a proper format.
108 *
109 * @param string $key TOTP secret key.
110 *
111 * @return boolean
112 */
113 public static function is_valid_key( $key ) {
114 $check = sprintf( '/^[%s]+$/', self::$_base_32_chars );
115
116 if ( 1 === preg_match( $check, $key ) ) {
117 return true;
118 }
119
120 return false;
121 }
122
123 /**
124 * Checks if a given code is valid for a given key, allowing for a certain amount of time drift
125 *
126 * @param string $key The share secret key to use.
127 * @param string $authcode The code to test.
128 *
129 * @return bool Whether the code is valid within the time frame
130 */
131 public static function is_valid_authcode( $key, $authcode ) {
132
133 $max_ticks = apply_filters( 'wp_2fa_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
134
135 // Array of all ticks to allow, sorted using absolute value to test closest match first.
136 $ticks = range( - $max_ticks, $max_ticks );
137 usort( $ticks, array( __CLASS__, 'abssort' ) );
138
139 $time = time() / self::DEFAULT_TIME_STEP_SEC;
140 foreach ( $ticks as $offset ) {
141 $log_time = $time + $offset;
142 $calculdated = (string) self::calc_totp( $key, $log_time );
143 if ( $calculdated === $authcode ) {
144 return true;
145 }
146 }
147 return false;
148 }
149
150 /**
151 * Calculate a valid code given the shared secret key
152 *
153 * @param string $key The shared secret key to use for calculating code.
154 * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
155 * @param int $digits The number of digits in the returned code.
156 * @param string $hash The hash used to calculate the code.
157 * @param int $time_step The size of the time step.
158 *
159 * @return string The totp code
160 */
161 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 ) {
162
163 $secret = self::base32_decode( $key );
164
165 if ( false === $step_count ) {
166 $step_count = floor( time() / $time_step );
167 }
168
169 $timestamp = self::pack64( $step_count );
170
171 $hash = hash_hmac( $hash, $timestamp, $secret, true );
172
173 $offset = ord( $hash[19] ) & 0xf;
174
175 $code = (
176 ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
177 ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
178 ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
179 ( ord( $hash[ $offset + 3 ] ) & 0xff )
180 ) % pow( 10, $digits );
181
182 return str_pad( $code, $digits, '0', STR_PAD_LEFT );
183 }
184
185 /**
186 * Decode a base32 string and return a binary representation
187 *
188 * @param string $base32_string The base 32 string to decode.
189 *
190 * @throws Exception If string contains non-base32 characters.
191 *
192 * @return string Binary representation of decoded string
193 */
194 public static function base32_decode( $base32_string ) {
195
196 $base32_string = strtoupper( $base32_string );
197
198 if ( ! preg_match( '/^[' . self::$_base_32_chars . ']+$/', $base32_string, $match ) ) {
199 throw new \Exception( 'Invalid characters in the base32 string.' );
200 }
201
202 $l = strlen( $base32_string );
203 $n = 0;
204 $j = 0;
205 $binary = '';
206
207 for ( $i = 0; $i < $l; $i++ ) {
208
209 $n = $n << 5; // Move buffer left by 5 to make room.
210 $n = $n + strpos( self::$_base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
211 $j += 5; // Keep track of number of bits in buffer.
212
213 if ( $j >= 8 ) {
214 $j -= 8;
215 $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
216 }
217 }
218
219 return $binary;
220 }
221
222 /**
223 * Used with usort to sort an array by distance from 0
224 *
225 * @param int $a First array element.
226 * @param int $b Second array element.
227 *
228 * @return int -1, 0, or 1 as needed by usort
229 */
230 private static function abssort( $a, $b ) {
231 $a = abs( $a );
232 $b = abs( $b );
233 if ( $a === $b ) {
234 return 0;
235 }
236 return ( $a < $b ) ? -1 : 1;
237 }
238
239 /**
240 * Pack stuff
241 *
242 * @param string $value The value to be packed.
243 *
244 * @return string Binary packed string.
245 */
246 public static function pack64( $value ) {
247 // 64bit mode (PHP_INT_SIZE == 8).
248 if ( PHP_INT_SIZE >= 8 ) {
249 // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
250 if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
251 return pack( 'J', $value );
252 }
253 $highmap = 0xffffffff << 32;
254 $higher = ( $value & $highmap ) >> 32;
255 } else {
256 /*
257 * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
258 * and not pack anything beyond it's limits.
259 */
260 $higher = 0;
261 }
262
263 $lowmap = 0xffffffff;
264 $lower = $value & $lowmap;
265
266 return pack( 'NN', $higher, $lower );
267 }
268
269 /**
270 * Generate a random eight-digit string to send out as an auth code.
271 *
272 * @since 0.1-dev
273 *
274 * @param int $length The code length.
275 * @param string|array $chars Valid auth code characters.
276 * @return string
277 */
278 public static function get_code( $length = 8, $chars = '1234567890' ) {
279 $code = '';
280 if ( is_array( $chars ) ) {
281 $chars = implode( '', $chars );
282 }
283 for ( $i = 0; $i < $length; $i++ ) {
284 $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
285 }
286 return $code;
287 }
288
289 /**
290 * Generate the user token.
291 *
292 * @since 0.1-dev
293 *
294 * @param int $user_id User ID.
295 * @return string
296 */
297 public static function generate_token( $user_id ) {
298 $token = self::get_code();
299 update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
300 return $token;
301 }
302
303 /**
304 * Validate the user token.
305 *
306 * @since 0.1-dev
307 *
308 * @param int $user_id User ID.
309 * @param string $token User token.
310 * @return boolean
311 */
312 public static function validate_token( $user_id, $token ) {
313 $hashed_token = self::get_user_token( $user_id );
314 // Bail if token is empty or it doesn't match.
315 if ( empty( $hashed_token ) || ( wp_hash( $token ) !== $hashed_token ) ) {
316 return false;
317 }
318
319 // Ensure that the token can't be re-used.
320 self::delete_token( $user_id );
321
322 return true;
323 }
324
325 /**
326 * Delete the user token.
327 *
328 * @since 0.1-dev
329 *
330 * @param int $user_id User ID.
331 */
332 public static function delete_token( $user_id ) {
333 delete_user_meta( $user_id, self::TOKEN_META_KEY );
334 }
335
336 /**
337 * Check if user has a valid token already.
338 *
339 * @param int $user_id User ID.
340 * @return boolean If user has a valid email token.
341 */
342 public static function user_has_token( $user_id ) {
343 $hashed_token = self::get_user_token( $user_id );
344 if ( ! empty( $hashed_token ) ) {
345 return true;
346 } else {
347 return false;
348 }
349 }
350
351 /**
352 * Get the authentication token for the user.
353 *
354 * @param int $user_id User ID.
355 *
356 * @return string|boolean User token or `false` if no token found.
357 */
358 public static function get_user_token( $user_id ) {
359 $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
360
361 if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
362 return $hashed_token;
363 }
364
365 return false;
366 }
367
368 /**
369 * Delete the TOTP secret key for a user.
370 *
371 * @param int $user_id User ID.
372 *
373 * @return boolean If the key was deleted successfully.
374 */
375 public static function delete_user_totp_key( $user_id ) {
376 return delete_user_meta( $user_id, self::SECRET_META_KEY );
377 }
378
379 /**
380 * Is user eligible for 2FA.
381 *
382 * @param int $user_id User id.
383 * @param string $current_policy Specific policy to check against.
384 */
385 public static function is_user_eligible_for_2fa( $user_id, $current_policy = '', $excluded_users = '', $excluded_roles = '', $enforced_users = '', $enforced_roles = '' ) {
386 if ( isset( $_GET['user_id'] ) ) {
387 $user_id = (int) $_GET['user_id'];
388 $user = get_user_by( 'id', $user_id );
389 $user_roles = $user->roles;
390 } elseif ( isset( $user_id ) ) {
391 $user = get_user_by( 'id', $user_id );
392 $user_roles = $user->roles;
393 } else {
394 $user = wp_get_current_user();
395 $user_roles = $user->roles;
396 }
397
398 if ( $current_policy ) {
399 $current_policy = $current_policy;
400 } else {
401 $current_policy = WP2FA::get_wp2fa_setting( 'enforcment-policy' );
402 }
403
404 $enabled_method = get_user_meta( $user->ID, 'wp_2fa_enabled_methods', true );
405 $user_eligable = false;
406
407 // Lets check the policy settings and if the user has setup totp/email by checking for the usermeta.
408 if ( 'all-users' === $current_policy && empty( $enabled_method ) ) {
409
410 if ( isset( $excluded_users ) ) {
411 $excluded_users = $excluded_users;
412 } else {
413 $excluded_users = WP2FA::get_wp2fa_setting( 'excluded_users' );
414 }
415
416 if ( ! empty( $excluded_users ) ) {
417 // Turn it into an array.
418 $excluded_users_array = explode( ',', $excluded_users );
419 // Compare our roles with the users and see if we get a match.
420 $result = in_array( $user->user_login, $excluded_users_array, true );
421 if ( ! $result ) {
422 $user_eligable = true;
423 }
424 }
425
426 if ( isset( $excluded_roles ) ) {
427 $excluded_roles = $excluded_roles;
428 } else {
429 $excluded_roles = WP2FA::get_wp2fa_setting( 'excluded_roles' );
430 }
431
432 if ( ! empty( $excluded_roles ) ) {
433 // Turn it into an array.
434 $excluded_roles_array = explode( ',', strtolower( $excluded_roles ) );
435 // Compare our roles with the users and see if we get a match.
436 $result = array_intersect( $excluded_roles_array, $user->roles );
437
438 if ( ! empty( $result ) ) {
439 $user_eligable = true;
440 }
441
442 if ( WP2FA::is_this_multisite() ) {
443 $users_caps = array();
444 $subsites = get_sites();
445 // Check each site and add to our array so we know each users actual roles.
446 foreach ( $subsites as $subsite ) {
447 $subsite_id = get_object_vars( $subsite )['blog_id'];
448 $users_caps[] = get_user_meta( $user->ID, 'wp_' .$subsite_id .'_capabilities', true );
449 }
450 // Strip the top layer ready.
451 $users_caps = $users_caps;
452 foreach ( $users_caps as $key => $value ) {
453 if ( ! empty( $value ) ) {
454 foreach ( $value as $key => $value ) {
455 $result = in_array( $key, $excluded_roles_array, true );
456 }
457 }
458 }
459 if ( ! empty( $result ) ) {
460 return false;
461 }
462 }
463 }
464
465 if ( true === $user_eligable || empty( $enabled_method ) ) {
466 return true;
467 }
468 } elseif ( 'certain-roles-only' === $current_policy && empty( $enabled_method ) ) {
469
470 if ( isset( $enforced_users ) && ! empty( $enforced_users ) ) {
471 $enforced_users = $enforced_users;
472 } else {
473 $enforced_users = WP2FA::get_wp2fa_setting( 'enforced_users' );
474 }
475
476 if ( ! empty( $enforced_users ) ) {
477 // Turn it into an array.
478 $enforced_users_array = explode( ',', $enforced_users );
479 // Compare our roles with the users and see if we get a match.
480 $result = in_array( $user->user_login, $enforced_users_array, true );
481 // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag.
482 if ( ! empty( $result ) ) {
483 return true;
484 }
485 }
486
487 if ( isset( $enforced_roles ) && ! empty( $enforced_roles ) ) {
488 $enforced_roles = $enforced_roles;
489 } else {
490 $enforced_roles = WP2FA::get_wp2fa_setting( 'enforced_roles' );
491 }
492
493 if ( ! empty( $enforced_roles ) ) {
494 // Turn it into an array.
495 $enforced_roles_array = explode( ',', strtolower( $enforced_roles ) );
496 // Compare our roles with the users and see if we get a match.
497 $result = array_intersect( $enforced_roles_array, $user->roles );
498 // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag.
499 if ( ! empty( $result ) ) {
500 return true;
501 }
502
503 if ( WP2FA::is_this_multisite() ) {
504 $users_caps = array();
505 $subsites = get_sites();
506 // Check each site and add to our array so we know each users actual roles.
507 foreach ( $subsites as $subsite ) {
508 $subsite_id = get_object_vars( $subsite )['blog_id'];
509 $users_caps[] = get_user_meta( $user->ID, 'wp_' .$subsite_id .'_capabilities', true );
510 }
511 // Strip the top layer ready.
512 $users_caps = $users_caps;
513 foreach ( $users_caps as $key => $value ) {
514 if ( ! empty( $value ) ) {
515 foreach ( $value as $key => $value ) {
516 $result = in_array( $key, $enforced_roles_array, true );
517 }
518 }
519 }
520 if ( ! empty( $result ) ) {
521 return true;
522 }
523 }
524 }
525
526 } elseif ( 'certain-users-only' === $current_policy && empty( $enabled_method ) ) {
527
528 if ( isset( $enforced_users ) && ! empty( $enforced_users ) ) {
529 $enforced_users = $enforced_users;
530 } else {
531 $enforced_users = WP2FA::get_wp2fa_setting( 'enforced_users' );
532 }
533
534 if ( ! empty( $enforced_users ) ) {
535 // Turn it into an array.
536 $enforced_users_array = explode( ',', $enforced_users );
537 // Compare our roles with the users and see if we get a match.
538 $result = in_array( $user->user_login, $enforced_users_array, true );
539 // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag.
540 if ( ! empty( $result ) ) {
541 return true;
542 }
543 }
544 }
545
546 return false;
547 }
548 }
549