PluginProbe
Two Factor Authentication / 1.14.14
Two Factor Authentication v1.14.14
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 / simba-tfa / providers / totp / loader.php

loader.php in Two Factor Authentication 1.14.14, at simba-tfa/providers/totp/loader.php

976 lines 33.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 /**
11 * Simba 2FA object
12 *
13 * @var object instance of Simba_Two_Factor_Authentication(_version)
14 */
15 private $tfa;
16
17 /**
18 * OTP helper object
19 *
20 * @var object instance of HOTP
21 */
22 private $otp_helper;
23
24 /**
25 * Forward counter window to check number of times
26 *
27 * @var int
28 */
29 private $check_forward_counter_window;
30
31 /**
32 * Salt prefix
33 *
34 * @var string
35 */
36 private $salt_prefix;
37
38 /**
39 * Password prefix
40 *
41 * @var string
42 */
43 private $pw_prefix;
44
45 /**
46 * Time window size
47 *
48 * @var int
49 */
50 private $time_window_size;
51
52 /**
53 * Check back time window
54 *
55 * @var int
56 */
57 private $check_back_time_windows;
58
59 /**
60 * Check forward time windows
61 *
62 * @var int
63 */
64 private $check_forward_time_windows;
65
66 /**
67 * OTP length
68 *
69 * @var int
70 */
71 private $otp_length = 6;
72
73 /**
74 * Emergency codes length
75 *
76 * @var int
77 */
78 private $emergency_codes_length = 8;
79
80 /**
81 * Default HMAC type
82 *
83 * @var string
84 */
85 public $default_hmac = 'totp';
86
87 /**
88 * Settings saved flag
89 *
90 * @var boolean
91 */
92 private $settings_saved = false;
93
94 /**
95 * Class constructor
96 *
97 * @param Object - main Simba_Two_Factor_Authentication(_version) plugin class
98 */
99 public function __construct($tfa) {
100 $this->tfa = $tfa;
101
102 $this->otp_helper = new HOTP();
103
104 add_action('plugins_loaded', array($this, 'plugins_loaded'));
105
106 add_action('admin_init', array($this, 'admin_init'));
107
108 if (!is_admin()) {
109 add_action('init', array($this, 'check_possible_reset'));
110 }
111
112 // Potentially show off-sync message for hotp
113 add_action('admin_notices', array($this, 'tfa_show_hotp_off_sync_message'));
114 }
115
116 /**
117 * Return whether or not this class detected and saved new settings
118 *
119 * @return Boolean
120 */
121 public function were_settings_saved() {
122 return $this->settings_saved;
123 }
124
125 /**
126 * Runs upon the WP action admin_init
127 */
128 public function admin_init() {
129
130 $this->check_possible_reset();
131
132 global $current_user;
133
134 if (!empty($_REQUEST['_tfa_activate_nonce']) && !empty($_POST['tfa_enable_tfa']) && wp_verify_nonce($_REQUEST['_tfa_activate_nonce'], 'tfa_activate') && !empty($_GET['settings-updated'])) {
135 $this->tfa->change_tfa_enabled_status($current_user->ID, $_POST['tfa_enable_tfa']);
136 $this->settings_saved = true;
137 }
138
139 if (!empty($_REQUEST['_tfa_algorithm_nonce']) && !empty($_POST['tfa_algorithm_type']) && !empty($_GET['settings-updated']) && wp_verify_nonce($_REQUEST['_tfa_algorithm_nonce'], 'tfa_algorithm')) {
140
141 $old_algorithm = $this->get_user_otp_algorithm($current_user->ID);
142
143 if ($old_algorithm != $_POST['tfa_algorithm_type']) {
144 $this->changeUserAlgorithmTo($current_user->ID, $_POST['tfa_algorithm_type']);
145 }
146
147 $this->settings_saved = true;
148 }
149
150 if (!empty($_GET['warning_button_clicked']) && !empty($_REQUEST['resyncnonce']) && wp_verify_nonce($_REQUEST['resyncnonce'], 'tfaresync')) {
151 delete_user_meta($current_user->ID, 'tfa_hotp_off_sync');
152 }
153
154 }
155
156 /**
157 * Enqueue adding of JavaScript for footer
158 */
159 public function add_footer() {
160
161 static $added_footer = false;
162 if ($added_footer) return;
163 $added_footer = true;
164
165 $qr_script_file = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? 'jquery-qrcode.js' : 'jquery-qrcode.min.js';
166
167 $qr_script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->tfa->includes_dir()."/jquery-qrcode/$qr_script_file");
168
169 wp_register_script('jquery-qrcode', $this->tfa->includes_url()."/jquery-qrcode/$qr_script_file", array('jquery'), $qr_script_ver);
170
171 $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->tfa->includes_dir()."/totp.js");
172
173 // Adds the necessary JavaScript for rendering and updating QR codes, and handling trusted devices removal in the admin area
174 wp_enqueue_script('simba-tfa-totp', $this->tfa->includes_url()."/totp.js", array('jquery-qrcode'), $script_ver);
175
176 wp_localize_script('simba-tfa-totp', 'simbatfa_totp', $this->translation_strings());
177
178 }
179
180 /**
181 * Get textual strings used from JavaScript
182 *
183 * @return Array
184 */
185 private function translation_strings() {
186
187 // It's possible that FORCE_ADMIN_SSL will make that SSL, whilst the user is on the front-end having logged in over non-SSL - and as a result, their login cookies won't get sent, and they're not registered as logged in.
188 $ajax_url = admin_url('admin-ajax.php');
189 $also_try = '';
190 if (!is_admin() && substr(strtolower($ajax_url), 0, 6) == 'https:' && !is_ssl()) {
191 $also_try = 'http:'.substr($ajax_url, 6);
192 }
193
194 return apply_filters('simba_tfa_totp_translation_strings', array(
195 'ajax_url' => $ajax_url,
196 'updating' => __('Updating...', 'two-factor-authentication'),
197 'tfa_shared_nonce' => wp_create_nonce('tfa_shared_nonce'),
198 'also_try' => $also_try,
199 'response' => __('Response:', 'two-factor-authentication'),
200 ));
201 }
202
203 /**
204 * Return a link to refresh the current OTP code
205 *
206 * @return String
207 */
208 public function refresh_current_otp_link() {
209 return '<a href="#" class="simbaotp_refresh">'.__('(update)', 'two-factor-authentication').'</a>';
210 }
211
212 /**
213 * Echo the radio buttons for changing between TOTP/HOTP
214 *
215 * TODO: Hide this choice on new installs (TOTP only)
216 *
217 * @param Integer $user_id
218 */
219 protected function print_algorithm_choice_radios($user_id) {
220 if (!$user_id) return;
221
222 $types = array(
223 'totp' => __('TOTP (time based - most common algorithm; used by Google Authenticator)', 'two-factor-authentication'),
224 'hotp' => __('HOTP (event based)', 'two-factor-authentication')
225 );
226
227 $setting = $this->get_user_otp_algorithm($user_id);
228
229 foreach ($types as $id => $name) {
230 print '<input type="radio" id="tfa_algorithm_type_'.esc_attr($id).'" name="tfa_algorithm_type" value="'.$id.'" '.($setting == $id ? 'checked="checked"' :'').'> <label for="tfa_algorithm_type_'.esc_attr($id).'">'.$name."</label><br>\n";
231 }
232 }
233
234 /**
235 * Print out the advanced settings box - choice of algorithm
236 *
237 * @param Boolean|Callable $submit_button_callback - if not a callback, then <form> tags will be added
238 */
239 public function advanced_settings_box($submit_button_callback = false) {
240
241 global $current_user;
242 $algorithm_type = $this->get_user_otp_algorithm($current_user->ID);
243
244 ?>
245 <h2 id="tfa_advanced_heading" style="clear:both;"><?php _e('Advanced settings', 'two-factor-authentication'); ?></h2>
246
247 <div id="tfa_advanced_box" class="tfa_settings_form" style="margin-top: 20px;">
248
249 <?php if (false === $submit_button_callback) { ?>
250 <form method="post" action="<?php print esc_url(add_query_arg('settings-updated', 'true', $_SERVER['REQUEST_URI'])); ?>">
251 <?php wp_nonce_field('tfa_algorithm', '_tfa_algorithm_nonce', false, true); ?>
252 <?php } ?>
253
254 <?php _e('Choose which algorithm for One Time Passwords you want to use.', 'two-factor-authentication'); ?>
255 <p>
256 <?php
257 $this->print_algorithm_choice_radios($current_user->ID);
258 if ('hotp' == $algorithm_type) {
259 $counter = $this->getUserCounter($current_user->ID);
260 print '<br>'.__('Your counter on the server is currently on', 'two-factor-authentication').': '.$counter;
261 }
262 ?>
263
264 </p>
265 <?php if (false === $submit_button_callback) { submit_button(); echo '</form>'; } else { call_user_func($submit_button_callback); } ?>
266
267 </div>
268 <?php
269 }
270
271 /**
272 * Return an HTML snippet for the current OTP code
273 *
274 * @param Integer|Boolean $user_id
275 *
276 * @return String
277 */
278 public function current_otp_code($user_id = false) {
279 global $current_user;
280 if (false == $user_id) $user_id = $current_user->ID;
281 $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true);
282 if (!$tfa_priv_key_64) $tfa_priv_key_64 = $this->addPrivateKey($user_id);
283 $time_now = time();
284 $refresh_after = 30 - ($time_now % 30);
285 return '<span class="simba_current_otp" data-refresh_after="'.$refresh_after.'">'.$this->generateOTP($user_id, $tfa_priv_key_64).'</span>';
286 }
287
288 /**
289 * Runs upon the WP 'init' action - check for possible private key reset request from the user
290 */
291 public function check_possible_reset() {
292 if (!empty($_GET['simbatfa_priv_key_reset']) && !empty($_REQUEST['nonce']) && wp_verify_nonce($_REQUEST['nonce'], 'simbatfa_reset_private_key')) {
293 $this->reset_private_key_and_emergency_codes();
294 exit;
295 }
296 }
297
298 /**
299 * Remove private key and emergency codes for the specified (or logged-in) user
300 *
301 * @param Boolean|Integer $user_id - WP user ID, or false for the currently logged-in user
302 * @param Boolean $redirect - if this is not false, then a redirection will occur - where to depends upon the value of $_REQUEST['noredirect']
303 */
304 public function reset_private_key_and_emergency_codes($user_id = false, $redirect = true) {
305
306 if (!$user_id) {
307 global $current_user;
308 $user_id = $current_user->ID;
309 }
310
311 delete_user_meta($user_id, 'tfa_priv_key_64');
312 delete_user_meta($user_id, 'simba_tfa_emergency_codes_64');
313
314 if (!$redirect) return;
315
316 if (empty($_REQUEST['noredirect'])) {
317 // TODO: Re-factoring
318 wp_safe_redirect(admin_url('admin.php').'?page='. $this->tfa->get_user_settings_page_slug() .'&settings-updated=1');
319 } else {
320 $url = (is_ssl() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . remove_query_arg(array('simbatfa_priv_key_reset', 'noredirect', 'nonce'));
321 wp_redirect(esc_url_raw($url));
322 }
323 }
324
325 /**
326 * Return HTML for a link to reset the current user's private key
327 *
328 * @return String
329 */
330 public function reset_link() {
331
332 // TODO: Refactoring
333 $url_base = is_admin() ? admin_url('admin.php').'?page='. $this->tfa->get_user_settings_page_slug() .'&settings-updated=1' : (( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST']);
334
335 $add_query_args = array('simbatfa_priv_key_reset' => 1);
336
337 if (!is_admin()) $add_query_args['noredirect'] = 1;
338
339 $url = $url_base.add_query_arg($add_query_args);
340
341 $url = wp_nonce_url($url, 'simbatfa_reset_private_key', 'nonce');
342
343 return '<a href="javascript:if(confirm(\''.__('Warning: if you reset this key you will have to update your apps with the new one. Are you sure you want this?', 'two-factor-authentication').'\')) { window.location = \''.esc_js($url).'\'; }">'.__('Reset private key', 'two-factor-authentication').'</a>';
344
345 }
346
347 /**
348 * Output the current codes box
349 *
350 * @param Boolean|Integer $user_id
351 */
352 public function current_codes_box($user_id = false) {
353
354 global $current_user;
355 if (false == $user_id) $user_id = $current_user->ID;
356
357 $admin = is_admin();
358
359 $this->add_footer();
360
361 $url = preg_replace('/^https?:\/\//i', '', site_url());
362
363 // TODO Replace this with an appropriate method
364 $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true);
365 if (!$tfa_priv_key_64) $tfa_priv_key_64 = $this->addPrivateKey($user_id);
366
367 $tfa_priv_key = trim($this->getPrivateKeyPlain($tfa_priv_key_64, $user_id), "\x00..\x1F");
368
369 $tfa_priv_key_32 = Base32::encode($tfa_priv_key);
370
371 $algorithm_type = $this->get_user_otp_algorithm($user_id);
372
373 if ($admin && $current_user->ID != $user_id) {
374 $user = get_user_by('id', $user_id);
375 $user_descrip = htmlspecialchars($user->user_nicename.' - '.$user->user_email);
376 echo '<h2>'.sprintf(__('Current codes (login: %s)', 'two-factor-authentication'), $user_descrip).'</h2>';
377 }
378
379 ?>
380 <div class="postbox" style="clear:both;">
381
382 <?php if ($admin) { ?>
383 <h3 style="padding: 10px 6px 0px; margin:4px 0 0; cursor: default;">
384 <span style="cursor: default;"><?php echo __('Current one-time password', 'two-factor-authentication').' ';
385 if ($current_user->ID == $user_id) { echo $this->refresh_current_otp_link(); } ?>
386 </span>
387 <div class="inside">
388 <p><strong style="font-size: 3em;"><?php echo $this->current_otp_code($user_id); ?></strong></p>
389 </div>
390 </h3>
391 <?php } else { ?>
392
393 <div class="inside">
394 <p class="simbatfa-frontend-current-otp" style="font-size: 1.5em; margin-top:6px;">
395 <strong><?php echo __('Current one-time password', 'two-factor-authentication').' '.$this->refresh_current_otp_link(); ?></strong> :
396
397 <?php
398 // TODO: Compare this with what's in current_otp_code() - why are we not using that?
399 $time_now = time();
400 $refresh_after = 30 - ($time_now % 30);
401 ?><span class="simba_current_otp" data-refresh_after="<?php echo $refresh_after; ?>"><?php print $this->generateOTP($user_id, $tfa_priv_key_64); ?></span>
402
403 </p>
404 </div>
405
406 <?php } ?>
407
408 <?php if ($admin) { ?>
409 <h3 style="padding-left: 10px; cursor: default;">
410 <span style="cursor: default;"><?php _e('Setting up - either scan the code, or type in the private key', 'two-factor-authentication'); ?></span>
411 </h3>
412 <?php } else { ?>
413 <h2><?php _e('Setting up', 'two-factor-authentication'); ?></h2>
414 <?php } ?>
415
416 <div class="inside">
417 <p>
418 <?php
419 _e('For OTP apps that support using a camera to scan a setup code (below), that is the quickest way to set the app up (e.g. with Duo Mobile, Google Authenticator).', 'two-factor-authentication');
420 echo ' ';
421 _e('Otherwise, you can type the textual private key (shown below) into your app. Always keep private keys secret.', 'two-factor-authentication');
422 ?>
423
424 <?php printf(__('You are currently using %s, %s', 'two-factor-authentication'), strtoupper($algorithm_type), ($algorithm_type == 'totp') ? __('a time based algorithm', 'two-factor-authentication') : __('an event based algorithm', 'two-factor-authentication')); ?>.
425 </p>
426
427 <?php $qr_url = $this->tfa_qr_code_url($algorithm_type, $url, $tfa_priv_key, $user_id); ?>
428 <div style="float: left; padding-right: 20px;" class="simbaotp_qr_container" data-qrcode="<?php echo esc_attr($qr_url); ?>"></div>
429
430 <p>
431 <?php
432 $this->print_private_keys('full', $user_id);
433 if ($current_user->ID == $user_id) {
434 echo $this->reset_link($admin);
435 } else {
436 echo '<a id="tfa-reset-privkey-for-user" data-user_id="'.$user_id.'" href="#">'.__('Reset private key', 'two-factor-authentication').'</a>';
437 }
438 ?>
439 </p>
440
441 <?php
442 if ($admin || false !== apply_filters('simba_tfa_emergency_codes_user_settings', false, $user_id)) {
443 ?>
444
445 <div style="min-height: 100px;">
446 <h3 class="normal" style="cursor: default"><?php _e('Emergency codes', 'two-factor-authentication'); ?></h3>
447 <?php
448 $default_text = '<a href="'.esc_url($this->tfa->get_premium_version_url()).'">'.__('One-time emergency codes are a feature of the Premium version of this plugin.', 'two-factor-authentication').'</a>';
449 echo apply_filters('simba_tfa_emergency_codes_user_settings', $default_text, $user_id);
450 ?>
451 </div>
452
453 <?php } ?>
454 </div>
455
456 </div>
457 <?php
458 }
459
460 /**
461 * Print out HTML showing the specified user's private key
462 *
463 * @param String $type
464 * @param Boolean|Integer $user_id
465 */
466 public function print_private_keys($type = 'full', $user_id = false) {
467
468 global $current_user;
469 if ($user_id == false) $user_id = $current_user->ID;
470
471 $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true);
472 if (!$tfa_priv_key_64) $tfa_priv_key_64 = $this->addPrivateKey($user_id);
473
474 $tfa_priv_key = trim($this->getPrivateKeyPlain($tfa_priv_key_64, $user_id), "\x00..\x1F");
475
476 $tfa_priv_key_32 = Base32::encode($tfa_priv_key);
477
478 // The first (base32) private key used to have the description "base 32 - used by Google Authenticator and Authy", and the base64 version was just described as "private key". But basically the former is what everything uses.
479 //<strong>Private key:</strong> htmlspecialchars($tfa_priv_key)
480 if ('full' == $type) {
481 ?>
482 <strong><?php _e('Private key:', 'two-factor-authentication');?></strong>
483 <?php echo htmlspecialchars($tfa_priv_key_32); ?><br>
484 <?php
485 } elseif ('plain' == $type) {
486 echo htmlspecialchars($tfa_priv_key);
487 } elseif ('base32' == $type) {
488 echo htmlspecialchars($tfa_priv_key_32);
489 } elseif ('base64' == $type) {
490 echo htmlspecialchars($tfa_priv_key_64);
491 }
492 }
493
494 /**
495 * Return the URL for a QR code image
496 *
497 * @param String $algorithm_type - 'totp' or 'hotp'
498 * @param String $url
499 * @param String $tfa_priv_key
500 * @param Boolean|Integer $user_id
501 *
502 * @return String
503 */
504 public function tfa_qr_code_url($algorithm_type, $url, $tfa_priv_key, $user_id = false) {
505 global $current_user;
506
507 $user = (false == $user_id) ? $current_user : get_user_by('id', $user_id);
508
509 $encode = 'otpauth://'.$algorithm_type.'/'.$url.':'.rawurlencode($user->user_login).'?secret='.Base32::encode($tfa_priv_key).'&issuer='.$url.'&counter='.$this->getUserCounter($user->ID);
510
511 return $encode;
512 }
513
514 /**
515 * See if HOTP is off sync, and if show, print out a message
516 */
517 public function tfa_show_hotp_off_sync_message() {
518
519 global $current_user;
520 $is_off_sync = get_user_meta($current_user->ID, 'tfa_hotp_off_sync', true);
521 if (!$is_off_sync) return;
522
523 ?>
524 <div class="error">
525 <h3><?php _e('Two Factor Authentication re-sync needed', 'two-factor-authentication');?></h3>
526 <p>
527 <?php _e('You need to resync your device for Two Factor Authentication since the OTP you last used is many steps ahead of the server.', 'two-factor-authentication'); ?>
528 <br>
529 <?php _e('Please re-sync or you might not be able to log in if you generate more OTPs without logging in.', 'two-factor-authentication');?>
530 <br><br>
531 <a href="<?php echo esc_url(wp_nonce_url('admin.php?page='. $this->tfa->get_user_settings_page_slug() .'&warning_button_clicked=1', 'tfaresync', 'resyncnonce')); ?>" class="button"><?php _e('Click here and re-scan the QR-Code', 'two-factor-authentication');?></a>
532 </p>
533 </div>
534
535 <?php
536
537 }
538
539 /**
540 * Runs upon the WP action plugins_loaded
541 */
542 public function plugins_loaded() {
543 $this->time_window_size = apply_filters('simbatfa_time_window_size', 30);
544 $this->check_back_time_windows = apply_filters('simbatfa_check_back_time_windows', 2);
545 $this->check_forward_time_windows = apply_filters('simbatfa_check_forward_time_windows', 1);
546 $this->check_forward_counter_window = apply_filters('simbatfa_check_forward_counter_window', 20);
547
548 $this->salt_prefix = defined('AUTH_SALT') ? AUTH_SALT : wp_salt('auth');
549 $this->pw_prefix = defined('AUTH_KEY') ? AUTH_KEY : get_site_option('auth_key');
550 }
551
552 /**
553 * Generate the current code for a specified user
554 *
555 * @param $user_id Integer - WordPress user ID
556 *
557 * @return String|Boolean - false if not set up
558 */
559 public function get_current_code($user_id) {
560
561 $tfa_priv_key_64 = get_user_meta($user_id, 'tfa_priv_key_64', true);
562
563 if (!$tfa_priv_key_64) return false;
564
565 return $this->generateOTP($user_id, $tfa_priv_key_64);
566
567 }
568
569 public function print_default_hmac_radios() {
570
571 $setting = $this->tfa->get_option('tfa_default_hmac');
572 if (!$setting) $setting = $this->default_hmac;
573
574 $types = array('totp' => __('TOTP (time based - most common algorithm; used by Google Authenticator)', 'two-factor-authentication'), 'hotp' => __('HOTP (event based)', 'two-factor-authentication'));
575
576 foreach ($types as $id => $name) {
577 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";
578 }
579 }
580
581 public function generateOTP($user_ID, $key_b64, $length = 6, $counter = false) {
582
583 $length = $length ? (int)$length : 6;
584
585 $key = $this->decryptString($key_b64, $user_ID);
586 $alg = $this->get_user_otp_algorithm($user_ID);
587
588 if ('hotp' == $alg) {
589 $db_counter = $this->getUserCounter($user_ID);
590
591 $counter = $counter ? $counter : $db_counter;
592 $otp_res = $this->otp_helper->generateByCounter($key, $counter);
593 } else {
594 //time() is supposed to be UTC
595 $time = $counter ? $counter : time();
596 $otp_res = $this->otp_helper->generateByTime($key, $this->time_window_size, $time);
597 }
598 $code = $otp_res->toHotp($length);
599
600 return $code;
601 }
602
603 /**
604 * Generate a list of OTP codes based on the user, key and time window
605 *
606 * @param Integer $user_ID - user ID
607 * @param String $key_b64 - the user's private key, in base64 format
608 *
609 * @return Array
610 */
611 private function generate_otps_for_login_check($user_ID, $key_b64) {
612 $key = trim($this->decryptString($key_b64, $user_ID));
613 $alg = $this->get_user_otp_algorithm($user_ID);
614
615 if ('totp' == $alg) {
616 $otp_res = $this->otp_helper->generateByTimeWindow($key, $this->time_window_size, -1*$this->check_back_time_windows, $this->check_forward_time_windows);
617 } elseif ('hotp' == $alg) {
618
619 $counter = $this->getUserCounter($user_ID);
620
621 $otp_res = array();
622
623 for ($i = 0; $i < $this->check_forward_counter_window; $i++) {
624 $otp_res[] = $this->otp_helper->generateByCounter($key, $counter+$i);
625 }
626 }
627 return $otp_res;
628 }
629
630
631 /**
632 * Generate a private key for the user.
633 *
634 * @param Integer $user_id - WordPress user ID
635 * @param Boolean|String $key
636 *
637 * @return String
638 */
639 public function addPrivateKey($user_id, $key = false) {
640
641 // To work with Google Authenticator it has to be 10 bytes = 16 chars in base32
642 $code = $key ? $key : strtoupper($this->randString(10));
643
644 // Encrypt the key
645 $code = $this->encryptString($code, $user_id);
646
647 // Add private key to usermeta
648 update_user_meta($user_id, 'tfa_priv_key_64', $code);
649
650 $alg = $this->get_user_otp_algorithm($user_id);
651
652 // This hook is used for generation of emergency codes to accompany the key
653 do_action('simba_tfa_adding_private_key', $alg, $user_id, $code, $this);
654
655 $this->changeUserAlgorithmTo($user_id, $alg);
656
657 return $code;
658 }
659
660 /**
661 * 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
662 */
663 public function potentially_port_private_keys() {
664
665 $simba_tfa_priv_key_format = get_site_option('simba_tfa_priv_key_format', false);
666
667 if ($simba_tfa_priv_key_format >= 1 || !function_exists('openssl_encrypt')) return;
668
669 $attempts = 0;
670 $successes = 0;
671
672 error_log("TFA: Beginning attempt to port private key encryption over to openssl");
673
674 global $wpdb;
675
676 $sql = "SELECT user_id, meta_value FROM ".$wpdb->usermeta." WHERE meta_key = 'tfa_priv_key_64'";
677
678 $user_results = $wpdb->get_results($sql);
679
680 foreach ($user_results as $u) {
681 $dec_openssl = $this->decryptString($u->meta_value, $u->user_id, true);
682
683 $ported = false;
684 if ('' == $dec_openssl) {
685
686 $attempts++;
687
688 $dec_default = $this->decryptString($u->meta_value, $u->user_id);
689
690 if ('' != $dec_default) {
691
692 $enc = $this->encryptString($dec_default, $u->user_id);
693
694 if ($enc) {
695
696 $ported = true;
697 $successes++;
698 update_user_meta($u->user_id, 'tfa_priv_key_64', $enc);
699 }
700 }
701
702 }
703
704 if ($ported) {
705 error_log("TFA: Successfully ported the key for user with ID ".$u->user_id." over to openssl");
706 } else {
707 error_log("TFA: Failed to port the key for user with ID ".$u->user_id." over to openssl");
708 }
709 }
710
711 if ($attempts == 0 || $successes > 0) update_site_option('simba_tfa_priv_key_format', 1);
712
713 }
714
715 public function getPrivateKeyPlain($enc, $user_ID) {
716 $dec = $this->decryptString($enc, $user_ID);
717 $this->potentially_port_private_keys();
718 return $dec;
719 }
720
721 /**
722 * @param Integer $user_id - WP user ID
723 * @param Boolean $generate_if_empty - generate some new codes if the list is empty
724 *
725 * @return String - human-usable codes, separated by ', ' (or a human-readable message, if there were none)
726 */
727 public function get_emergency_codes_as_string($user_id, $generate_if_empty = false) {
728
729 $codes = get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true);
730 if (!is_array($codes)) $codes = array();
731
732 if ($generate_if_empty && empty($codes)) {
733 $tfa_priv_key = get_user_meta($user_id, 'tfa_priv_key_64', true);
734 $algorithm = get_user_meta($user_id, 'tfa_algorithm_type', true);
735 do_action('simba_tfa_emergency_codes_empty', $algorithm, $user_id, $tfa_priv_key, $this);
736 $codes = get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true);
737 if (!is_array($codes)) $codes = array();
738 }
739
740 $emergency_str = '';
741
742 foreach ($codes as $p_code) {
743 $emergency_str .= $this->decryptString($p_code, $user_id).', ';
744 }
745
746 $emergency_str = rtrim($emergency_str, ', ');
747
748 $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>';
749
750 return $emergency_str;
751 }
752
753 /**
754 * Check a code for a user (checks the code only - does not check activation status etc.)
755 *
756 * @param Integer $user_id - WP user ID
757 * @param String $user_code - the code to check
758 * @param Boolean $allow_emergency_code - whether to check against emergency codes
759 *
760 * @return Boolean
761 */
762 public function check_code_for_user($user_id, $user_code, $allow_emergency_code = true) {
763
764 $tfa_priv_key = get_user_meta($user_id, 'tfa_priv_key_64', true);
765 // $tfa_last_login = get_user_meta($user_id, 'tfa_last_login', true); // Unused
766 $tfa_last_pws_arr = get_user_meta($user_id, 'tfa_last_pws', true);
767 $tfa_last_pws = @$tfa_last_pws_arr ? $tfa_last_pws_arr : array();
768 $alg = $this->get_user_otp_algorithm($user_id);
769
770 $current_time_window = intval(time()/30);
771
772 //Give the user 1,5 minutes time span to enter/retrieve the code
773 //Or check $this->check_forward_counter_window number of events if hotp
774 $codes = $this->generate_otps_for_login_check($user_id, $tfa_priv_key);
775
776 //A recently used code was entered; that's not OK.
777 if (in_array($this->hash($user_code, $user_id), $tfa_last_pws)) return false;
778
779 $match = false;
780 foreach ($codes as $index => $code) {
781 if (trim($code->toHotp(6)) == trim($user_code)) {
782 $match = true;
783 $found_index = $index;
784 break;
785 }
786 }
787
788 // Check emergency codes
789 if (!$match) {
790 $emergency_codes = $allow_emergency_code ? get_user_meta($user_id, 'simba_tfa_emergency_codes_64', true) : array();
791
792 if (!$emergency_codes) return $match;
793
794 $dec = array();
795 foreach ($emergency_codes as $emergency_code)
796 $dec[] = trim($this->decryptString(trim($emergency_code), $user_id));
797
798 $in_array = array_search($user_code, $dec);
799 $match = $in_array !== false;
800
801 //Remove emergency code
802 if ($match) {
803 array_splice($emergency_codes, $in_array, 1);
804 update_user_meta($user_id, 'simba_tfa_emergency_codes_64', $emergency_codes);
805 do_action('simba_tfa_emergency_code_used', $user_id, $emergency_codes);
806 }
807
808 } else {
809 //Add the used code as well so it cant be used again
810 //Keep the two last codes
811 $tfa_last_pws[] = $this->hash($user_code, $user_id);
812 $nr_of_old_to_save = $alg == 'hotp' ? $this->check_forward_counter_window : $this->check_back_time_windows;
813
814 if (count($tfa_last_pws) > $nr_of_old_to_save) array_splice($tfa_last_pws, 0, 1);
815
816 update_user_meta($user_id, 'tfa_last_pws', $tfa_last_pws);
817 }
818
819 if ($match) {
820 //Save the time window when the last successful login took place
821 update_user_meta($user_id, 'tfa_last_login', $current_time_window);
822
823 //Update the counter if HOTP was used
824 if ($alg == 'hotp') {
825 $counter = $this->getUserCounter($user_id);
826
827 $enc_new_counter = $this->encryptString($counter+1, $user_id);
828 update_user_meta($user_id, 'tfa_hotp_counter', $enc_new_counter);
829
830 if ($found_index > 10) update_user_meta($user_id, 'tfa_hotp_off_sync', 1);
831 }
832 }
833
834 return $match;
835
836 }
837
838 public function getUserCounter($user_ID) {
839 $enc_counter = get_user_meta($user_ID, 'tfa_hotp_counter', true);
840 return $enc_counter ? trim($this->decryptString(trim($enc_counter), $user_ID)) : '';
841 }
842
843 public function changeUserAlgorithmTo($user_id, $new_algorithm) {
844 update_user_meta($user_id, 'tfa_algorithm_type', $new_algorithm);
845 delete_user_meta($user_id, 'tfa_hotp_off_sync');
846
847 $counter_start = rand(13, 999999999);
848 $enc_counter_start = $this->encryptString($counter_start, $user_id);
849
850 if ('hotp' == $new_algorithm) {
851 update_user_meta($user_id, 'tfa_hotp_counter', $enc_counter_start);
852 } else {
853 delete_user_meta($user_id, 'tfa_hotp_counter');
854 }
855 }
856
857 /**
858 * Whether HOTP or TOTP is being used
859 *
860 * @param Integer|Boolean $user_id - WordPress user ID, or false for the site-wide default
861 *
862 * @return String - 'hotp' or 'totp'
863 */
864 public function get_user_otp_algorithm($user_id = false) {
865
866 $setting = $user_id ? get_user_meta($user_id, 'tfa_algorithm_type', true) : false;
867
868 $default_hmac = $this->tfa->get_option('tfa_default_hmac');
869 if (!$default_hmac) $default_hmac = $this->default_hmac;
870
871 return $setting ? $setting : $default_hmac;
872 }
873
874 private function get_iv_size() {
875 // mcrypt first, for backwards compatibility
876 if (function_exists('mcrypt_get_iv_size')) {
877 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);
878 } elseif (function_exists('openssl_cipher_iv_length')) {
879 return openssl_cipher_iv_length('AES-128-CBC');
880 }
881 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
882 }
883
884 private function encrypt($key, $string, $iv) {
885 // Prefer OpenSSL, because it uses correct padding, and its output can be decrypted by mcrypt - whereas, the converse is not true
886 if (function_exists('openssl_encrypt')) {
887 return openssl_encrypt($string, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
888 } elseif (function_exists('mcrypt_encrypt')) {
889 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);
890 }
891 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
892 }
893
894 private function decrypt($key, $enc, $iv, $force_openssl = false) {
895 // 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
896 if (function_exists('mcrypt_decrypt') && !$force_openssl) {
897 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);
898 } elseif (function_exists('openssl_decrypt')) {
899 $decrypted = openssl_decrypt($enc, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
900 if (false === $decrypted && !$force_openssl) {
901 $extra = function_exists('wp_debug_backtrace_summary') ? " backtrace: ".wp_debug_backtrace_summary() : '';
902 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");
903 }
904 return $decrypted;
905 }
906 if ($force_openssl) return false;
907 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
908 }
909
910 public function encryptString($string, $salt_suffix) {
911 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
912
913 $iv_size = $this->get_iv_size();
914 $iv = $GLOBALS['simba_two_factor_authentication']->random_bytes($iv_size);
915
916 $enc = $this->encrypt($key, $string, $iv);
917
918 if (false === $enc) return false;
919
920 $enc = $iv.$enc;
921 $enc_b64 = base64_encode($enc);
922 return $enc_b64;
923 }
924
925 private function decryptString($enc_b64, $salt_suffix, $force_openssl = false) {
926 $key = $this->hashAndBin($this->pw_prefix.$salt_suffix, $this->salt_prefix.$salt_suffix);
927
928 $iv_size = $this->get_iv_size();
929 $enc_conc = bin2hex(base64_decode($enc_b64));
930
931 $iv = hex2bin(substr($enc_conc, 0, $iv_size*2));
932 $enc = hex2bin(substr($enc_conc, $iv_size*2));
933
934 $string = $this->decrypt($key, $enc, $iv, $force_openssl);
935
936 // Remove padding bytes
937 return rtrim($string, "\x00..\x1F");
938 }
939
940 private function hashAndBin($pw, $salt) {
941 $key = $this->hash($pw, $salt);
942 $key = pack('H*', $key);
943 // 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
944 // 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.
945 // In summary: this isn't encryption, and is not intended to be.
946 return str_repeat(chr(0), 16);
947 }
948
949 private function hash($pw, $salt) {
950 //$hash = hash_pbkdf2('sha256', $pw, $salt, 10);
951 //$hash = crypt($pw, '$5$'.$salt.'$');
952 $hash = md5($salt.$pw);
953 return $hash;
954 }
955
956 private function randString($len = 10) {
957 $chars = '23456789QWERTYUPASDFGHJKLZXCVBNM';
958 $chars = str_split($chars);
959 shuffle($chars);
960 if (function_exists('random_int')) {
961 $code = '';
962 for ($i = 1; $i <= $len; $i++) {
963 $code .= $chars[random_int(0, count($chars)-1)];
964 }
965 } else {
966 $code = implode('', array_splice($chars, 0, $len));
967 }
968 return $code;
969 }
970
971 public function setUserHMACTypes() {
972 trigger_error("Deprecated: setUserHMACTypes() does nothing: remove any calls to it");
973 }
974
975 }
976