PluginProbe
Two Factor Authentication / 1.12.2
Two Factor Authentication v1.12.2
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 / providers / totp-hotp / loader.php

loader.php in Two Factor Authentication 1.12.2, at providers/totp-hotp/loader.php

493 lines 17.6 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('No direct access.');
4
5 if (!class_exists('HOTP')) require_once(__DIR__.'/hotp-php-master/hotp.php');
6 if (!class_exists('Base32')) require_once(__DIR__.'/Base32/Base32.php');
7
8 class Simba_TFA_Provider_TOTP {
9
10 // @var Simba_Two_Factor_Authentication
11 private $tfa;
12
13 // @var String
14 private $salt_prefix;
15
16 // @var String
17 private $pw_prefix;
18
19 // @var Integer
20 private $time_window_size;
21
22 // @var Integer
23 private $check_back_time_windows;
24
25 // @var Integer
26 private $check_forward_time_windows;
27
28 // @var Integer
29 private $otp_length = 6;
30
31 // @var Integer
32 private $emergency_codes_length = 8;
33
34 // @var String
35 public $default_hmac = 'totp';
36
37 /**
38 * Class constructor
39 *
40 * @param Simba_Two_Factor_Authentication main plugin class
41 */
42 public function __construct($tfa) {
43 $this->tfa = $tfa;
44
45 $this->otp_helper = new HOTP();
46
47 add_action('plugins_loaded', array($this, 'plugins_loaded'));
48
49 }
50
51 /**
52 * Runs upon the WP action plugins_loaded
53 */
54 public function plugins_loaded() {
55 $this->time_window_size = apply_filters('simbatfa_time_window_size', 30);
56 $this->check_back_time_windows = apply_filters('simbatfa_check_back_time_windows', 2);
57 $this->check_forward_time_windows = apply_filters('simbatfa_check_forward_time_windows', 1);
58 $this->check_forward_counter_window = apply_filters('simbatfa_check_forward_counter_window', 20);
59
60 $this->salt_prefix = defined('AUTH_SALT') ? AUTH_SALT : wp_salt('auth');
61 $this->pw_prefix = defined('AUTH_KEY') ? AUTH_KEY : get_site_option('auth_key');
62 }
63
64 /**
65 * Generate the current code for a specified user
66 *
67 * @param $user_id Integer - WordPress user ID
68 *
69 * @return String|Boolean - false if not set up
70 */
71 public function get_current_code($user_id) {
72
73 $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true);
74
75 if (!$tfa_priv_key_64) return false;
76
77 return $this->generateOTP($user_id, $tfa_priv_key_64);
78
79 }
80
81 public function print_default_hmac_radios() {
82
83 $setting = $this->tfa->get_option('tfa_default_hmac');
84
85 $setting = $setting === false || !$setting ? $this->default_hmac : $setting;
86
87 $types = array('totp' => __('TOTP (time based - most common algorithm; used by Google Authenticator)', 'two-factor-authentication'), 'hotp' => __('HOTP (event based)', 'two-factor-authentication'));
88
89 foreach ($types as $id => $name) {
90 print '<input type="radio" id="tfa_default_hmac_'.esc_attr($id).'" name="tfa_default_hmac" value="'.$id.'" '.($setting == $id ? 'checked="checked"' :'').'> '.'<label for="tfa_default_hmac_'.esc_attr($id).'">'."$name</label><br>\n";
91 }
92 }
93
94 public function generateOTP($user_ID, $key_b64, $length = 6, $counter = false) {
95
96 $length = $length ? (int)$length : 6;
97
98 $key = $this->decryptString($key_b64, $user_ID);
99 $alg = $this->get_user_otp_algorithm($user_ID);
100
101 if ('hotp' == $alg) {
102 $db_counter = $this->getUserCounter($user_ID);
103
104 $counter = $counter ? $counter : $db_counter;
105 $otp_res = $this->otp_helper->generateByCounter($key, $counter);
106 } else {
107 //time() is supposed to be UTC
108 $time = $counter ? $counter : time();
109 $otp_res = $this->otp_helper->generateByTime($key, $this->time_window_size, $time);
110 }
111 $code = $otp_res->toHotp($length);
112
113 return $code;
114 }
115
116 /**
117 * Generate a list of OTP codes based on the user, key and time window
118 *
119 * @param Integer $user_ID - user ID
120 * @param String $key_b64 - the user's private key, in base64 format
121 *
122 * @return Array
123 */
124 private function generate_otps_for_login_check($user_ID, $key_b64) {
125 $key = trim($this->decryptString($key_b64, $user_ID));
126 $alg = $this->get_user_otp_algorithm($user_ID);
127
128 if ('totp' == $alg) {
129 $otp_res = $this->otp_helper->generateByTimeWindow($key, $this->time_window_size, -1*$this->check_back_time_windows, $this->check_forward_time_windows);
130 } elseif ('hotp' == $alg) {
131
132 $counter = $this->getUserCounter($user_ID);
133
134 $otp_res = array();
135
136 for ($i = 0; $i < $this->check_forward_counter_window; $i++) {
137 $otp_res[] = $this->otp_helper->generateByCounter($key, $counter+$i);
138 }
139 }
140 return $otp_res;
141 }
142
143
144 /**
145 * Generate a private key for the user.
146 *
147 * @param Integer $user_id - WordPress user ID
148 * @param Boolean|String $key
149 *
150 * @return String
151 */
152 public function addPrivateKey($user_id, $key = false) {
153
154 // To work with Google Authenticator it has to be 10 bytes = 16 chars in base32
155 $code = $key ? $key : strtoupper($this->randString(10));
156
157 // Encrypt the key
158 $code = $this->encryptString($code, $user_id);
159
160 // Add private key to usermeta
161 update_user_meta($user_id, 'tfa_priv_key_64', $code);
162
163 $alg = $this->get_user_otp_algorithm($user_id);
164
165 // This hook is used for generation of emergency codes to accompany the key
166 do_action('simba_tfa_adding_private_key', $alg, $user_id, $code, $this);
167
168 $this->changeUserAlgorithmTo($user_id, $alg);
169
170 return $code;
171 }
172
173 // 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
174 public function potentially_port_private_keys() {
175
176 $simba_tfa_priv_key_format = get_site_option('simba_tfa_priv_key_format', false);
177
178 if ($simba_tfa_priv_key_format >= 1 || !function_exists('openssl_encrypt')) return;
179
180 $attempts = 0;
181 $successes = 0;
182
183 error_log("TFA: Beginning attempt to port private key encryption over to openssl");
184
185 global $wpdb;
186
187 $sql = "SELECT user_id, meta_value FROM ".$wpdb->usermeta." WHERE meta_key = 'tfa_priv_key_64'";
188
189 $user_results = $wpdb->get_results($sql);
190
191 foreach ($user_results as $u) {
192 $dec_openssl = $this->decryptString($u->meta_value, $u->user_id, true);
193
194 $ported = false;
195 if ('' == $dec_openssl) {
196
197 $attempts++;
198
199 $dec_default = $this->decryptString($u->meta_value, $u->user_id);
200
201 if ('' != $dec_default) {
202
203 $enc = $this->encryptString($dec_default, $u->user_id);
204
205 if ($enc) {
206
207 $ported = true;
208 $successes++;
209 update_user_meta($u->user_id, 'tfa_priv_key_64', $enc);
210 }
211 }
212
213 }
214
215 if ($ported) {
216 error_log("TFA: Successfully ported the key for user with ID ".$u->user_id." over to openssl");
217 } else {
218 error_log("TFA: Failed to port the key for user with ID ".$u->user_id." over to openssl");
219 }
220 }
221
222 if ($attempts == 0 || $successes > 0) update_site_option('simba_tfa_priv_key_format', 1);
223
224 }
225
226 public function getPrivateKeyPlain($enc, $user_ID) {
227 $dec = $this->decryptString($enc, $user_ID);
228 $this->potentially_port_private_keys();
229 return $dec;
230 }
231
232 /**
233 * @param Array $codes - current list of codes (encrypted)
234 * @param Integer $user_ID - WP user ID
235 * @param Boolean $generate_if_empty - generate some new codes if the list is empty
236 *
237 * @return String - human-usable codes, separated by ', ' (or a human-readable message, if there were none)
238 */
239 public function getPanicCodesString($codes, $user_ID, $generate_if_empty = false) {
240 if (!is_array($codes)) return '<em>'.__('No emergency codes left. Sorry.', 'two-factor-authentication').'</em>';
241 if ($generate_if_empty && empty($codes)) {
242 $tfa_priv_key = get_user_meta($user_ID, 'tfa_priv_key_64', true);
243 $algorithm = get_user_meta($user_ID, 'tfa_algorithm_type', true);
244 do_action('simba_tfa_emergency_codes_empty', $algorithm, $user_ID, $tfa_priv_key, $this);
245 $codes = get_user_meta($user_ID, 'simba_tfa_emergency_codes_64', true);
246 if (!is_array($codes)) $codes = array();
247 }
248
249 $emergency_str = '';
250
251 foreach ($codes as $p_code) {
252 $emergency_str .= $this->decryptString($p_code, $user_ID).', ';
253 }
254
255 $emergency_str = rtrim($emergency_str, ', ');
256
257 $emergency_str = $emergency_str ? $emergency_str : '<em>'.__('There are no emergency codes left. You will need to reset your private key.', 'two-factor-authentication').'</em>';
258
259 return $emergency_str;
260 }
261
262 /**
263 * Check a code for a user (checks the code only - does not check activation status etc.)
264 *
265 * @param Integer $user_id - WP user ID
266 * @param String $user_code - the code to check
267 * @param Boolean $allow_emergency_code - whether to check against emergency codes
268 *
269 * @return Boolean
270 */
271 public function check_code_for_user($user_id, $user_code, $allow_emergency_code = true) {
272
273 $tfa_priv_key = get_user_meta($user_id, 'tfa_priv_key_64', true);
274 // $tfa_last_login = get_user_meta($user_id, 'tfa_last_login', true); // Unused
275 $tfa_last_pws_arr = get_user_meta($user_id, 'tfa_last_pws', true);
276 $tfa_last_pws = @$tfa_last_pws_arr ? $tfa_last_pws_arr : array();
277 $alg = $this->get_user_otp_algorithm($user_id);
278
279 $current_time_window = intval(time()/30);
280
281 //Give the user 1,5 minutes time span to enter/retrieve the code
282 //Or check $this->check_forward_counter_window number of events if hotp
283 $codes = $this->generate_otps_for_login_check($user_id, $tfa_priv_key);
284
285 //A recently used code was entered; that's not OK.
286 if (in_array($this->hash($user_code, $user_id), $tfa_last_pws)) return false;
287
288 $match = false;
289 foreach ($codes as $index => $code) {
290 if (trim($code->toHotp(6)) == trim($user_code)) {
291 $match = true;
292 $found_index = $index;
293 break;
294 }
295 }
296
297 // Check emergency codes
298 if (!$match) {
299 $emergency_codes = $allow_emergency_code ? get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true) : array();
300
301 if (!$emergency_codes) return $match;
302
303 $dec = array();
304 foreach ($emergency_codes as $emergency_code)
305 $dec[] = trim($this->decryptString(trim($emergency_code), $user_id));
306
307 $in_array = array_search($user_code, $dec);
308 $match = $in_array !== false;
309
310 //Remove emergency code
311 if ($match) {
312 array_splice($emergency_codes, $in_array, 1);
313 update_user_meta($user_id, 'simba_tfa_emergency_codes_64', $emergency_codes);
314 do_action('simba_tfa_emergency_code_used', $user_id, $emergency_codes);
315 }
316
317 } else {
318 //Add the used code as well so it cant be used again
319 //Keep the two last codes
320 $tfa_last_pws[] = $this->hash($user_code, $user_id);
321 $nr_of_old_to_save = $alg == 'hotp' ? $this->check_forward_counter_window : $this->check_back_time_windows;
322
323 if (count($tfa_last_pws) > $nr_of_old_to_save) array_splice($tfa_last_pws, 0, 1);
324
325 update_user_meta($user_id, 'tfa_last_pws', $tfa_last_pws);
326 }
327
328 if ($match) {
329 //Save the time window when the last successful login took place
330 update_user_meta($user_id, 'tfa_last_login', $current_time_window);
331
332 //Update the counter if HOTP was used
333 if ($alg == 'hotp') {
334 $counter = $this->getUserCounter($user_id);
335
336 $enc_new_counter = $this->encryptString($counter+1, $user_id);
337 update_user_meta($user_id, 'tfa_hotp_counter', $enc_new_counter);
338
339 if ($found_index > 10) update_user_meta($user_id, 'tfa_hotp_off_sync', 1);
340 }
341 }
342
343 return $match;
344
345 }
346
347 public function getUserCounter($user_ID) {
348 $enc_counter = get_user_meta($user_ID, 'tfa_hotp_counter', true);
349 return $enc_counter ? trim($this->decryptString(trim($enc_counter), $user_ID)) : '';
350 }
351
352 public function changeUserAlgorithmTo($user_id, $new_algorithm) {
353 update_user_meta($user_id, 'tfa_algorithm_type', $new_algorithm);
354 delete_user_meta($user_id, 'tfa_hotp_off_sync');
355
356 $counter_start = rand(13, 999999999);
357 $enc_counter_start = $this->encryptString($counter_start, $user_id);
358
359 if ('hotp' == $new_algorithm) {
360 update_user_meta($user_id, 'tfa_hotp_counter', $enc_counter_start);
361 } else {
362 delete_user_meta($user_id, 'tfa_hotp_counter');
363 }
364 }
365
366 /**
367 * Whether HOTP or TOTP is being used
368 *
369 * @param Integer $user_id - WordPress user ID
370 *
371 * @return String - 'hotp' or 'totp'
372 */
373 public function get_user_otp_algorithm($user_id) {
374 global $simba_two_factor_authentication;
375 $setting = get_user_meta($user_id, 'tfa_algorithm_type', true);
376 $default_hmac = $simba_two_factor_authentication->get_option('tfa_default_hmac');
377 $default_hmac = $default_hmac ? $default_hmac : $this->default_hmac;
378
379 $setting = $setting === false || !$setting ? $default_hmac : $setting;
380 return $setting;
381 }
382
383 private function get_iv_size() {
384 // mcrypt first, for backwards compatibility
385 if (function_exists('mcrypt_get_iv_size')) {
386 return $GLOBALS['simba_two_factor_authentication']->is_mcrypt_deprecated() ? @mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC) : mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
387 } elseif (function_exists('openssl_cipher_iv_length')) {
388 return openssl_cipher_iv_length('AES-128-CBC');
389 }
390 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
391 }
392
393 private function encrypt($key, $string, $iv) {
394 // Prefer OpenSSL, because it uses correct padding, and its output can be decrypted by mcrypt - whereas, the converse is not true
395 if (function_exists('openssl_encrypt')) {
396 return openssl_encrypt($string, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
397 } elseif (function_exists('mcrypt_encrypt')) {
398 return $GLOBALS['simba_two_factor_authentication']->is_mcrypt_deprecated() ? @mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv) : mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $iv);
399 }
400 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
401 }
402
403 private function decrypt($key, $enc, $iv, $force_openssl = false) {
404 // 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
405 if (function_exists('mcrypt_decrypt') && !$force_openssl) {
406 return $GLOBALS['simba_two_factor_authentication']->is_mcrypt_deprecated() ? @mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv) : mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $enc, MCRYPT_MODE_CBC, $iv);
407 } elseif (function_exists('openssl_decrypt')) {
408 $decrypted = openssl_decrypt($enc, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
409 if (false === $decrypted && !$force_openssl) {
410 $extra = function_exists('wp_debug_backtrace_summary') ? " backtrace: ".wp_debug_backtrace_summary() : '';
411 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.$extra");
412 }
413 return $decrypted;
414 }
415 if ($force_openssl) return false;
416 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
417 }
418
419 public function encryptString($string, $salt_suffix) {
420 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
421
422 $iv_size = $this->get_iv_size();
423 $iv = $GLOBALS['simba_two_factor_authentication']->random_bytes($iv_size);
424
425 $enc = $this->encrypt($key, $string, $iv);
426
427 if (false === $enc) return false;
428
429 $enc = $iv.$enc;
430 $enc_b64 = base64_encode($enc);
431 return $enc_b64;
432 }
433
434 private function decryptString($enc_b64, $salt_suffix, $force_openssl = false) {
435 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
436
437 $iv_size = $this->get_iv_size();
438 $enc_conc = bin2hex(base64_decode($enc_b64));
439
440 $iv = hex2bin(substr($enc_conc, 0, $iv_size*2));
441 $enc = hex2bin(substr($enc_conc, $iv_size*2));
442
443 $string = $this->decrypt($key, $enc, $iv, $force_openssl);
444
445 // Remove padding bytes
446 return rtrim($string, "\x00..\x1F");
447 }
448
449 private function hashAndBin($pw, $salt) {
450 $key = $this->hash($pw, $salt);
451 $key = pack('H*', $key);
452 // 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
453 // 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.
454 // In summary: this isn't encryption, and is not intended to be.
455 return str_repeat(chr(0), 16);
456 }
457
458 private function hash($pw, $salt) {
459 //$hash = hash_pbkdf2('sha256', $pw, $salt, 10);
460 //$hash = crypt($pw, '$5$'.$salt.'$');
461 $hash = md5($salt.$pw);
462 return $hash;
463 }
464
465 private function randString($len = 10) {
466 $chars = '23456789QWERTYUPASDFGHJKLZXCVBNM';
467 $chars = str_split($chars);
468 shuffle($chars);
469 if (function_exists('random_int')) {
470 $code = '';
471 for ($i = 1; $i <= $len; $i++) {
472 $code .= $chars[random_int(0, count($chars)-1)];
473 }
474 } else {
475 $code = implode('', array_splice($chars, 0, $len));
476 }
477 return $code;
478 }
479
480 public function setUserHMACTypes() {
481 // We need this because we dont want to change third party apps users algorithm
482 $users = get_users(array('meta_key' => 'simbatfa_delivery_type', 'meta_value' => 'third-party-apps'));
483 if (empty($users)) return;
484 foreach ($users as $user) {
485 $tfa_algorithm_type = get_user_meta($user->ID, 'tfa_algorithm_type', true);
486 if ($tfa_algorithm_type) continue;
487
488 update_user_meta($user->ID, 'tfa_algorithm_type', $this->get_user_otp_algorithm($user->ID));
489 }
490 }
491
492 }
493