PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 1.7.0
WP 2FA – Two-factor authentication for WordPress v1.7.0
4.1.0 4.0.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 5 years ago BackupCodes.php 6 years ago Login.php 5 years ago index.php 5 years ago
Authentication.php
421 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\Admin\User;
13 use \WP2FA\WP2FA as WP2FA;
14 use WP2FA\Admin\SettingsPage;
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 private static $_base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
30
31 /**
32 * Constructor.
33 */
34 public function __construct() {
35
36 }
37
38 /**
39 * Gemerate QR code
40 *
41 * @param string $name Username.
42 * @param string $key Auth key.
43 * @param string $title Site title.
44 * @return string QR code URL.
45 */
46 public static function get_google_qr_code( $name, $key, $title = null ) {
47 // Encode to support spaces, question marks and other characters.
48 $name = rawurlencode( $name );
49 $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
50 if ( isset( $title ) ) {
51 $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
52 }
53 return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url;
54 }
55
56 /**
57 * Generates key
58 *
59 * @param int $bitsize Nume of bits to use for key.
60 *
61 * @return string $bitsize long string composed of available base32 chars.
62 */
63 public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
64 $bytes = ceil( $bitsize / 8 );
65 $secret = wp_generate_password( $bytes, true, true );
66
67 return self::base32_encode( $secret );
68 }
69 /**
70 * Returns a base32 encoded string.
71 *
72 * @param string $string String to be encoded using base32.
73 *
74 * @return string base32 encoded string without padding.
75 */
76 public static function base32_encode( $string ) {
77 if ( empty( $string ) ) {
78 return '';
79 }
80
81 $binary_string = '';
82
83 foreach ( str_split( $string ) as $character ) {
84 $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
85 }
86
87 $five_bit_sections = str_split( $binary_string, 5 );
88 $base32_string = '';
89
90 foreach ( $five_bit_sections as $five_bit_section ) {
91 $base32_string .= self::$_base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
92 }
93
94 return $base32_string;
95 }
96
97 /**
98 * Get the TOTP secret key for a user.
99 *
100 * @param int $user_id User ID.
101 *
102 * @return string
103 */
104 public static function get_user_totp_key( $user_id ) {
105 return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
106 }
107
108 /**
109 * Check if the TOTP secret key has a proper format.
110 *
111 * @param string $key TOTP secret key.
112 *
113 * @return boolean
114 */
115 public static function is_valid_key( $key ) {
116 $check = sprintf( '/^[%s]+$/', self::$_base_32_chars );
117
118 if ( 1 === preg_match( $check, $key ) ) {
119 return true;
120 }
121
122 return false;
123 }
124
125 /**
126 * Checks if a given code is valid for a given key, allowing for a certain amount of time drift
127 *
128 * @param string $key The share secret key to use.
129 * @param string $authcode The code to test.
130 *
131 * @return bool Whether the code is valid within the time frame
132 */
133 public static function is_valid_authcode( $key, $authcode ) {
134
135 $max_ticks = apply_filters( 'wp_2fa_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
136
137 // Array of all ticks to allow, sorted using absolute value to test closest match first.
138 $ticks = range( - $max_ticks, $max_ticks );
139 usort( $ticks, array( __CLASS__, 'abssort' ) );
140
141 $time = time() / self::DEFAULT_TIME_STEP_SEC;
142 foreach ( $ticks as $offset ) {
143 $log_time = $time + $offset;
144 $calculdated = (string) self::calc_totp( $key, $log_time );
145 if ( $calculdated === $authcode ) {
146 return true;
147 }
148 }
149 return false;
150 }
151
152 /**
153 * Calculate a valid code given the shared secret key
154 *
155 * @param string $key The shared secret key to use for calculating code.
156 * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
157 * @param int $digits The number of digits in the returned code.
158 * @param string $hash The hash used to calculate the code.
159 * @param int $time_step The size of the time step.
160 *
161 * @return string The totp code
162 */
163 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 ) {
164
165 $secret = self::base32_decode( $key );
166
167 if ( false === $step_count ) {
168 $step_count = floor( time() / $time_step );
169 }
170
171 $timestamp = self::pack64( $step_count );
172
173 $hash = hash_hmac( $hash, $timestamp, $secret, true );
174
175 $offset = ord( $hash[19] ) & 0xf;
176
177 $code = (
178 ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
179 ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
180 ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
181 ( ord( $hash[ $offset + 3 ] ) & 0xff )
182 ) % pow( 10, $digits );
183
184 return str_pad( $code, $digits, '0', STR_PAD_LEFT );
185 }
186
187 /**
188 * Decode a base32 string and return a binary representation
189 *
190 * @param string $base32_string The base 32 string to decode.
191 *
192 * @throws Exception If string contains non-base32 characters.
193 *
194 * @return string Binary representation of decoded string
195 */
196 public static function base32_decode( $base32_string ) {
197
198 $base32_string = strtoupper( $base32_string );
199
200 if ( ! preg_match( '/^[' . self::$_base_32_chars . ']+$/', $base32_string, $match ) ) {
201 throw new \Exception( 'Invalid characters in the base32 string.' );
202 }
203
204 $l = strlen( $base32_string );
205 $n = 0;
206 $j = 0;
207 $binary = '';
208
209 for ( $i = 0; $i < $l; $i++ ) {
210
211 $n = $n << 5; // Move buffer left by 5 to make room.
212 $n = $n + strpos( self::$_base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
213 $j += 5; // Keep track of number of bits in buffer.
214
215 if ( $j >= 8 ) {
216 $j -= 8;
217 $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
218 }
219 }
220
221 return $binary;
222 }
223
224 /**
225 * Used with usort to sort an array by distance from 0
226 *
227 * @param int $a First array element.
228 * @param int $b Second array element.
229 *
230 * @return int -1, 0, or 1 as needed by usort
231 */
232 private static function abssort( $a, $b ) {
233 $a = abs( $a );
234 $b = abs( $b );
235 if ( $a === $b ) {
236 return 0;
237 }
238 return ( $a < $b ) ? -1 : 1;
239 }
240
241 /**
242 * Pack stuff
243 *
244 * @param string $value The value to be packed.
245 *
246 * @return string Binary packed string.
247 */
248 public static function pack64( $value ) {
249 // 64bit mode (PHP_INT_SIZE == 8).
250 if ( PHP_INT_SIZE >= 8 ) {
251 // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
252 if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
253 return pack( 'J', $value );
254 }
255 $highmap = 0xffffffff << 32;
256 $higher = ( $value & $highmap ) >> 32;
257 } else {
258 /*
259 * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
260 * and not pack anything beyond it's limits.
261 */
262 $higher = 0;
263 }
264
265 $lowmap = 0xffffffff;
266 $lower = $value & $lowmap;
267
268 return pack( 'NN', $higher, $lower );
269 }
270
271 /**
272 * Generate a random eight-digit string to send out as an auth code.
273 *
274 * @since 0.1-dev
275 *
276 * @param int $length The code length.
277 * @param string|array $chars Valid auth code characters.
278 * @return string
279 */
280 public static function get_code( $length = 8, $chars = '1234567890' ) {
281 $code = '';
282 if ( is_array( $chars ) ) {
283 $chars = implode( '', $chars );
284 }
285 for ( $i = 0; $i < $length; $i++ ) {
286 $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
287 }
288 return $code;
289 }
290
291 /**
292 * Generate the user token.
293 *
294 * @since 0.1-dev
295 *
296 * @param int $user_id User ID.
297 * @return string
298 */
299 public static function generate_token( $user_id ) {
300 $token = self::get_code();
301 update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
302 return $token;
303 }
304
305 /**
306 * Validate the user token.
307 *
308 * @since 0.1-dev
309 *
310 * @param int $user_id User ID.
311 * @param string $token User token.
312 * @return boolean
313 */
314 public static function validate_token( $user_id, $token ) {
315 $hashed_token = self::get_user_token( $user_id );
316 // Bail if token is empty or it doesn't match.
317 if ( empty( $hashed_token ) || ( wp_hash( $token ) !== $hashed_token ) ) {
318 return false;
319 }
320
321 // Ensure that the token can't be re-used.
322 self::delete_token( $user_id );
323
324 return true;
325 }
326
327 /**
328 * Delete the user token.
329 *
330 * @since 0.1-dev
331 *
332 * @param int $user_id User ID.
333 */
334 public static function delete_token( $user_id ) {
335 delete_user_meta( $user_id, self::TOKEN_META_KEY );
336 }
337
338 /**
339 * Check if user has a valid token already.
340 *
341 * @param int $user_id User ID.
342 * @return boolean If user has a valid email token.
343 */
344 public static function user_has_token( $user_id ) {
345 $hashed_token = self::get_user_token( $user_id );
346 if ( ! empty( $hashed_token ) ) {
347 return true;
348 } else {
349 return false;
350 }
351 }
352
353 /**
354 * Get the authentication token for the user.
355 *
356 * @param int $user_id User ID.
357 *
358 * @return string|boolean User token or `false` if no token found.
359 */
360 public static function get_user_token( $user_id ) {
361 $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
362
363 if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
364 return $hashed_token;
365 }
366
367 return false;
368 }
369
370 /**
371 * Delete the TOTP secret key for a user.
372 *
373 * @param int $user_id User ID.
374 *
375 * @return boolean If the key was deleted successfully.
376 */
377 public static function delete_user_totp_key( $user_id ) {
378 return delete_user_meta( $user_id, self::SECRET_META_KEY );
379 }
380
381 public static function getApps() {
382 return [
383 'authy' => [
384 'logo' => 'authy-logo.png',
385 'hash' => 'authy',
386 'name' => 'Authy'
387 ],
388 'google' => [
389 'logo' => 'google-logo.png',
390 'hash' => 'google',
391 'name' => 'Google Authenticator'
392 ],
393 'microsoft' => [
394 'logo' => 'microsoft-logo.png',
395 'hash' => 'microsoft',
396 'name' => 'Microsoft Authenticator'
397 ],
398 'duo' => [
399 'logo' => 'duo-logo.png',
400 'hash' => 'duo',
401 'name' => 'Duo Security'
402 ],
403 'lastpass' => [
404 'logo' => 'lastpass-logo.png',
405 'hash' => 'lastpass',
406 'name' => 'LastPass'
407 ],
408 'freeotp' => [
409 'logo' => 'free-otp-logo.png',
410 'hash' => 'freeotp',
411 'name' => 'FreeOTP'
412 ],
413 'okta' => [
414 'logo' => 'okta-logo.png',
415 'hash' => 'okta',
416 'name' => 'Okta'
417 ]
418 ];
419 }
420 }
421