PluginProbe
Two Factor Authentication / 1.2.10
Two Factor Authentication v1.2.10
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.10, at includes/class.TFA.php

584 lines 18.8 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.', SIMBA_TFA_TEXT_DOMAIN).'</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.', SIMBA_TFA_TEXT_DOMAIN).'</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 global $simba_two_factor_authentication, $wpdb;
205
206 if(!$this->isCallerActive($params))
207 return true;
208
209 $field = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? 'user_email' : 'user_login';
210 $query = $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE ".$field."=%s", $params['log']);
211 $response = $wpdb->get_row($query);
212
213 $user_ID = is_object($response) ? $response->ID : false;
214 $user_registered = is_object($response) ? $response->user_registered : false;
215
216 $user_code = trim(@$params['two_factor_code']);
217
218 if(!$user_ID)
219 return true;
220
221 if(!$this->isActivatedForUser($user_ID))
222 return true;
223
224 if(!$this->isActivatedByUser($user_ID)) {
225
226 if (!$this->isRequiredForUser($user_ID)) {
227 return true;
228 }
229
230 $requireafter = absint($simba_two_factor_authentication->get_option('tfa_requireafter')) * 86400;
231
232 $account_age = time() - strtotime($user_registered);
233
234 if ($account_age > $requireafter) {
235 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login', '<strong>'.__('Error:', SIMBA_TFA_TEXT_DOMAIN).'</strong> '.__('The site owner has forbidden you to login without two-factor authentication. Please contact the site owner to re-gain access.', SIMBA_TFA_TEXT_DOMAIN)));
236 }
237
238 return true;
239 }
240
241 $tfa_priv_key = get_user_meta($user_ID, 'tfa_priv_key_64', true);
242 $tfa_last_login = get_user_meta($user_ID, 'tfa_last_login', true);
243 $tfa_last_pws_arr = get_user_meta($user_ID, 'tfa_last_pws', true);
244 $tfa_last_pws = @$tfa_last_pws_arr ? $tfa_last_pws_arr : array();
245 $alg = $this->getUserAlgorithm($user_ID);
246
247 $current_time_window = intval(time()/30);
248
249 //Give the user 1,5 minutes time span to enter/retrieve the code
250 //Or check $this->check_forward_counter_window number of events if hotp
251 $codes = $this->generateOTPsForLoginCheck($user_ID, $tfa_priv_key);
252
253 //A recently used code was entered.
254 //Not ok
255 if(in_array($this->hash($user_code, $user_ID), $tfa_last_pws))
256 return false;
257
258 $match = false;
259 foreach($codes as $index => $code)
260 {
261 if(trim($code->toHotp(6)) == trim($user_code))
262 {
263 $match = true;
264 $found_index = $index;
265 break;
266 }
267 }
268
269 //Check emergency codes
270 if(!$match)
271 {
272 $emergency_codes = get_user_meta($user_ID, 'simba_tfa_emergency_codes_64', true);
273
274 if(!@$emergency_codes)
275 return $match;
276
277 $dec = array();
278 foreach($emergency_codes as $emergency_code)
279 $dec[] = trim($this->decryptString(trim($emergency_code), $user_ID));
280
281 $in_array = array_search($user_code, $dec);
282 $match = $in_array !== false;
283
284 if($match)//Remove emergency code
285 {
286 array_splice($emergency_codes, $in_array, 1);
287 update_user_meta($user_ID, 'simba_tfa_emergency_codes_64', $emergency_codes);
288 do_action('simba_tfa_emergency_code_used', $user_ID, $emergency_codes);
289 }
290
291 } else {
292 //Add the used code as well so it cant be used again
293 //Keep the two last codes
294 $tfa_last_pws[] = $this->hash($user_code, $user_ID);
295 $nr_of_old_to_save = $alg == 'hotp' ? $this->check_forward_counter_window : $this->check_back_time_windows;
296
297 if(count($tfa_last_pws) > $nr_of_old_to_save)
298 array_splice($tfa_last_pws, 0, 1);
299
300 update_user_meta($user_ID, 'tfa_last_pws', $tfa_last_pws);
301 }
302
303 if($match)
304 {
305 //Save the time window when the last successful login took place
306 update_user_meta($user_ID, 'tfa_last_login', $current_time_window);
307
308 //Update the counter if HOTP was used
309 if($alg == 'hotp')
310 {
311 $counter = $this->getUserCounter($user_ID);
312
313 $enc_new_counter = $this->encryptString($counter+1, $user_ID);
314 update_user_meta($user_ID, 'tfa_hotp_counter', $enc_new_counter);
315
316 if($found_index > 10)
317 update_user_meta($user_ID, 'tfa_hotp_off_sync', 1);
318 }
319 }
320
321 return $match;
322
323 }
324
325 public function getUserCounter($user_ID)
326 {
327 $enc_counter = get_user_meta($user_ID, 'tfa_hotp_counter', true);
328
329 if($enc_counter)
330 $counter = $this->decryptString(trim($enc_counter), $user_ID);
331 else
332 return '';
333
334 return trim($counter);
335 }
336
337 public function changeUserAlgorithmTo($user_id, $new_algorithm)
338 {
339 update_user_meta($user_id, 'tfa_algorithm_type', $new_algorithm);
340 delete_user_meta($user_id, 'tfa_hotp_off_sync');
341
342 $counter_start = rand(13, 999999999);
343 $enc_counter_start = $this->encryptString($counter_start, $user_id);
344
345 if($new_algorithm == 'hotp')
346 update_user_meta($user_id, 'tfa_hotp_counter', $enc_counter_start);
347 else
348 delete_user_meta($user_id, 'tfa_hotp_counter');
349 }
350
351 //Added
352 public function changeEnableTFA($user_id, $setting)
353 {
354 $setting = ($setting === 'true') ? 1 : 0;
355
356 update_user_meta($user_id, 'tfa_enable_tfa', $setting);
357 }
358
359 public function getUserAlgorithm($user_id)
360 {
361 global $simba_two_factor_authentication;
362 $setting = get_user_meta($user_id, 'tfa_algorithm_type', true);
363 $default_hmac = $simba_two_factor_authentication->get_option('tfa_default_hmac');
364 $default_hmac = $default_hmac ? $default_hmac : $this->default_hmac;
365
366 $setting = $setting === false || !$setting ? $default_hmac : $setting;
367 return $setting;
368 }
369
370 public function isActivatedForUser($user_id)
371 {
372
373 if (empty($user_id)) return false;
374
375 global $simba_two_factor_authentication;
376
377 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
378 if (is_multisite() && is_super_admin($user_id)) {
379 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
380 $role = '_super_admin';
381 $db_val = $simba_two_factor_authentication->get_option('tfa_'.$role);
382 $db_val = $db_val === false || $db_val ? 1 : 0; //Nothing saved or > 0 returns 1;
383
384 return ($db_val) ? true : false;
385 }
386
387 $user = new WP_User($user_id);
388
389 foreach($user->roles as $role)
390 {
391 $db_val = $simba_two_factor_authentication->get_option('tfa_'.$role);
392 $db_val = $db_val === false || $db_val ? 1 : 0; //Nothing saved or > 0 returns 1;
393
394 if($db_val)
395 return true;
396 }
397
398 return false;
399
400 }
401
402 // N.B. - This doesn't check isActivatedForUser() - the caller would normally want to do that first
403 public function isRequiredForUser($user_id)
404 {
405
406 if (empty($user_id)) return false;
407
408 global $simba_two_factor_authentication;
409
410 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
411 if (is_multisite() && is_super_admin($user_id)) {
412 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
413 $role = '_super_admin';
414 $db_val = $simba_two_factor_authentication->get_option('tfa_required_'.$role);
415
416 return ($db_val) ? true : false;
417 }
418
419 $user = new WP_User($user_id);
420
421 foreach($user->roles as $role)
422 {
423 $db_val = $simba_two_factor_authentication->get_option('tfa_required_'.$role);
424
425 if($db_val)
426 return true;
427 }
428
429 return false;
430
431 }
432
433 //Added
434 public function isActivatedByUser($user_id){
435 $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true);
436 $enabled = empty($enabled) ? false : true;
437
438 return $enabled;
439 }
440
441 // Disabled: unused
442 // public function saveCallerStatus($caller_id, $status)
443 // {
444 // global $simba_two_factor_authentication;
445 // if($caller_id == 'xmlrpc')
446 // $simba_two_factor_authentication->set_option('tfa_xmlrpc_on', $status);
447 // }
448
449 private function isCallerActive($params)
450 {
451
452 if(!preg_match('/(\/xmlrpc\.php)$/', trim($params['caller'])))
453 return true;
454
455 global $simba_two_factor_authentication;
456 $saved_data = $simba_two_factor_authentication->get_option('tfa_xmlrpc_on');
457
458 if($saved_data)
459 return true;
460
461 return false;
462 }
463
464 private function get_iv_size() {
465 // mcrypt first, for backwards compatibility
466 if (function_exists('mcrypt_get_iv_size')) {
467 return mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
468 } elseif (function_exists('openssl_cipher_iv_length')) {
469 return openssl_cipher_iv_length('AES-128-CBC');
470 }
471 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
472 }
473
474 private function create_iv($iv_size) {
475 if (function_exists('mcrypt_create_iv')) {
476 return mcrypt_create_iv($iv_size, MCRYPT_RAND);
477 } elseif (function_exists('openssl_random_pseudo_bytes')) {
478 return openssl_random_pseudo_bytes($iv_size);
479 }
480 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
481 }
482
483 private function encrypt($key, $string, $iv) {
484 // Prefer OpenSSL, because it uses correct padding, and its output can be decrypted by mcrypt - whereas, the converse is not true
485 if (function_exists('openssl_encrypt')) {
486 return openssl_encrypt($string, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
487 } elseif (function_exists('mcrypt_encrypt')) {
488 return mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv);
489 }
490 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
491 }
492
493 private function decrypt($key, $enc, $iv, $force_openssl = false) {
494 // 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
495 if (function_exists('mcrypt_decrypt') && !$force_openssl) {
496 return mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv);
497 } elseif (function_exists('openssl_decrypt')) {
498 $decrypted = openssl_decrypt($enc, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
499 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."); }
500 return $decrypted;
501 }
502 if ($force_openssl) return false;
503 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
504 }
505
506 public function encryptString($string, $salt_suffix)
507 {
508 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
509
510 $iv_size = $this->get_iv_size();
511 $iv = $this->create_iv($iv_size);
512
513 $enc = $this->encrypt($key, $string, $iv);
514
515 if (false === $enc) return false;
516
517 $enc = $iv.$enc;
518 $enc_b64 = base64_encode($enc);
519 return $enc_b64;
520 }
521
522 private function decryptString($enc_b64, $salt_suffix, $force_openssl = false)
523 {
524 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
525
526 $iv_size = $this->get_iv_size();
527 $enc_conc = base64_decode($enc_b64);
528
529 $iv = substr($enc_conc, 0, $iv_size);
530 $enc = substr($enc_conc, $iv_size);
531
532 $string = $this->decrypt($key, $enc, $iv, $force_openssl);
533
534 // Remove padding bytes
535 return rtrim($string, "\x00..\x1F");
536 }
537
538 private function hashAndBin($pw, $salt)
539 {
540 $key = $this->hash($pw, $salt);
541 $key = pack('H*', $key);
542 // 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
543 // 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.
544 // In summary: this isn't encryption, and is not intended to be.
545 return str_repeat(chr(0), 16);
546 }
547
548 private function hash($pw, $salt)
549 {
550 //$hash = hash_pbkdf2('sha256', $pw, $salt, 10);
551 //$hash = crypt($pw, '$5$'.$salt.'$');
552 $hash = md5($salt.$pw);
553 return $hash;
554 }
555
556 private function randString($len = 6)
557 {
558 $chars = '23456789QWERTYUPASDFGHJKLZXCVBNM';
559 $chars = str_split($chars);
560 shuffle($chars);
561 $code = implode('', array_splice($chars, 0, $len));
562
563 return $code;
564 }
565
566 public function setUserHMACTypes()
567 {
568 //We need this because we dont want to change third party apps users algorithm
569 $users = get_users(array('meta_key' => 'simbatfa_delivery_type', 'meta_value' => 'third-party-apps'));
570 if(!empty($users))
571 {
572 foreach($users as $user)
573 {
574 $tfa_algorithm_type = get_user_meta($user->ID, 'tfa_algorithm_type', true);
575 if($tfa_algorithm_type)
576 continue;
577
578 update_user_meta($user->ID, 'tfa_algorithm_type', $this->getUserAlgorithm($user->ID));
579 }
580 }
581 }
582
583 }
584