PluginProbe
Two Factor Authentication / 1.2.13
Two Factor Authentication v1.2.13
1.12.2 1.13.0 1.14.10 1.14.11 1.14.14 1.14.15 1.14.16 1.14.17 1.14.23 1.14.24 1.14.26 1.14.27 1.14.3 1.14.4 1.14.5 1.14.7 1.14.8 1.15.5 1.16.0 1.2.10 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 All 98 releases
two-factor-authentication / includes / class.TFA.php

class.TFA.php in Two Factor Authentication 1.2.13, at includes/class.TFA.php

599 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('Access denied.');
4
5 class Simba_TFA {
6
7 private $salt_prefix;
8 private $pw_prefix;
9
10 public function __construct($base32_encoder, $otp_helper)
11 {
12 $this->base32_encoder = $base32_encoder;
13 $this->otp_helper = $otp_helper;
14 $this->time_window_size = apply_filters('simbatfa_time_window_size', 30);
15 $this->check_back_time_windows = apply_filters('simbatfa_check_back_time_windows', 2);
16 $this->check_forward_counter_window = apply_filters('simbatfa_check_forward_counter_window', 20);
17 $this->otp_length = 6;
18 $this->emergency_codes_length = 8;
19 $this->salt_prefix = AUTH_SALT;
20 $this->pw_prefix = AUTH_KEY;
21 $this->default_hmac = 'totp';
22 }
23
24 public function generateOTP($user_ID, $key_b64, $length = 6, $counter = false)
25 {
26
27 $length = $length ? (int)$length : 6;
28
29 $key = $this->decryptString($key_b64, $user_ID);
30 $alg = $this->getUserAlgorithm($user_ID);
31
32 if($alg == 'hotp')
33 {
34 $db_counter = $this->getUserCounter($user_ID);
35
36 $counter = $counter ? $counter : $db_counter;
37 $otp_res = $this->otp_helper->generateByCounter($key, $counter);
38 }
39 else
40 {
41 //time() is supposed to be UTC
42 $time = $counter ? $counter : time();
43 $otp_res = $this->otp_helper->generateByTime($key, $this->time_window_size, $time);
44 }
45 $code = $otp_res->toHotp($length);
46
47 return $code;
48 }
49
50 public function generateOTPsForLoginCheck($user_ID, $key_b64)
51 {
52 $key = trim($this->decryptString($key_b64, $user_ID));
53 $alg = $this->getUserAlgorithm($user_ID);
54
55 if($alg == 'totp')
56 $otp_res = $this->otp_helper->generateByTimeWindow($key, $this->time_window_size, -1*$this->check_back_time_windows, 0);
57 elseif($alg == 'hotp')
58 {
59 $counter = $this->getUserCounter($user_ID);
60
61 $otp_res = array();
62 for($i = 0; $i < $this->check_forward_counter_window; $i++)
63 $otp_res[] = $this->otp_helper->generateByCounter($key, ($counter+$i));
64 }
65 return $otp_res;
66 }
67
68
69 public function addPrivateKey($user_ID, $key = false)
70 {
71 //Generate a private key for the user.
72 //To work with Google Authenticator it has to be 10 bytes = 16 chars in base32
73 $code = $key ? $key : strtoupper($this->randString(10));
74
75 //Lets encrypt the key
76 $code = $this->encryptString($code, $user_ID);
77
78 //Add private key to users meta
79 update_user_meta($user_ID, 'tfa_priv_key_64', $code);
80
81 $alg = $this->getUserAlgorithm($user_ID);
82
83 do_action('simba_tfa_adding_private_key', $alg, $user_ID, $code, $this);
84
85 $this->changeUserAlgorithmTo($user_ID, $alg);
86
87 return $code;
88 }
89
90 // Port over keys that were encrypted with mcrypt and its non-compliant padding scheme, so that if the site is ever migrated to a server without mcrypt, they can still be decrypted
91 public function potentially_port_private_keys() {
92
93 $simba_tfa_priv_key_format = get_site_option('simba_tfa_priv_key_format', false);
94
95 $attempts = 0;
96 $successes = 0;
97
98 if ($simba_tfa_priv_key_format < 1 && function_exists('openssl_encrypt')) {
99
100 error_log("TFA: Beginning attempt to port private key encryption over to openssl");
101 global $wpdb;
102 $sql = "SELECT user_id, meta_value FROM ".$wpdb->usermeta." WHERE meta_key = 'tfa_priv_key_64'";
103
104 $user_results = $wpdb->get_results($sql);
105
106 foreach ($user_results as $u) {
107 $dec_openssl = $this->decryptString($u->meta_value, $u->user_id, true);
108
109 $ported = false;
110 if ('' == $dec_openssl) {
111
112 $attempts++;
113
114 $dec_default = $this->decryptString($u->meta_value, $u->user_id);
115
116 if ('' != $dec_default) {
117
118 $enc = $this->encryptString($dec_default, $u->user_id);
119
120 if ($enc) {
121
122 $ported = true;
123 $successes++;
124 update_user_meta($u->user_id, 'tfa_priv_key_64', $enc);
125 }
126 }
127
128 }
129
130 if ($ported) {
131 error_log("TFA: Successfully ported the key for user with ID ".$u->user_id." over to openssl");
132 } else {
133 error_log("TFA: Failed to port the key for user with ID ".$u->user_id." over to openssl");
134 }
135 }
136 if ($attempts == 0 || $successes > 0) update_site_option('simba_tfa_priv_key_format', 1);
137
138 }
139 }
140
141 public function getPrivateKeyPlain($enc, $user_ID)
142 {
143 $dec = $this->decryptString($enc, $user_ID);
144 $this->potentially_port_private_keys();
145 return $dec;
146 }
147
148
149 public function getPanicCodesString($arr, $user_ID)
150 {
151 if(!is_array($arr)) return '<em>'.__('No emergency codes left. Sorry.', 'two-factor-authentication').'</em>';
152
153 $emergency_str = '';
154
155 foreach($arr as $p_code) {
156 $emergency_str .= $this->decryptString($p_code, $user_ID).', ';
157 }
158
159 $emergency_str = rtrim($emergency_str, ', ');
160
161 $emergency_str = $emergency_str ? $emergency_str : '<em>'.__('No emergency codes left. Sorry.', 'two-factor-authentication').'</em>';
162 return $emergency_str;
163 }
164
165 public function preAuth($params)
166 {
167 global $wpdb;
168 $query = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']);
169 $user = $wpdb->get_row($query);
170 if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
171 // Corner-case: login looks like an email, but is a username rather than email address
172 $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']));
173 }
174 $is_activated_for_user = true;
175 $is_activated_by_user = false;
176
177 if($user) {
178 $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true);
179 $is_activated_for_user = $this->isActivatedForUser($user->ID);
180 $is_activated_by_user = $this->isActivatedByUser($user->ID);
181
182 if($is_activated_for_user && $is_activated_by_user)
183 {
184 // $delivery_type = get_user_meta($user->ID, 'simbatfa_delivery_type', true);
185
186 //No private key yet, generate one.
187 //This is safe to do since the code is emailed to the user.
188 //Not safe to do if the user has disabled email.
189 if(!$tfa_priv_key)
190 $tfa_priv_key = $this->addPrivateKey($user->ID);
191
192 $code = $this->generateOTP($user->ID, $tfa_priv_key);
193
194 return true;//Set to true
195 }
196 return false;
197 }
198 return false;
199 }
200
201 public function authUserFromLogin($params)
202 {
203
204 $params = apply_filters('simbatfa_auth_user_from_login_params', $params);
205
206 global $simba_two_factor_authentication, $wpdb;
207
208 if(!$this->isCallerActive($params))
209 return true;
210
211 $field = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? 'user_email' : 'user_login';
212 $query = $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE ".$field."=%s", $params['log']);
213 $response = $wpdb->get_row($query);
214
215 $user_ID = is_object($response) ? $response->ID : false;
216 $user_registered = is_object($response) ? $response->user_registered : false;
217
218 $user_code = trim(@$params['two_factor_code']);
219
220 if(!$user_ID)
221 return true;
222
223 if(!$this->isActivatedForUser($user_ID))
224 return true;
225
226 if(!$this->isActivatedByUser($user_ID)) {
227
228 if (!$this->isRequiredForUser($user_ID)) {
229 return true;
230 }
231
232 $requireafter = absint($simba_two_factor_authentication->get_option('tfa_requireafter')) * 86400;
233
234 $account_age = time() - strtotime($user_registered);
235
236 if ($account_age > $requireafter) {
237 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('The site owner has forbidden you to login without two-factor authentication. Please contact the site owner to re-gain access.', 'two-factor-authentication')));
238 }
239
240 return true;
241 }
242
243 $tfa_creds_user_id = !empty($params['creds_user_id']) ? $params['creds_user_id'] : $user_ID;
244
245 if ($tfa_creds_user_id != $user_ID) {
246
247 // Authenticating using a different user's credentials (e.g. https://wordpress.org/plugins/use-administrator-password/)
248 // In this case, we require that different user to have TFA active - so that this mechanism can't be used to avoid TFA
249
250 if(!$this->isActivatedForUser($tfa_creds_user_id) || !$this->isActivatedByUser($tfa_creds_user_id)) {
251 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login_altuser', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('You are attempting to log in to an account that has two-factor authentication enabled; this requires you to also have two-factor authentication enabled on the account whose credentials you are using.', 'two-factor-authentication')));
252 }
253
254 }
255
256 $tfa_priv_key = get_user_meta($tfa_creds_user_id, 'tfa_priv_key_64', true);
257 // $tfa_last_login = get_user_meta($tfa_creds_user_id, 'tfa_last_login', true); // Unused
258 $tfa_last_pws_arr = get_user_meta($tfa_creds_user_id, 'tfa_last_pws', true);
259 $tfa_last_pws = @$tfa_last_pws_arr ? $tfa_last_pws_arr : array();
260 $alg = $this->getUserAlgorithm($tfa_creds_user_id);
261
262 $current_time_window = intval(time()/30);
263
264 //Give the user 1,5 minutes time span to enter/retrieve the code
265 //Or check $this->check_forward_counter_window number of events if hotp
266 $codes = $this->generateOTPsForLoginCheck($tfa_creds_user_id, $tfa_priv_key);
267
268 //A recently used code was entered.
269 //Not ok
270 if(in_array($this->hash($user_code, $tfa_creds_user_id), $tfa_last_pws))
271 return false;
272
273 $match = false;
274 foreach($codes as $index => $code)
275 {
276 if(trim($code->toHotp(6)) == trim($user_code))
277 {
278 $match = true;
279 $found_index = $index;
280 break;
281 }
282 }
283
284 //Check emergency codes
285 if(!$match)
286 {
287 $emergency_codes = get_user_meta($tfa_creds_user_id, 'simba_tfa_emergency_codes_64', true);
288
289 if(!@$emergency_codes)
290 return $match;
291
292 $dec = array();
293 foreach($emergency_codes as $emergency_code)
294 $dec[] = trim($this->decryptString(trim($emergency_code), $tfa_creds_user_id));
295
296 $in_array = array_search($user_code, $dec);
297 $match = $in_array !== false;
298
299 if($match)//Remove emergency code
300 {
301 array_splice($emergency_codes, $in_array, 1);
302 update_user_meta($tfa_creds_user_id, 'simba_tfa_emergency_codes_64', $emergency_codes);
303 do_action('simba_tfa_emergency_code_used', $tfa_creds_user_id, $emergency_codes);
304 }
305
306 } else {
307 //Add the used code as well so it cant be used again
308 //Keep the two last codes
309 $tfa_last_pws[] = $this->hash($user_code, $tfa_creds_user_id);
310 $nr_of_old_to_save = $alg == 'hotp' ? $this->check_forward_counter_window : $this->check_back_time_windows;
311
312 if(count($tfa_last_pws) > $nr_of_old_to_save)
313 array_splice($tfa_last_pws, 0, 1);
314
315 update_user_meta($tfa_creds_user_id, 'tfa_last_pws', $tfa_last_pws);
316 }
317
318 if($match)
319 {
320 //Save the time window when the last successful login took place
321 update_user_meta($tfa_creds_user_id, 'tfa_last_login', $current_time_window);
322
323 //Update the counter if HOTP was used
324 if($alg == 'hotp')
325 {
326 $counter = $this->getUserCounter($tfa_creds_user_id);
327
328 $enc_new_counter = $this->encryptString($counter+1, $tfa_creds_user_id);
329 update_user_meta($tfa_creds_user_id, 'tfa_hotp_counter', $enc_new_counter);
330
331 if($found_index > 10)
332 update_user_meta($tfa_creds_user_id, 'tfa_hotp_off_sync', 1);
333 }
334 }
335
336 return $match;
337
338 }
339
340 public function getUserCounter($user_ID)
341 {
342 $enc_counter = get_user_meta($user_ID, 'tfa_hotp_counter', true);
343
344 if($enc_counter)
345 $counter = $this->decryptString(trim($enc_counter), $user_ID);
346 else
347 return '';
348
349 return trim($counter);
350 }
351
352 public function changeUserAlgorithmTo($user_id, $new_algorithm)
353 {
354 update_user_meta($user_id, 'tfa_algorithm_type', $new_algorithm);
355 delete_user_meta($user_id, 'tfa_hotp_off_sync');
356
357 $counter_start = rand(13, 999999999);
358 $enc_counter_start = $this->encryptString($counter_start, $user_id);
359
360 if($new_algorithm == 'hotp')
361 update_user_meta($user_id, 'tfa_hotp_counter', $enc_counter_start);
362 else
363 delete_user_meta($user_id, 'tfa_hotp_counter');
364 }
365
366 //Added
367 public function changeEnableTFA($user_id, $setting)
368 {
369 $setting = ($setting === 'true') ? 1 : 0;
370
371 update_user_meta($user_id, 'tfa_enable_tfa', $setting);
372 }
373
374 public function getUserAlgorithm($user_id)
375 {
376 global $simba_two_factor_authentication;
377 $setting = get_user_meta($user_id, 'tfa_algorithm_type', true);
378 $default_hmac = $simba_two_factor_authentication->get_option('tfa_default_hmac');
379 $default_hmac = $default_hmac ? $default_hmac : $this->default_hmac;
380
381 $setting = $setting === false || !$setting ? $default_hmac : $setting;
382 return $setting;
383 }
384
385 public function isActivatedForUser($user_id)
386 {
387
388 if (empty($user_id)) return false;
389
390 global $simba_two_factor_authentication;
391
392 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
393 if (is_multisite() && is_super_admin($user_id)) {
394 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
395 $role = '_super_admin';
396 $db_val = $simba_two_factor_authentication->get_option('tfa_'.$role);
397 $db_val = $db_val === false || $db_val ? 1 : 0; //Nothing saved or > 0 returns 1;
398
399 return ($db_val) ? true : false;
400 }
401
402 $user = new WP_User($user_id);
403
404 foreach($user->roles as $role)
405 {
406 $db_val = $simba_two_factor_authentication->get_option('tfa_'.$role);
407 $db_val = $db_val === false || $db_val ? 1 : 0; //Nothing saved or > 0 returns 1;
408
409 if($db_val)
410 return true;
411 }
412
413 return false;
414
415 }
416
417 // N.B. - This doesn't check isActivatedForUser() - the caller would normally want to do that first
418 public function isRequiredForUser($user_id)
419 {
420
421 if (empty($user_id)) return false;
422
423 global $simba_two_factor_authentication;
424
425 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
426 if (is_multisite() && is_super_admin($user_id)) {
427 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
428 $role = '_super_admin';
429 $db_val = $simba_two_factor_authentication->get_option('tfa_required_'.$role);
430
431 return ($db_val) ? true : false;
432 }
433
434 $user = new WP_User($user_id);
435
436 foreach($user->roles as $role)
437 {
438 $db_val = $simba_two_factor_authentication->get_option('tfa_required_'.$role);
439
440 if($db_val)
441 return true;
442 }
443
444 return false;
445
446 }
447
448 //Added
449 public function isActivatedByUser($user_id){
450 $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true);
451 $enabled = empty($enabled) ? false : true;
452
453 return $enabled;
454 }
455
456 // Disabled: unused
457 // public function saveCallerStatus($caller_id, $status)
458 // {
459 // global $simba_two_factor_authentication;
460 // if($caller_id == 'xmlrpc')
461 // $simba_two_factor_authentication->set_option('tfa_xmlrpc_on', $status);
462 // }
463
464 private function isCallerActive($params)
465 {
466
467 if(!preg_match('/(\/xmlrpc\.php)$/', trim($params['caller'])))
468 return true;
469
470 global $simba_two_factor_authentication;
471 $saved_data = $simba_two_factor_authentication->get_option('tfa_xmlrpc_on');
472
473 if($saved_data)
474 return true;
475
476 return false;
477 }
478
479 private function get_iv_size() {
480 // mcrypt first, for backwards compatibility
481 if (function_exists('mcrypt_get_iv_size')) {
482 return mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
483 } elseif (function_exists('openssl_cipher_iv_length')) {
484 return openssl_cipher_iv_length('AES-128-CBC');
485 }
486 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
487 }
488
489 private function create_iv($iv_size) {
490 if (function_exists('mcrypt_create_iv')) {
491 return mcrypt_create_iv($iv_size, MCRYPT_RAND);
492 } elseif (function_exists('openssl_random_pseudo_bytes')) {
493 return openssl_random_pseudo_bytes($iv_size);
494 }
495 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
496 }
497
498 private function encrypt($key, $string, $iv) {
499 // Prefer OpenSSL, because it uses correct padding, and its output can be decrypted by mcrypt - whereas, the converse is not true
500 if (function_exists('openssl_encrypt')) {
501 return openssl_encrypt($string, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
502 } elseif (function_exists('mcrypt_encrypt')) {
503 return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv);
504 }
505 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
506 }
507
508 private function decrypt($key, $enc, $iv, $force_openssl = false) {
509 // Prefer mcrypt, because it can decrypt the output of both mcrypt_encrypt() and openssl_decrypt(), whereas (because of mcrypt_encrypt() using bad padding), the converse is not true
510 if (function_exists('mcrypt_decrypt') && !$force_openssl) {
511 return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv);
512 } elseif (function_exists('openssl_decrypt')) {
513 $decrypted = openssl_decrypt($enc, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
514 if (false === $decrypted && !$force_openssl) { error_log("TFA decryption failure: was your site migrated to a server without mcrypt? You may need to install mcrypt, or disable TFA, in order to successfully decrypt data that was previously encrypted with mcrypt."); }
515 return $decrypted;
516 }
517 if ($force_openssl) return false;
518 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
519 }
520
521 public function encryptString($string, $salt_suffix)
522 {
523 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
524
525 $iv_size = $this->get_iv_size();
526 $iv = $this->create_iv($iv_size);
527
528 $enc = $this->encrypt($key, $string, $iv);
529
530 if (false === $enc) return false;
531
532 $enc = $iv.$enc;
533 $enc_b64 = base64_encode($enc);
534 return $enc_b64;
535 }
536
537 private function decryptString($enc_b64, $salt_suffix, $force_openssl = false)
538 {
539 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
540
541 $iv_size = $this->get_iv_size();
542 $enc_conc = base64_decode($enc_b64);
543
544 $iv = substr($enc_conc, 0, $iv_size);
545 $enc = substr($enc_conc, $iv_size);
546
547 $string = $this->decrypt($key, $enc, $iv, $force_openssl);
548
549 // Remove padding bytes
550 return rtrim($string, "\x00..\x1F");
551 }
552
553 private function hashAndBin($pw, $salt)
554 {
555 $key = $this->hash($pw, $salt);
556 $key = pack('H*', $key);
557 // Yes: it's a null encryption key. See: https://wordpress.org/support/topic/warning-mcrypt_decrypt-key-of-size-0-not-supported-by-this-algorithm-only-k?replies=5#post-6806922
558 // Basically: the original plugin had a bug here, which caused a null encryption key. This fails on PHP 5.6+. But, fixing it would break backwards compatibility for existing installs - and note that the only unknown once you have access to the encrypted data is the AUTH_SALT and AUTH_KEY constants... which means that actually the intended encryption was non-portable, + problematic if you lose your wp-config.php or try to migrate data to another site, or changes these values. (Normally changing these values only causes a compulsory re-log-in - but with the intended encryption in the original author's plugin, it'd actually cause a permanent lock-out until you disabled his plugin). If someone has read-access to the database, then it'd be reasonable to assume they have read-access to wp-config.php too: or at least, the number of attackers who can do one and not the other would be small. The "encryption's" not worth it.
559 // In summary: this isn't encryption, and is not intended to be.
560 return str_repeat(chr(0), 16);
561 }
562
563 private function hash($pw, $salt)
564 {
565 //$hash = hash_pbkdf2('sha256', $pw, $salt, 10);
566 //$hash = crypt($pw, '$5$'.$salt.'$');
567 $hash = md5($salt.$pw);
568 return $hash;
569 }
570
571 private function randString($len = 6)
572 {
573 $chars = '23456789QWERTYUPASDFGHJKLZXCVBNM';
574 $chars = str_split($chars);
575 shuffle($chars);
576 $code = implode('', array_splice($chars, 0, $len));
577
578 return $code;
579 }
580
581 public function setUserHMACTypes()
582 {
583 //We need this because we dont want to change third party apps users algorithm
584 $users = get_users(array('meta_key' => 'simbatfa_delivery_type', 'meta_value' => 'third-party-apps'));
585 if(!empty($users))
586 {
587 foreach($users as $user)
588 {
589 $tfa_algorithm_type = get_user_meta($user->ID, 'tfa_algorithm_type', true);
590 if($tfa_algorithm_type)
591 continue;
592
593 update_user_meta($user->ID, 'tfa_algorithm_type', $this->getUserAlgorithm($user->ID));
594 }
595 }
596 }
597
598 }
599