PluginProbe
Two Factor Authentication / 1.2.6
Two Factor Authentication v1.2.6
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.6, at includes/class.TFA.php

489 lines 15.1 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
91 public function getPrivateKeyPlain($enc, $user_ID)
92 {
93 $dec = $this->decryptString($enc, $user_ID);
94 return $dec;
95 }
96
97
98 public function getPanicCodesString($arr, $user_ID)
99 {
100 if(!is_array($arr)) return '<em>'.__('No emergency codes left. Sorry.', SIMBA_TFA_TEXT_DOMAIN).'</em>';
101
102 $emergency_str = '';
103
104 foreach($arr as $p_code) {
105 $emergency_str .= $this->decryptString($p_code, $user_ID).', ';
106 }
107
108 $emergency_str = rtrim($emergency_str, ', ');
109
110 $emergency_str = $emergency_str ? $emergency_str : '<em>'.__('No emergency codes left. Sorry.', SIMBA_TFA_TEXT_DOMAIN).'</em>';
111 return $emergency_str;
112 }
113
114 public function preAuth($params)
115 {
116 global $wpdb;
117 $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']);
118 $user = $wpdb->get_row($query);
119 if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
120 // Corner-case: login looks like an email, but is a username rather than email address
121 $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']));
122 }
123 $is_activated_for_user = true;
124 $is_activated_by_user = false;
125
126 if($user) {
127 $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true);
128 $is_activated_for_user = $this->isActivatedForUser($user->ID);
129 $is_activated_by_user = $this->isActivatedByUser($user->ID);
130
131 if($is_activated_for_user && $is_activated_by_user)
132 {
133 // $delivery_type = get_user_meta($user->ID, 'simbatfa_delivery_type', true);
134
135 //No private key yet, generate one.
136 //This is safe to do since the code is emailed to the user.
137 //Not safe to do if the user has disabled email.
138 if(!$tfa_priv_key)
139 $tfa_priv_key = $this->addPrivateKey($user->ID);
140
141 $code = $this->generateOTP($user->ID, $tfa_priv_key);
142
143 return true;//Set to true
144 }
145 return false;
146 }
147 return false;
148 }
149
150 public function authUserFromLogin($params)
151 {
152
153 global $simba_two_factor_authentication, $wpdb;
154
155 if(!$this->isCallerActive($params))
156 return true;
157
158 $field = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? 'user_email' : 'user_login';
159 $query = $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE ".$field."=%s", $params['log']);
160 $response = $wpdb->get_row($query);
161
162 $user_ID = is_object($response) ? $response->ID : false;
163 $user_registered = is_object($response) ? $response->user_registered : false;
164
165 $user_code = trim(@$params['two_factor_code']);
166
167 if(!$user_ID)
168 return true;
169
170 if(!$this->isActivatedForUser($user_ID))
171 return true;
172
173 if(!$this->isActivatedByUser($user_ID)) {
174
175 if (!$this->isRequiredForUser($user_ID)) {
176 return true;
177 }
178
179 $requireafter = absint($simba_two_factor_authentication->get_option('tfa_requireafter')) * 86400;
180
181 $account_age = time() - strtotime($user_registered);
182
183 if ($account_age > $requireafter) {
184 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)));
185 }
186
187 return true;
188 }
189
190 $tfa_priv_key = get_user_meta($user_ID, 'tfa_priv_key_64', true);
191 $tfa_last_login = get_user_meta($user_ID, 'tfa_last_login', true);
192 $tfa_last_pws_arr = get_user_meta($user_ID, 'tfa_last_pws', true);
193 $tfa_last_pws = @$tfa_last_pws_arr ? $tfa_last_pws_arr : array();
194 $alg = $this->getUserAlgorithm($user_ID);
195
196 $current_time_window = intval(time()/30);
197
198 //Give the user 1,5 minutes time span to enter/retrieve the code
199 //Or check $this->check_forward_counter_window number of events if hotp
200 $codes = $this->generateOTPsForLoginCheck($user_ID, $tfa_priv_key);
201
202 //A recently used code was entered.
203 //Not ok
204 if(in_array($this->hash($user_code, $user_ID), $tfa_last_pws))
205 return false;
206
207 $match = false;
208 foreach($codes as $index => $code)
209 {
210 if(trim($code->toHotp(6)) == trim($user_code))
211 {
212 $match = true;
213 $found_index = $index;
214 break;
215 }
216 }
217
218 //Check emergency codes
219 if(!$match)
220 {
221 $emergency_codes = get_user_meta($user_ID, 'simba_tfa_emergency_codes_64', true);
222
223 if(!@$emergency_codes)
224 return $match;
225
226 $dec = array();
227 foreach($emergency_codes as $emergency_code)
228 $dec[] = trim($this->decryptString(trim($emergency_code), $user_ID));
229
230 $in_array = array_search($user_code, $dec);
231 $match = $in_array !== false;
232
233 if($match)//Remove emergency code
234 {
235 array_splice($emergency_codes, $in_array, 1);
236 update_user_meta($user_ID, 'simba_tfa_emergency_codes_64', $emergency_codes);
237 do_action('simba_tfa_emergency_code_used', $user_ID, $emergency_codes);
238 }
239
240 } else {
241 //Add the used code as well so it cant be used again
242 //Keep the two last codes
243 $tfa_last_pws[] = $this->hash($user_code, $user_ID);
244 $nr_of_old_to_save = $alg == 'hotp' ? $this->check_forward_counter_window : $this->check_back_time_windows;
245
246 if(count($tfa_last_pws) > $nr_of_old_to_save)
247 array_splice($tfa_last_pws, 0, 1);
248
249 update_user_meta($user_ID, 'tfa_last_pws', $tfa_last_pws);
250 }
251
252 if($match)
253 {
254 //Save the time window when the last successful login took place
255 update_user_meta($user_ID, 'tfa_last_login', $current_time_window);
256
257 //Update the counter if HOTP was used
258 if($alg == 'hotp')
259 {
260 $counter = $this->getUserCounter($user_ID);
261
262 $enc_new_counter = $this->encryptString($counter+1, $user_ID);
263 update_user_meta($user_ID, 'tfa_hotp_counter', $enc_new_counter);
264
265 if($found_index > 10)
266 update_user_meta($user_ID, 'tfa_hotp_off_sync', 1);
267 }
268 }
269
270 return $match;
271
272 }
273
274 public function getUserCounter($user_ID)
275 {
276 $enc_counter = get_user_meta($user_ID, 'tfa_hotp_counter', true);
277
278 if($enc_counter)
279 $counter = $this->decryptString(trim($enc_counter), $user_ID);
280 else
281 return '';
282
283 return trim($counter);
284 }
285
286 public function changeUserAlgorithmTo($user_id, $new_algorithm)
287 {
288 update_user_meta($user_id, 'tfa_algorithm_type', $new_algorithm);
289 delete_user_meta($user_id, 'tfa_hotp_off_sync');
290
291 $counter_start = rand(13, 999999999);
292 $enc_counter_start = $this->encryptString($counter_start, $user_id);
293
294 if($new_algorithm == 'hotp')
295 update_user_meta($user_id, 'tfa_hotp_counter', $enc_counter_start);
296 else
297 delete_user_meta($user_id, 'tfa_hotp_counter');
298 }
299
300 //Added
301 public function changeEnableTFA($user_id, $setting)
302 {
303 $setting = ($setting === 'true') ? 1 : 0;
304
305 update_user_meta($user_id, 'tfa_enable_tfa', $setting);
306 }
307
308 public function getUserAlgorithm($user_id)
309 {
310 global $simba_two_factor_authentication;
311 $setting = get_user_meta($user_id, 'tfa_algorithm_type', true);
312 $default_hmac = $simba_two_factor_authentication->get_option('tfa_default_hmac');
313 $default_hmac = $default_hmac ? $default_hmac : $this->default_hmac;
314
315 $setting = $setting === false || !$setting ? $default_hmac : $setting;
316 return $setting;
317 }
318
319 public function isActivatedForUser($user_id)
320 {
321
322 if (empty($user_id)) return false;
323
324 global $simba_two_factor_authentication;
325
326 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
327 if (is_multisite() && is_super_admin($user_id)) {
328 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
329 $role = '_super_admin';
330 $db_val = $simba_two_factor_authentication->get_option('tfa_'.$role);
331 $db_val = $db_val === false || $db_val ? 1 : 0; //Nothing saved or > 0 returns 1;
332
333 return ($db_val) ? true : false;
334 }
335
336 $user = new WP_User($user_id);
337
338 foreach($user->roles as $role)
339 {
340 $db_val = $simba_two_factor_authentication->get_option('tfa_'.$role);
341 $db_val = $db_val === false || $db_val ? 1 : 0; //Nothing saved or > 0 returns 1;
342
343 if($db_val)
344 return true;
345 }
346
347 return false;
348
349 }
350
351 // N.B. - This doesn't check isActivatedForUser() - the caller would normally want to do that first
352 public function isRequiredForUser($user_id)
353 {
354
355 if (empty($user_id)) return false;
356
357 global $simba_two_factor_authentication;
358
359 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
360 if (is_multisite() && is_super_admin($user_id)) {
361 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
362 $role = '_super_admin';
363 $db_val = $simba_two_factor_authentication->get_option('tfa_required_'.$role);
364
365 return ($db_val) ? true : false;
366 }
367
368 $user = new WP_User($user_id);
369
370 foreach($user->roles as $role)
371 {
372 $db_val = $simba_two_factor_authentication->get_option('tfa_required_'.$role);
373
374 if($db_val)
375 return true;
376 }
377
378 return false;
379
380 }
381
382 //Added
383 public function isActivatedByUser($user_id){
384 $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true);
385 $enabled = empty($enabled) ? false : true;
386
387 return $enabled;
388 }
389
390 // Disabled: unused
391 // public function saveCallerStatus($caller_id, $status)
392 // {
393 // global $simba_two_factor_authentication;
394 // if($caller_id == 'xmlrpc')
395 // $simba_two_factor_authentication->set_option('tfa_xmlrpc_on', $status);
396 // }
397
398 private function isCallerActive($params)
399 {
400
401 if(!preg_match('/(\/xmlrpc\.php)$/', trim($params['caller'])))
402 return true;
403
404 global $simba_two_factor_authentication;
405 $saved_data = $simba_two_factor_authentication->get_option('tfa_xmlrpc_on');
406
407 if($saved_data)
408 return true;
409
410 return false;
411 }
412
413 public function encryptString($string, $salt_suffix)
414 {
415 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
416
417 $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
418 $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
419
420 $enc = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv);
421
422 $enc = $iv.$enc;
423 $enc_b64 = base64_encode($enc);
424 return $enc_b64;
425 }
426
427 private function decryptString($enc_b64, $salt_suffix)
428 {
429 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
430
431 $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
432 $enc_conc = base64_decode($enc_b64);
433
434 $iv = substr($enc_conc, 0, $iv_size);
435 $enc = substr($enc_conc, $iv_size);
436
437 $string = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv);
438
439 // Remove zeroed bytes
440 return rtrim($string);
441 }
442
443 private function hashAndBin($pw, $salt)
444 {
445 $key = $this->hash($pw, $salt);
446 $key = pack('H*', $key);
447 // 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
448 // 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.
449 // In summary: this isn't encryption, and is not intended to be.
450 return str_repeat(chr(0), 16);
451 }
452
453 private function hash($pw, $salt)
454 {
455 //$hash = hash_pbkdf2('sha256', $pw, $salt, 10);
456 //$hash = crypt($pw, '$5$'.$salt.'$');
457 $hash = md5($salt.$pw);
458 return $hash;
459 }
460
461 private function randString($len = 6)
462 {
463 $chars = '23456789QWERTYUPASDFGHJKLZXCVBNM';
464 $chars = str_split($chars);
465 shuffle($chars);
466 $code = implode('', array_splice($chars, 0, $len));
467
468 return $code;
469 }
470
471 public function setUserHMACTypes()
472 {
473 //We need this because we dont want to change third party apps users algorithm
474 $users = get_users(array('meta_key' => 'simbatfa_delivery_type', 'meta_value' => 'third-party-apps'));
475 if(!empty($users))
476 {
477 foreach($users as $user)
478 {
479 $tfa_algorithm_type = get_user_meta($user->ID, 'tfa_algorithm_type', true);
480 if($tfa_algorithm_type)
481 continue;
482
483 update_user_meta($user->ID, 'tfa_algorithm_type', $this->getUserAlgorithm($user->ID));
484 }
485 }
486 }
487
488 }
489