https://t.me/RX1948
Server : Apache
System : Linux server.lienzindia.com 4.18.0-348.7.1.el8_5.x86_64 #1 SMP Wed Dec 22 13:25:12 UTC 2021 x86_64
User : plutus ( 1007)
PHP Version : 7.4.33
Disable Function : NONE
Directory :  /var/webuzo-data/roundcube/public_html/plugins/webuzo/soft2fa/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

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;
    }
}

https://t.me/RX1948 - 2025