Current File : //var/webuzo-data/roundcube/public_html/plugins/webuzo/soft2fa/soft2fa.php |
<?php
class soft2fa{
private $rc;
private $webuzo_user = '';
private $email = '';
private $session_key = 'soft2fa_plugin';
// Reference to the main webuzo plugin instance
private $plugin;
public function __construct($plugin){
$this->plugin = $plugin;
}
public function initialize(){
global $globals;
// Load roundcube instance
$this->rc = rcmail::get_instance();
$this->webuzo_user = $this->rc->config->get('webuzo_user');
if(empty($this->webuzo_user)){
return;
}
// Include necessary libs
include_once($globals['path'].'/lib/classes/hotp.php');
include_once($globals['path'].'/lib/classes/Base32.php');
include_once($globals['path'].'/lib/IPv6/IPv6.php');
$this->conf_path = '/var/webuzo/users/'. $this->webuzo_user.'/rc_2fa';
$this->email = !empty($this->rc->user->data['username']) ? $this->rc->user->data['username'] : '';
// Include locals
$this->plugin->add_texts('soft2fa/localization/');
// Add required hooks
$this->plugin->add_hook('startup', [$this, 'startup']);
$this->plugin->add_hook('login_after', [$this, 'login_after']);
// Load settings
if(!empty($this->is_login()) && $this->rc->task == 'settings') {
// Init plugin
$this->plugin->register_action('plugin.soft2fa', [$this, 'soft2fa_settings']);
// Register menu
$this->plugin->add_hook('settings_actions', [$this, 'settings_actions']);
// Handle AJAX
$this->plugin->register_action('plugin.soft2fa_post_data', [$this, 'soft2fa_post_data']);
$this->plugin->register_action('plugin.soft2fa_download_backup_codes', [$this, 'soft2fa_download_backup_codes']);
}
}
private function is_login(){
if(empty($_SESSION['user_id']) || empty($_SESSION['password'])){
return false;
}
return true;
}
public function startup() {
// Skip if already logged in and 2FA is verified or IP is in whitelist.
if (!empty($this->is_login()) && (!empty($_SESSION['soft2fa_plugin']['soft2fa_verify']) || $this->check_whitelist())) {
// Show verified msg if already logged in and trying to access
if($this->rc->action ==='plugin.verify2fa'){
$this->soft2fa_verification_ui(true);
}
return;
}
// Fetch 2FA and backup code data
$app_auth = $this->get_2fa_auth_data($this->email);
$backup_codes = $this->get_2fa_backup_codes($this->email);
// Skip if both 2FA methods are disable
if (empty($app_auth['2fa_status']) && empty($backup_codes)) {
return;
}
// If not login task, redirect to 2FA verification
if ($this->rc->task != 'login') {
$this->rc->output->redirect(['_task' => 'login', '_action' => 'plugin.verify2fa']);
}
// 2FA verification handler
if ($this->rc->task === 'login' && $this->rc->action === 'plugin.verify2fa') {
$token = rcube_utils::get_input_value('_token', rcube_utils::INPUT_POST);
if (empty($token)) {
// Show the 2FA UI
$this->soft2fa_verification_ui();
return;
}
if (!rcmail::get_instance()->check_request()) {
$this->rc->output->show_message($this->plugin->gettext('csrf_error'), 'error');
$this->soft2fa_verification_ui();
return;
}
$code_input = trim(rcube_utils::get_input_value('_2fa_code', rcube_utils::INPUT_POST));
$encoded_input = base64_encode($code_input);
$index = array_search($encoded_input, array_column($backup_codes, 'code'));
// Check OTP or valid backup code
$is_otp_valid = (!empty($app_auth['2fa_otp']) && $app_auth['2fa_otp'] === $code_input);
$is_valid_backup_code = ($index !== false);
if (!empty($is_otp_valid) || !empty($is_valid_backup_code)) {
// If backup code used, mark it as used
if (!empty($is_valid_backup_code)) {
if (!empty($backup_codes[$index]['status'])) {
$this->rc->output->show_message($this->plugin->gettext('code_used'), 'warning');
$this->soft2fa_verification_ui();
return;
}
// Mark the code as used
$data = $this->plugin->loaddata($this->conf_path);
$data[$this->email]['2fa']['backup_codes'][$index]['status'] = true;
$resp = $this->plugin->writedata($this->conf_path, $data);
if(empty($resp)){
$this->rc->output->show_message($this->plugin->gettext('write_error'), 'warning');
$this->soft2fa_verification_ui();
return;
}
}
// Set session
$this->rc->session->append($this->session_key,'soft2fa_verify', true);
$this->rc->output->redirect(['_task' => 'mail']);
return;
}
// Invalid code
$this->rc->output->show_message($this->plugin->gettext('invalid_code'), 'warning');
$this->soft2fa_verification_ui();
}
}
// Does auto loggedin?
function login_after(){
// Do not proceed further if not autologin
if (!isset($_POST['_autologin'])) {
return;
}
$email_path = '/var/webuzo/users/'.$this->webuzo_user.'/emails';
$emails = $this->plugin->loaddata($email_path);
$email = !empty($this->rc->user->data['username']) ? $this->rc->user->data['username'] : '';
$stored_hash = !empty($email) && !empty($emails[$email]['password']) ? $emails[$email]['password'] : '';
$rc_passwd_hash = sha1($this->rc->decrypt($_SESSION['password']));
// Show the 2FA page if the stored password is empty or if the session password matches the password in the Webuzo email file
if (empty($stored_hash) || $stored_hash == $rc_passwd_hash) {
return;
}
$this->rc->session->append($this->session_key,'soft2fa_verify', true);
}
private function soft2fa_verification_ui($is_loggedin = false){
$this->rc->output->set_env('soft2fa_verified', $is_loggedin);
$base_url = $this->rc->url([''], true);
if(!empty($is_loggedin)){
$base_url = $this->rc->url([
'_task' => 'mail',
'_mbox' => 'INBOX'
]);
}
$this->rc->output->set_env('rc_webmail_url', $base_url);
$this->rc->output->add_handlers(['soft2fa_ui_form' => [$this, 'validate_2fa_form']]);
$this->rc->output->set_pagetitle($this->plugin->gettext('soft2fa_verify'));
$this->rc->output->send('webuzo.soft2fa_verify');
}
public function settings_actions($args){
$args['actions'][] = [
'action' => 'plugin.soft2fa',
'class' => 'license',
'label' => 'two_factor_auth',
'title' => 'two_factor_auth',
'domain' => 'webuzo',
];
return $args;
}
public function soft2fa_settings(){
$this->rc->output->set_pagetitle($this->plugin->gettext('two_factor_auth'));
// Load JS & CSS
$this->plugin->include_stylesheet('soft2fa/css/soft2fa.css');
$this->plugin->include_script('soft2fa/js/jquery.qrcode.min.js');
$this->plugin->include_script('soft2fa/js/soft2fa.js');
$skin = $this->rc->config->get('skin');
if ($skin === 'larry' || $skin === 'classic') {
$this->plugin->include_stylesheet('soft2fa/css/soft2fa_legacy.css');
}
// Handle JS labels
$this->rc->output->add_label(
"webuzo.auth_confirm_dialog",
'webuzo.auth_reset_dialog',
'webuzo.backup_code_enable',
'webuzo.backup_code_disable',
'webuzo.backup_code_regen',
'webuzo.clipboard_success',
'webuzo.clipboard_error',
'webuzo.ip_delete_confirm_dialog'
);
// Handle 2FA setup form
$this->plugin->register_handler('plugin.body', [$this, 'soft2fa_settings_form']);
$this->rc->output->send('plugin');
}
public function validate_2fa_form($attrib){
// Create 2FA input field
$field = new html_inputfield([
'name' => '_2fa_code',
'id' => '2fakey',
'required' => true,
'size' => 6,
'class' => 'form-control',
'placeholder' => $this->plugin->gettext('2fa_placeholder')
]);
$form_content = html::div('col-md-12',
$field->show()
).
html::div('col-md-12 mt-4',
html::tag(
'button', ['type' => 'submit', 'name' => '2fa_submit' ,'class' => 'button mainaction submit btn btn-primary btn-md text-uppercase w-100'], 'Verify Code'
)
);
return $form_content;
}
public function soft2fa_settings_form(){
$data = $this->plugin->loaddata($this->conf_path);
$app2fa = $this->get_2fa_auth_data($this->email);
$auth_status = !empty($data[$this->email]['2fa']['auth']['status']) ? $data[$this->email]['2fa']['auth']['status'] : false;
$backup_codes = !empty($data[$this->email]['2fa']['backup_codes']) ? $data[$this->email]['2fa']['backup_codes'] : [];
$preferences = [
'authenticator' => [
'icon' => 'qrcode-icon',
'status' => $auth_status,
'en_text' => $this->plugin->gettext('en_auth'),
'dis_text' => $this->plugin->gettext('dis_auth'),
'name' => $this->plugin->gettext('authenticator'),
'onclick' => !empty($auth_status) ? 'app_authenticator(2)' : 'app_authenticator()'
],
'backup_codes' => [
'icon' => 'shild-icon',
'status' => !empty($backup_codes) ? true : false,
'en_text' => $this->plugin->gettext('gen_codes'),
'dis_text' => $this->plugin->gettext('view_codes'),
'name' => $this->plugin->gettext('backup_codes'),
'onclick' => !empty($backup_codes) ? 'app_backup_code()' : 'app_backup_code(1)',
]
];
$pref_options = '';
foreach ($preferences as $key => $pref) {
$btn_text = !empty($pref['status']) ? $pref['dis_text'] : $pref['en_text'];
$action_btn = new html_button([
'class' => 'button w-100 soft-btn',
'onclick' => $pref['onclick'],
]);
$pref_options .= html::div('pref-container',
html::div('pref-row',
html::div('pref-col pref-100',
html::div('pref-row option_preference ',
html::div('pref-col pref-75',
html::span($pref['icon'].' pref-icon','').
html::span('pref-title', $pref['name'])
).
html::div('pref-col pref-25', $action_btn->show($btn_text))
)
)
)
);
}
$auth_model = '';
if(empty($auth_status)){
$secret_field = new html_inputfield(['name' => 'soft_2fa_code_key', 'id' => 'soft2fa_app_key', 'disabled' => 'disabled']);
$secret32_field = new html_inputfield(['name' => 'soft_2fa_code_key_32', 'id' => 'soft2fa_app_key_32', 'disabled' => 'disabled']);
$verify_fields = '';
for ($i=0;$i < 6; $i++) {
$tmp_field = new html_inputfield(['name' => 'soft_2fa_code_key', 'class'=> 'form-control otp-input', 'maxlength' => '1']);
$verify_fields .= html::div('col-sm-2', $tmp_field->show());
}
$body = html::div('row px-4 pb-0',
html::div('col-md-12 pt-2 text-center',
html::label('',
html::div(['id' => 'app_qr', 'data-qrcode' => htmlspecialchars($app2fa['2fa_qr'])])
)
).
html::div('col-md-12 mt-2',
$this->plugin->gettext('qr_code_step1').html::br().
html::div('mt-2 text-center', html::label(['class' => 'secret_code', 'id' => 'secret_code'], $app2fa['2fa_key32']).html::div(['onclick' => 'copy_code(this)', 'class' => 'copy-icon', 'id' => 'qr_auth_code', 'title' => 'Copy code', 'data-code' => base64_encode($app2fa['2fa_key32'])],''))
).
html::div('col-md-12 mt-2',
$this->plugin->gettext('qr_code_step2')
).
html::div('col-md-12 mt-4',
html::div('row otp-wrapper',
$verify_fields
)
).
html::div('col-md-12 mt-4 text-right',
$this->plugin->gettext('refresh_qr').html::div(['class' => 'reload-icon', 'title' => 'Refresh QR code', 'onclick' => 'app_authenticator(3)'], '')
)
);
$footer = html::tag('button', [
'type' => 'button',
'class' => 'btn btn-danger',
'data-dismiss' => 'modal'
], $this->plugin->gettext('cancel')) .
html::tag('button', [
'type' => 'button',
'class' => 'btn btn-primary soft-btn',
'onclick' => 'app_authenticator(1)'
], $this->plugin->gettext('activate')
);
$auth_model = $this->soft2fa_model('authenticator_model',$this->plugin->gettext('auth_model_title'), $body, $footer);
}
$backup_model = '';
if(!empty($backup_codes) && is_array($backup_codes)){
$codes = '';
$all_exipred = true;
foreach ($backup_codes as $key => $code) {
$codes .= html::div(['class' => 'col-md-6 backup-code text-center my-2', 'data-used' => $code['status'], 'title' => (!empty($code['status']) ? $this->plugin->gettext('code_used_title') : '')], base64_decode($code['code']));
if(empty($code['status'])){
$all_exipred = false;
}
}
$body = html::div('row',
html::div('col-md-12',
$this->plugin->gettext('backup_codes_title')
).
html::div('col-md-12',
html::div('row backup-codes-wrap',
$codes
)
).
html::div('col-md-12',
html::tag('ul',null,
html::tag('li', null, $this->plugin->gettext('backup_code_warn1')).
html::tag('li', null, $this->plugin->gettext('backup_code_warn2'))
)
).
html::div('col-md-12 mt-2 text-right',
$this->plugin->gettext('regen_backup_codes').html::div(['class' => 'reload-icon', 'onclick' => 'app_backup_code(3)'], '')
)
);
$download_url = !empty($all_exipred) ? 'javascript:void(0);' : $this->rc->url([
'_task' => 'settings',
'_action' => 'plugin.soft2fa_download_backup_codes',
]);
$footer = html::tag('button', [
'type' => 'button',
'class' => 'btn btn-danger',
'data-dismiss' => 'modal'
], $this->plugin->gettext('cancel')
).
html::tag('a', [
'href' => $download_url,
'class' => 'btn btn-primary soft-btn'.(!empty($all_exipred) ? 'disabled' : ''),
], $this->plugin->gettext('download_codes')
).
html::tag('button', [
'type' => 'button',
'class' => 'btn btn-primary soft-btn',
'onclick' => 'app_backup_code(2)'
], $this->plugin->gettext('deactivate')
);
$backup_model = $this->soft2fa_model('backup_codes_model', $this->plugin->gettext('backup_codes_model_title'), $body, $footer);
}
$table = new html_table(array(
'class' => 'whitelist-table w-100',
'cols' => 4,
'border' => 0,
));
// Add the table header
$table->add_header('start-ip', rcube::Q('Start IP'));
$table->add_header('end-ip', rcube::Q('End IP'));
$table->add_header('date', rcube::Q('Date'));
$table->add_header('options', rcube::Q('Options'));
if(!empty($data[$this->email]['2fa']['whitelist'])){
foreach ($data[$this->email]['2fa']['whitelist'] as $key => $ip) {
// $table->add_row();
$table->add('start-ip', rcube::Q($ip['start']));
$table->add('end-ip', rcube::Q($ip['end']));
$table->add('date', rcube::Q(date('d/m/Y', $ip['time'])));
$table->add('options', html::a([
'href' => 'javascript:void(0);',
'onclick' => "handle_ip(".$key.")",
'class' => 'btn btn-danger btn-sm delete-link',
], 'Delete'));
}
}
$w_start_ip_field = new html_inputfield([
'name' => '_w_start_ip',
'id' => 'w_start_ip',
'required' => true,
'size' => 6,
'class' => 'form-control',
'placeholder' => $this->plugin->gettext('ip_range')
]);
$w_end_ip_field = new html_inputfield([
'name' => '_w_end_ip',
'id' => 'w_end_ip',
'size' => 6,
'class' => 'form-control',
'placeholder' => $this->plugin->gettext('ip_range')
]);
$w_form_content = html::div('pref-row mb-0',
html::div('pref-col pref-100 px-0',
html::label('', $this->plugin->gettext('ip_start_range'))
).
html::div('pref-col pref-100 px-0',
$w_start_ip_field->show()
).
html::div('pref-col pref-100 px-0 mt-4',
html::label('', $this->plugin->gettext('ip_end_range'))
).
html::div('pref-col pref-100 px-0',
$w_end_ip_field->show()
).
html::div('pref-col pref-100 mt-4 flex-right px-0',
html::tag(
'button', ['type' => 'submit', 'name' => '2fa_submit' ,'class' => 'button soft-btn btn btn-secondary'], $this->plugin->gettext('add_ip_range')
)
)
);
$w_form = $this->rc->output->form_tag([
'id' => 'whitelist-form',
'name' => 'whitelist-form',
'class' => 'w-100 mt-2',
'method' => 'post',
'action' => '',
],
$w_form_content
);
$whitelist_content = html::div('pref-container mb-30',
html::div('pref-row',
html::div('pref-col pref-50 option_preference p-4 mt-3',
html::div('w-100',
html::div('pref-col',
html::span('pref-title', $this->plugin->gettext('w_ips'))
).
html::div('pref-col',
$w_form
)
)
).
html::div('pref-col pref-50 option_preference p-4 mt-3',
html::div('w-100',
html::div('pref-col',
html::div('pref-title h-auto', $this->plugin->gettext('w_ip_list'))
).
html::div('pref-col mt-4',
html::div('w_ip_list w-100',
$table->show()
)
)
)
)
)
);
$plugin_content = html::div('soft2fa-wrapper',
$auth_model.
$backup_model.
html::div('pref-main-title',
$this->plugin->gettext('two_factor_auth')
).
html::div('preference-content',
$pref_options.
$whitelist_content
).
html::div('advance-settings',
)
);
return $plugin_content;
$this->rc->output->send('plugin');
}
public function soft2fa_download_backup_codes(){
// ensure user is logged in
if (empty($this->is_login())) {
header('HTTP/1.1 403 Forbidden');
exit('Unauthorized access');
}
$codes = $this->get_2fa_backup_codes($this->email);
if (empty($codes)) {
header('HTTP/1.1 404 Not Found');
exit('No backup codes found');
}
$content = '';
foreach ($codes as $code) {
if (empty($code['status'])) {
$content .= base64_decode($code['code']) . "\n";
}
}
if(empty($content)){
header('HTTP/1.1 404 Not Found');
exit('All backup codes are expired!');
}
$content = "Generated codes
---------------
Username: ".$this->email."\n\n".
$content;
$filename = 'backup_codes_'.str_ireplace(['.','@'],'_', $this->email);
header('Content-Type: text/plain');
header('Content-Disposition: attachment; filename="'.$filename);
header('Content-Length: ' . strlen($content));
echo $content;
exit;
}
private function soft2fa_model($id= '', $title = '', $content = '', $footer = '', $size = 'md'){
$modal = html::div(
[
'class' => 'modal fade',
'id' => $id,
'tabindex' => '-1',
'aria-hidden' => 'true'
],
html::div('modal-dialog modal-'.$size, html::div('modal-content',
html::div('modal-header',
html::tag('h5', 'modal-title', $title).
html::tag('button', [
'type' => 'button',
'class' => 'btn-close',
'data-dismiss' => 'modal',
'aria-label' => 'Close'
])
).
html::div('modal-body', $content).
html::div('modal-footer', $footer)
)
)
);
return $modal;
}
public function soft2fa_post_data(){
if(empty($this->is_login()) || $this->rc->task != 'settings' || $this->rc->action != 'plugin.soft2fa_post_data') {
return;
}
// get 2fa info
$type = rcube_utils::get_input_value('type', rcube_utils::INPUT_POST);
$data = $this->plugin->loaddata($this->conf_path);
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_POST);
if(empty($action)){
$this->return_JSON(['error' => $this->plugin->gettext('invalid_action')]);
return;
}
// Handle authenticator AJAX requests
if($type == 'auth'){
if(in_array($action, ['enable', 'disable'])){
if(!empty($data[$this->email]['2fa']['auth']['status']) && $action == 'enable'){
$this->return_JSON(['error' => $this->plugin->gettext('auth_enabled')]);
return;
}
if($action == 'enable'){
$post_otp = rcube_utils::get_input_value('otp', rcube_utils::INPUT_POST);
$app2fa = $this->get_2fa_auth_data($this->email);
$otp = $app2fa['2fa_otp'];
if((int)$post_otp != $otp){
$this->return_JSON(['error' => $this->plugin->gettext('invalid_code')]);
return;
}
$data[$this->email]['2fa']['auth']['status'] = true;
$this->rc->session->append($this->session_key,'soft2fa_verify', true);
}else{
unset( $data[$this->email]['2fa']['auth'] );
$this->rc->session->remove('soft2fa_verify');
}
$resp = $this->plugin->writedata($this->conf_path, $data);
if(!$resp){
$this->rc->session->remove('soft2fa_verify');
$this->return_JSON(['error' => $this->plugin->gettext('write_error')]);
return;
}
$this->return_JSON(['auth_response' => 1, 'message' => ($action == 'enable' ? $this->plugin->gettext('auth_activate') : $this->plugin->gettext('auth_deactivate'))]);
return;
}
if($action == 'regen_qr'){
$app2fa = $this->get_2fa_auth_data($this->email, true);
$resp['reset_qr'] = 0;
$resp['message'] = $this->plugin->gettext('error');
if(!empty($app2fa)){
$resp = [
'regen_qr_response' => 1,
'qr_code' => htmlspecialchars($app2fa['2fa_qr']),
'auth_code' => base64_encode($app2fa['2fa_key32']),
'message' => $this->plugin->gettext('qr_reset_success'),
'prevent_refresh' => 1
];
}
$this->return_JSON($resp);
return;
}
$this->return_JSON(['error' => $this->plugin->gettext('invalid_action')]);
return;
}
// Handle backup codes AJAX requests
if($type == 'backup_codes'){
if(!in_array($action, ['enable', 'regen_backup_codes', 'disable'])){
$this->return_JSON(['error' => $this->plugin->gettext('invalid_action')]);
return;
}
if(!empty($data[$this->email]['2fa']['backup_codes']) && $action == 'enable'){
$this->return_JSON(['error' => $this->plugin->gettext('backup_codes_exists')]);
return;
}
if(empty($data[$this->email]['2fa']['backup_codes']) && $action == 'disable'){
$this->return_JSON(['error' => $this->plugin->gettext('backup_code_deactivated')]);
return;
}
if($action == 'enable' || $action == 'regen_backup_codes'){
$resp = $this->get_2fa_backup_codes($this->email, true);
if(empty($resp)){
$this->return_JSON(['error' => $this->plugin->gettext('write_error')]);
return;
}
$this->rc->session->append($this->session_key,'soft2fa_verify', true);
$this->return_JSON(['message' => ($action == 'enable' ? $this->plugin->gettext('backup_code_activated') : $this->plugin->gettext('backup_code_regenerated'))]);
return;
}
unset($data[$this->email]['2fa']['backup_codes']);
$resp = $this->plugin->writedata($this->conf_path, $data);
if(empty($resp)){
$this->return_JSON(['error' => $this->plugin->gettext('write_error')]);
return;
}
$this->rc->session->remove('soft2fa_verify');
$this->return_JSON(['message' => $this->plugin->gettext('backup_code_deactivated')]);
return;
}
if($type == 'whitelist'){
if(!in_array($action, ['add', 'delete'])){
$this->return_JSON(['error' => $this->plugin->gettext('invalid_action')]);
return;
}
if($action == 'delete'){
$id = $token = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
if(!is_numeric($id) || strlen($id) ===0 || !isset($data[$this->email]['2fa']['whitelist'][$id])){
$this->return_JSON(['error' => $this->plugin->gettext('invlid_id')]);
return;
}
unset($data[$this->email]['2fa']['whitelist'][$id]);
$resp = $this->plugin->writedata($this->conf_path, $data);
if(empty($resp)){
$this->return_JSON(['error' => $this->plugin->gettext('write_error')]);
return;
}
$this->rc->session->append($this->session_key,'soft2fa_verify', true);
$this->return_JSON(['message' => $this->plugin->gettext('whitelist_updated')]);
return;
}
// Add IP
$startip = $token = rcube_utils::get_input_value('start_ip', rcube_utils::INPUT_POST);
$endip = $token = rcube_utils::get_input_value('end_ip', rcube_utils::INPUT_POST);
if(empty($startip)){
$this->return_JSON(['message' => $this->plugin->gettext('invalid_input')]);
return;
}
$w_list = !empty($data[$this->email]['2fa']['whitelist']) ? $data[$this->email]['2fa']['whitelist'] : [];
$this->iprange_validate($startip, $endip, $w_list, $error);
if(!empty($error)){
$exists[] = $startip.' - '.$endip;
}else{
$data[$this->email]['2fa']['whitelist'][] = array(
'start' => $startip,
'end' => empty($endip) ? $startip : $endip,
'time' => time()
);
}
if(!empty($exists)){
$error['ip'] = implode(', <br>', $exists);
$error = array_unique($error);
$this->return_JSON(['message' => $error[0], 'prevent_refresh' => 1]);
return false;
}
// Update conf
$resp = $this->plugin->writedata($this->conf_path, $data);
if(empty($resp)){
$this->return_JSON(['error' => $this->plugin->gettext('write_error')]);
return;
}
$this->return_JSON(['message' => $this->plugin->gettext('whitelist_updated')]);
}
$this->return_JSON(['error' => $this->plugin->gettext('method_error')]);
}
private function return_JSON($data = []){
if(empty($data)){
return;
}
$this->rc->output->command('plugin.soft2fa_response', $data);
$this->rc->output->send();
}
private function get_2fa_backup_codes($email = '', $genrate = false){
global $globals;
if(empty($email)){
return;
}
$codes = [];
$data = $this->plugin->loaddata($this->conf_path);
if(!empty($data[$email]['2fa']['backup_codes'])){
$codes = $data[$email]['2fa']['backup_codes'];
}
if(!empty($genrate)){
$tmp_codes = [];
for ($i=0; $i <= 7; $i++) {
$code = base64_encode(strtoupper($this->plugin->generateRandStr(10)));
$tmp_codes[] = [
'code' => $code,
'status' => false
];
}
$data[$email]['2fa']['backup_codes'] = $tmp_codes;
$resp = $this->plugin->writedata($this->conf_path, $data);
if(empty($resp)){
return false;
}
$codes = $tmp_codes;
}
return $codes;
}
private function get_2fa_auth_data($email = '', $reset = false){
global $globals;
if(empty($email)){
return;
}
$data = $this->plugin->loaddata($this->conf_path);
$app_key = '';
if(!empty($data[$email]['2fa']['auth']['key'])){
$app_key = $data[$email]['2fa']['auth']['key'];
}
$status = false;
if(!empty($data[$email]['2fa']['auth']['status'])){
$status = $data[$email]['2fa']['auth']['status'];
}
$settings = array();
// For 2fa_app we must be prepared
$settings['2fa_key'] = empty($app_key) ? '' : base64_decode($app_key);// Just decode it
$settings['2fa_status'] = $status;
// We might need to create a 10 char secret KEY for 2fa App based
if(empty($settings['2fa_key']) || !empty($reset)){
// Generate
$settings['2fa_key'] = strtoupper($this->plugin->generateRandStr(10));
$data[$email]['2fa']['auth']['key'] = base64_encode($settings['2fa_key']);
// Update conf
$this->plugin->writedata($this->conf_path, $data);
}
// Base32 Key
$settings['2fa_key32'] = Base32::encode($settings['2fa_key']);
// The QR Code text
$settings['2fa_qr'] = 'otpauth://'.(empty($settings['2fa_type']) ? 'totp' : $settings['2fa_type']).'/'.rawurlencode('Webuzo - Webmail').':'.$email.'?secret='.Base32::encode($settings['2fa_key']).'&issuer='.rawurlencode($globals['sn']).'&counter=';
// Time now
$settings['2fa_server_time'] = date('Y-m-d H:i:s', time());
// Current OTP
$settings['2fa_otp'] = $this->soft2fa_app_key($settings);
return $settings;
}
private function soft2fa_app_key($settings, $length = 6, $counter = 0){
$key = $settings['2fa_key'];
$type = (empty($settings['2fa_type']) ? 'totp' : $settings['2fa_type']);
if($type == 'hotp'){
$stored_in_db = 1;
$counter = !empty($counter) ? $counter : $stored_in_db;
$res = HOTP::generateByCounter($key, $counter);
}else{
$time = !empty($counter) ? $counter : time();
$res = HOTP::generateByTime($key, 30, $time);
}
return $res->toHotp($length);
}
private function valid_ip($ip, $version = '4'){
$flag = ($version == '4' ? FILTER_FLAG_IPV4 : FILTER_FLAG_IPV6);
return filter_var($ip, FILTER_VALIDATE_IP, $flag);
}
// IP range validations
public function iprange_validate($start_ip, $end_ip, $cur_list, &$error = array()){
if(!function_exists('inet_ptoi')){
return false;
}
if(empty($start_ip)){
$cur_error[] = $this->plugin->gettext('error_start_ip');
}
// If no end IP we consider only 1 IP
if(empty($end_ip)){
$end_ip = $start_ip;
}
if(!$this->valid_ip($start_ip)){
$cur_error[] = $this->plugin->gettext('error_val_start_ip');
}
if(!$this->valid_ip($end_ip)){
$cur_error[] = $this->plugin->gettext('error_val_end_ip');
}
if(inet_ptoi($start_ip) > inet_ptoi($end_ip)){
// BUT, if 0.0.0.1 - 255.255.255.255 is given, it will not work
if(inet_ptoi($start_ip) >= 0 && inet_ptoi($end_ip) < 0){
// This is right
}else{
$cur_error[] = $this->plugin->gettext('error_ip_len');
}
}
if(!empty($cur_error)){
foreach($cur_error as $rk => $rv){
$error[] = $rv;
}
return $error;
}
if(!empty($cur_list)){
foreach($cur_list as $k => $v){
// This is to check if there is any other range exists with the same Start or End IP
if(( inet_ptoi($start_ip) <= inet_ptoi($v['start']) && inet_ptoi($v['start']) <= inet_ptoi($end_ip) )
|| ( inet_ptoi($start_ip) <= inet_ptoi($v['end']) && inet_ptoi($v['end']) <= inet_ptoi($end_ip) )
){
$cur_error[] = $this->plugin->gettext('error_ip_confl');;
break;
}
// This is to check if there is any other range exists with the same Start IP
if(inet_ptoi($v['start']) <= inet_ptoi($start_ip) && inet_ptoi($start_ip) <= inet_ptoi($v['end'])){
$cur_error[] = $this->plugin->gettext('error_start_ip_exits');
break;
}
// This is to check if there is any other range exists with the same End IP
if(inet_ptoi($v['start']) <= inet_ptoi($end_ip) && inet_ptoi($end_ip) <= inet_ptoi($v['end'])){
$cur_error[] = $this->plugin->gettext('error_end_ip_exits');
break;
}
}
}
if(!empty($cur_error)){
foreach($cur_error as $rk => $rv){
$error[] = $rv;
}
return $error;
}
return true;
}
public function check_whitelist(){
$data = $this->plugin->loaddata($this->conf_path);
$user_ip = $_SERVER["REMOTE_ADDR"];
// Is IP whitelisted ?
$whitelist = $data[$this->email]['2fa']['whitelist'];
// Whitelist empty?
if(empty($whitelist)){
return false;
}
$result = 0;
foreach($whitelist as $k => $v){
// Is the IP in the whitelist ?
if(inet_ptoi($v['start']) <= inet_ptoi($user_ip) && inet_ptoi($user_ip) <= inet_ptoi($v['end'])){
$result = 1;
break;
}
// Is it in a wider range ?
if(inet_ptoi($v['start']) >= 0 && inet_ptoi($v['end']) < 0){
if(inet_ptoi($v['start']) <= inet_ptoi($user_ip) || inet_ptoi($user_ip) <= inet_ptoi($v['end'])){
$result = 1;
break;
}
}
}
// You are whitelisted
if(!empty($result)){
return true;
}
return false;
}
}