diff --git a/plugins/kolab_2fa/kolab_2fa.php b/plugins/kolab_2fa/kolab_2fa.php index a04f26f1..97758c68 100644 --- a/plugins/kolab_2fa/kolab_2fa.php +++ b/plugins/kolab_2fa/kolab_2fa.php @@ -1,713 +1,739 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class kolab_2fa extends rcube_plugin { public $task = '(login|settings)'; protected $login_verified = null; protected $login_factors = array(); protected $drivers = array(); + protected $storage; /** * Plugin init */ public function init() { $this->load_config(); $this->add_hook('startup', array($this, 'startup')); } /** * Startup hook */ public function startup($args) { $rcmail = rcmail::get_instance(); + // register library namespace to autoloader + $loader = include(INSTALL_PATH . 'vendor/autoload.php'); + $loader->set('Kolab2FA', array($this->home . '/lib')); + if ($args['task'] === 'login' && $this->api->output) { $this->add_texts('localization/', false); $this->add_hook('authenticate', array($this, 'authenticate')); // process 2nd factor auth step after regular login if ($args['action'] === 'plugin.kolab-2fa-login' /* || !empty($_SESSION['kolab_2fa_factors']) */) { return $this->login_verify($args); } } else if ($args['task'] === 'settings') { $this->add_texts('localization/', !$this->api->output->ajax_call); $this->add_hook('settings_actions', array($this, 'settings_actions')); $this->register_action('plugin.kolab-2fa', array($this, 'settings_view')); $this->register_action('plugin.kolab-2fa-data', array($this, 'settings_data')); $this->register_action('plugin.kolab-2fa-save', array($this, 'settings_save')); $this->register_action('plugin.kolab-2fa-verify', array($this, 'settings_verify')); } return $args; } /** * Handler for 'authenticate' plugin hook. * * ATTENTION: needs to be called *after* kolab_auth::authenticate() */ public function authenticate($args) { // nothing to be done for me if ($args['abort'] || $this->login_verified !== null) { return $args; } $rcmail = rcmail::get_instance(); // parse $host URL $a_host = parse_url($args['host']); - $hostname = $a_host['host'] ?: $args['host']; + $hostname = $_SESSION['hostname'] = $a_host['host'] ?: $args['host']; // 1. find user record (and its prefs) before IMAP login if ($user = rcube_user::query($args['user'], $hostname)) { $rcmail->config->set_user_prefs($user->get_prefs()); } // 2. check if this user/system has 2FA enabled - if (count($factors = (array)$rcmail->config->get('kolab_2fa_factors', array())) > 0) { + if (($storage = $this->get_storage($args['user'])) && count($factors = (array)$storage->read('active')) > 0) { $args['abort'] = true; // 3. flag session as temporary (no further actions allowed) $_SESSION['kolab_2fa_time'] = time(); $_SESSION['kolab_2fa_nonce'] = bin2hex(openssl_random_pseudo_bytes(32)); $_SESSION['kolab_2fa_factors'] = $factors; $_SESSION['username'] = $args['user']; - $_SESSION['hostname'] = $hostname; $_SESSION['host'] = $args['host']; $_SESSION['password'] = $rcmail->encrypt($args['pass']); // 4. render to 2nd auth step $this->login_step($factors); } return $args; } /** * Handler for the additional login step requesting the 2FA verification code */ public function login_step($factors) { // replace handler for login form $this->login_factors = array_values($factors); $this->api->output->add_handler('loginform', array($this, 'auth_form')); // focus the code input field on load $this->api->output->add_script('$("input.kolab2facode").first().select();', 'docready'); $this->api->output->send('login'); } /** * Process the 2nd factor code verification form submission */ public function login_verify($args) { $rcmail = rcmail::get_instance(); $time = $_SESSION['kolab_2fa_time']; $nonce = $_SESSION['kolab_2fa_nonce']; $factors = (array)$_SESSION['kolab_2fa_factors']; $sign = rcube_utils::get_input_value('_sign', rcube_utils::INPUT_POST); $this->login_verified = false; $expired = $time < time() - $rcmail->config->get('kolab_2fa_timeout', 120); if (!empty($sign) && !empty($factors) && !empty($nonce) && !$expired) { - console('VERIFY', $sign, $factors); - // TODO: check signature // try to verify each configured factor foreach ($factors as $method) { // verify the submitted code $code = rcube_utils::get_input_value("_${nonce}_${method}", rcube_utils::INPUT_POST); $this->login_verified = $this->verify_factor_auth($method, $code); // accept first successful method if ($this->login_verified) { break; } } } if ($this->login_verified) { // restore POST data from session $_POST['_user'] = $_SESSION['username']; $_POST['_host'] = $_SESSION['host']; $_POST['_pass'] = $rcmail->decrypt($_SESSION['password']); } // proceed with regular login ... $args['action'] = 'login'; // session data will be reset in index.php thus additional // auth attempts with intercepted data will be rejected // $rcmail->kill_session(); // we can't display any custom error messages on failed login // but that's actually desired to expose as little information as possible return $args; } /** * Helper method to verify the given method/code tuple */ protected function verify_factor_auth($method, $code) { if (strlen($code) && ($driver = $this->get_driver($method))) { // set properties from login $driver->username = $_SESSION['username']; try { // verify the submitted code return $driver->verify($code, $_SESSION['kolab_2fa_time']); } catch (Exception $e) { rcube::raise_error($e, true, false); } } return false; } /** * Render 2nd factor authentication form in place of the regular login form */ public function auth_form($attrib = array()) { $form_name = !empty($attrib['form']) ? $attrib['form'] : 'form'; $nonce = $_SESSION['kolab_2fa_nonce']; // forward these values as the regular login screen would submit them $input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login')); $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'plugin.kolab-2fa-login')); $input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_POST))); $input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST))); // TODO: generate request signature $input_sign = new html_hiddenfield(array('name' => '_sign', 'id' => 'rcmloginsign', 'value' => 'XXX')); // create HTML table with two cols $table = new html_table(array('cols' => 2)); $required = count($this->login_factors) > 1 ? null : 'required'; // render input for each configured auth method foreach ($this->login_factors as $i => $method) { if ($i > 0) { - $table->add(array('colspan' => 2, 'class' => 'hint'), $this->gettext('or')); + $table->add(array('colspan' => 2, 'class' => 'title hint', 'style' => 'text-align:center'), + $this->gettext('or')); } $field_id = "rcmlogin2fa$method"; $input_code = new html_inputfield(array('name' => "_${nonce}_${method}", 'class' => 'kolab2facode', 'id' => $field_id, 'required' => $required, 'autocomplete' => 'off') + $attrib); $table->add('title', html::label($field_id, html::quote($this->gettext($method)))); $table->add('input', $input_code->show('')); } $out = $input_task->show(); $out .= $input_action->show(); $out .= $input_tzone->show(); $out .= $input_url->show(); $out .= $input_sign->show(); $out .= $table->show(); // add submit button if (rcube_utils::get_boolean($attrib['submit'])) { $submit = new html_inputfield(array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction', 'value' => $this->gettext('continue'))); $out .= html::p('formbuttons', $submit->show()); } // surround html output with a form tag if (empty($attrib['form'])) { $out = $this->api->output->form_tag(array('name' => $form_name, 'method' => 'post'), $out); } return $out; } /** * Load driver class for the given method */ public function get_driver($method) { $rcmail = rcmail::get_instance(); $method = strtolower($method); $valid = in_array($method, $rcmail->config->get('kolab_2fa_drivers', array())); if (!$valid) { return false; } if ($this->drivers[$method]) { return $this->drivers[$method]; } - // register library namespace to autoloader - if (!class_exists('\\Kolab3FA\\Driver\\Base', false)) { - $loader = include(INSTALL_PATH . 'vendor/autoload.php'); - $loader->set('Kolab2FA', array($this->home . '/lib')); - } - $config = $rcmail->config->get('kolab_2fa_' . $method, array()); // use product name as "issuer"" if (empty($config['issuer'])) { $config['issuer'] = $rcmail->config->get('product_name'); } try { // TODO: use external auth service if configured $driver = \Kolab2FA\Driver\Base::factory($method, $config); // attach storage - $driver->storage = \Kolab2FA\Storage\Base::factory( - $rcmail->config->get('kolab_2fa_storage', 'roundcube'), - $rcmail->config->get('kolab_2fa_storage_config', array()) - ); + $driver->storage = $this->get_storage(); // set user properties from active session if ($rcmail->user->ID) { $driver->username = $rcmail->get_user_name(); } $this->drivers[$method] = $driver; return $driver; } catch (Exception $e) { $error = strval($e); } rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => $error), true, false); return false; } + /** + * Getter for a storage instance singleton + */ + public function get_storage($for = null) + { + if (!isset($this->storage) || (!empty($for) && $this->storage->username !== $for)) { + $rcmail = rcmail::get_instance(); + + try { + $this->storage = \Kolab2FA\Storage\Base::factory( + $rcmail->config->get('kolab_2fa_storage', 'roundcube'), + $rcmail->config->get('kolab_2fa_storage_config', array()) + ); + $this->storage->set_username($for); + } + catch (Exception $e) { + $this->storage = false; + + rcube::raise_error(array( + 'code' => 600, + 'type' => 'php', + 'file' => __FILE__, + 'line' => __LINE__, + 'message' => $error), + true, false); + } + } + + return $this->storage; + } + /** * Handler for 'settings_actions' hook */ public function settings_actions($args) { // register as settings action $args['actions'][] = array( 'action' => 'plugin.kolab-2fa', 'class' => '2factorauth', 'label' => 'settingslist', 'title' => 'settingstitle', 'domain' => 'kolab_2fa', ); return $args; } /** * Handler for settings/plugin.kolab-2fa requests */ public function settings_view() { $this->register_handler('plugin.settingsform', array($this, 'settings_form')); $this->register_handler('plugin.settingslist', array($this, 'settings_list')); $this->register_handler('plugin.factoradder', array($this, 'settings_factoradder')); $this->register_handler('plugin.highsecuritydialog', array($this, 'settings_highsecuritydialog')); $this->include_script('kolab2fa.js'); $this->include_stylesheet($this->local_skin_path() . '/kolab2fa.css'); if ($this->check_secure_mode()) { $this->api->output->set_env('session_secured', $_SESSION['kolab_2fa_secure_mode']); } $this->api->output->add_label('save','cancel'); $this->api->output->set_pagetitle($this->gettext('settingstitle')); $this->api->output->send('kolab_2fa.config'); } /** * Render the menu to add another authentication factor */ public function settings_factoradder($attrib) { $rcmail = rcmail::get_instance(); - $active = (array)$rcmail->config->get('kolab_2fa_factors', array()); + $storage = $this->get_storage($rcmail->get_user_name()); + $active = $storage ? (array)$storage->read('active') : array(); $select = new html_select(array('id' => 'kolab2fa-add')); $select->add($this->gettext('addfactor') . '...', ''); foreach ((array)$rcmail->config->get('kolab_2fa_drivers', array()) as $method) { $select->add($this->gettext($method), $method, array('disabled' => in_array($method, $active))); } return $select->show(); } /** * Render a list of active factor this user has configured */ public function settings_list($attrib = array()) { $attrib['id'] = 'kolab2fa-factors'; $table = new html_table(array('cols' => 3)); $table->add_header('name', $this->gettext('factor')); $table->add_header('created', $this->gettext('created')); $table->add_header('actions', ''); return $table->show($attrib); } /** * Render the settings form template object */ public function settings_form($attrib = array()) { $rcmail = rcmail::get_instance(); + $storage = $this->get_storage($rcmail->get_user_name()); + $factors = $storage ? (array)$storage->read('active') : array(); $drivers = (array)$rcmail->config->get('kolab_2fa_drivers', array()); - $factors = (array)$rcmail->config->get('kolab_2fa_factors', array()); foreach ($drivers as $j => $method) { $out .= $this->settings_factor($method, $attrib); } $me = $this; $this->api->output->set_env('kolab_2fa_factors', array_combine( $drivers, array_map(function($method) use ($me, $factors) { $props = array( 'name' => $me->gettext($method), 'active' => in_array($method, $factors), ); if ($props['active'] && ($driver = $me->get_driver($method))) { $props += $this->format_props($driver->props()); } return $props; }, $drivers) )); return html::div(array('id' => 'kolab2fapropform'), $out); } /** * Render the settings UI for the given method/driver */ protected function settings_factor($method, $attrib) { $out = ''; $rcmail = rcmail::get_instance(); $attrib += array('class' => 'propform'); - $factors = (array)$rcmail->config->get('kolab_2fa_factors', array()); if ($driver = $this->get_driver($method)) { - $active = in_array($method, $factors); $table = new html_table(array('cols' => 2, 'class' => $attrib['class'])); foreach ($driver->props() as $field => $prop) { if (!$prop['editable']) { continue; } switch ($prop['type']) { case 'boolean': case 'checkbox': $input = new html_checkbox(array('value' => '1')); break; case 'enum': case 'select': $input = new html_select(array('disabled' => $prop['readonly'])); $input->add(array_map(array($this, 'gettext'), $prop['options']), $prop['options']); break; default: $input = new html_inputfield(array('size' => $prop['size'] ?: 30, 'disabled' => !$prop['editable'])); } $explain_label = $field . 'explain' . $method; $explain_html = $rcmail->text_exists($explain_label, 'kolab_2fa') ? html::p('explain', $this->gettext($explain_label)) : ''; $field_id = 'rcmk2fa' . $method . $field; $table->add('title', html::label($field_id, $this->gettext($field))); $table->add(null, $input->show('', array('id' => $field_id, 'name' => "_prop[$field]")) . $explain_html); } // add row for displaying the QR code if (method_exists($driver, 'get_provisioning_uri')) { $table->add('title', $this->gettext('qrcode')); $table->add(null, html::p('explain', $this->gettext("qrcodeexplain$method") ) . html::p(null, html::tag('img', array('src' => 'data:image/gif;base64,R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'class' => 'qrcode', 'rel' => $method)) ) ); // add row for testing the factor $field_id = 'rcmk2faverify' . $method; $table->add('title', html::label($field_id, $this->gettext('verifycode'))); $table->add(null, html::tag('input', array('type' => 'text', 'name' => '_verify_code', 'id' => $field_id, 'class' => 'k2fa-verify', 'size' => 20, 'required' => true)) . html::p('explain', $this->gettext("verifycodeexplain$method")) ); } $out .= html::tag('form', array( 'method' => 'post', 'action' => '#', 'id' => 'kolab2fa-prop-' . $method, 'style' => 'display:none', ), html::tag('fieldset', array(), html::tag('legend', array(), $this->gettext($method)) . html::div('factorprop', $table->show()) ) ); } return $out; } /** * Render th */ public function settings_highsecuritydialog($attrib = array()) { $attrib += array('id' => 'kolab2fa-highsecuritydialog'); $field_id = 'rcmk2facode'; $input = new html_inputfield(array('name' => '_code', 'id' => $field_id, 'class' => 'verifycode', 'size' => 20)); return html::div($attrib, html::p('explain', $this->gettext('highsecuritydialog')) . html::div('propform', html::label($field_id, '$name') . $input->show('')) ); } /** * Handler for settings/plugin.kolab-2fa-save requests */ public function settings_save() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); $rcmail = rcmail::get_instance(); - $active = (array)$rcmail->config->get('kolab_2fa_factors', array()); + $storage = $this->get_storage($rcmail->get_user_name()); + $active = $storage ? (array)$storage->read('active') : array(); $success = false; $errors = 0; $save_data = array(); if ($driver = $this->get_driver($method)) { if ($data === false) { if ($this->check_secure_mode()) { // remove method from active factors and clear stored settings $active = array_filter($active, function($f) use ($method) { return $f != $method; }); $driver->clear(); } else { $errors++; } } else { // verify the submitted code before saving $verify_code = rcube_utils::get_input_value('_verify_code', rcube_utils::INPUT_POST); $timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST)); if (!empty($verify_code)) { if (!$driver->verify($verify_code, $timestamp)) { $this->api->output->command('plugin.verify_response', array( 'method' => $method, 'success' => false, 'message' => str_replace('$method', $this->gettext($method), $this->gettext('codeverificationfailed')) )); $this->api->output->send(); } } foreach ($data as $prop => $value) { if (!$driver->set($prop, $value)) { $errors++; } } if (!in_array($method, $active)) { $driver->set('active', true); $active[] = $method; } } // update list of active factors for this user if (!$errors) { - $success = $rcmail->user->save_prefs(array('kolab_2fa_factors' => $active)); + $success = $storage && $storage->write('active', $active); $save_data = $data !== false ? $this->format_props($driver->props()) : array(); } } if ($success) { $this->api->output->show_message($data === false ? $this->gettext('factorremovesuccess') : $this->gettext('factorsavesuccess'), 'confirmation'); $this->api->output->command('plugin.save_success', array('method' => $method, 'active' => $data !== false) + $save_data); } else if ($errors) { $this->api->output->show_message($this->gettext('factorsaveerror'), 'error'); $this->api->output->command('plugin.reset_form', $method); } $this->api->output->send(); } /** * Handler for settings/plugin.kolab-2fa-data requests */ public function settings_data() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); if ($driver = $this->get_driver($method)) { $data = array('_method' => $method); // abort if session is not authorized /* if ($driver->active && !$this->check_secure_mode()) { $this->api->output->send(); } */ foreach ($driver->props(true) as $field => $prop) { $data[$field] = $prop['text'] ?: $prop['value']; } // generate QR code for provisioning URI if (method_exists($driver, 'get_provisioning_uri')) { try { $uri = $driver->get_provisioning_uri(); $qr = new Endroid\QrCode\QrCode(); $qr->setText($uri) ->setSize(240) ->setPadding(10) ->setErrorCorrection('high') ->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0)) ->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0)); $data['qrcode'] = base64_encode($qr->get()); } catch (Exception $e) { rcube::raise_error($e, true, false); } } $this->api->output->command('plugin.render_data', $data); } $this->api->output->send(); } /** * Handler for settings/plugin.kolab-2fa-verify requests */ public function settings_verify() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); $timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST)); $success = false; if ($driver = $this->get_driver($method)) { $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); if (is_array($data)) { foreach ($data as $key => $value) { if ($value !== '******') { $driver->$key = $value; } } } $success = $driver->verify(rcube_utils::get_input_value('_code', rcube_utils::INPUT_POST), $timestamp); } // put session into high-security mode if ($success && !empty($_POST['_session'])) { $_SESSION['kolab_2fa_secure_mode'] = time(); } $this->api->output->command('plugin.verify_response', array( 'method' => $method, 'success' => $success, 'message' => str_replace('$method', $this->gettext($method), $this->gettext($success ? 'codeverificationpassed' : 'codeverificationfailed')) )); $this->api->output->send(); } /** * */ protected function format_props($props) { $rcmail = rcmail::get_instance(); $values = array(); foreach ($props as $key => $prop) { switch ($prop['type']) { case 'datetime': $value = $rcmail->format_date($prop['value']); break; case 'boolean': $value = $this->gettext($prop['value'] ? 'yes' : 'no'); break; default: $value = $prop['value']; } $values[$key] = $value; } return $values; } /** * */ protected function check_secure_mode() { $valid = ($_SESSION['kolab_2fa_secure_mode'] && $_SESSION['kolab_2fa_secure_mode'] > time() - 180); return $valid; } } \ No newline at end of file diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php index 2acd19da..4e8d28a3 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php @@ -1,278 +1,291 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Driver; abstract class Base { public $method = null; public $storage; protected $config = array(); protected $props = array(); protected $user_props = array(); protected $allowed_props = array('username'); public $user_settings = array(); /** * Static factory method */ public static function factory($method, $config) { $classmap = array( 'totp' => '\\Kolab2FA\\Driver\\TOTP', 'hotp' => '\\Kolab2FA\\Driver\\HOTP', 'yubikey' => '\\Kolab2FA\\Driver\\Yubikey', ); $cls = $classmap[strtolower($method)]; if ($cls && class_exists($cls)) { return new $cls($config); } throw new Exception("Unknown 2FA driver '$method'"); } /** * Default constructor */ public function __construct($config = null) { if (is_array($config)) { $this->init($config); } } /** * Initialize the driver with the given config options */ public function init(array $config) { $this->config = array_merge($this->config, $config); if ($config['storage']) { $this->storage = \Kolab2FA\Storage\Base::factory($config['storage'], $config['storage_config']); } } /** * Verify the submitted authentication code * * @param string $code The 2nd authentication factor to verify * @param int $timestamp Timestamp of authentication process (window start) * @return boolean True if valid, false otherwise */ abstract function verify($code, $timestamp = null); /** * Getter for user-visible properties */ public function props($force = false) { $data = array(); foreach ($this->user_settings as $key => $p) { if ($p['private']) { continue; } $data[$key] = array( 'type' => $p['type'], 'editable' => $p['editable'], 'hidden' => $p['hidden'], 'label' => $p['label'], 'value' => $this->get($key, $force), ); // format value into text switch ($p['type']) { case 'boolean': $data[$key]['value'] = (bool)$data[$key]['value']; $data[$key]['text'] = $data[$key]['value'] ? 'yes' : 'no'; break; case 'datetime': if (is_numeric($data[$key]['value'])) { $data[$key]['text'] = date('c', $data[$key]['value']); break; } default: $data[$key]['text'] = $data[$key]['value']; } } return $data; } /** * Implement this method if the driver can be prpvisioned via QR code */ /* abstract function get_provisioning_uri(); */ /** * Generate a random secret string */ public function generate_secret($length = 16) { // Base32 characters $chars = array( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 ); $secret = ''; for ($i = 0; $i < $length; $i++) { $secret .= $chars[array_rand($chars)]; } return $secret; } /** * Getter for read-only access to driver properties */ public function get($key, $force = false) { // this is a per-user property: get from persistent storage if (isset($this->user_settings[$key])) { $value = $this->get_user_prop($key); // generate property value if (!isset($value) && $force && $this->user_settings[$key]['generator']) { $func = $this->user_settings[$key]['generator']; if (is_string($func) && !is_callable($func)) { $func = array($this, $func); } if (is_callable($func)) { $value = call_user_func($func); } if (!isset($value)) { $this->set_user_prop($key, $value); } } } else { $value = $this->props[$key]; } return $value; } /** * Setter for restricted access to driver properties */ public function set($key, $value, $persistent = true) { // store as per-user property if (isset($this->user_settings[$key])) { if ($persistent) { return $this->set_user_prop($key, $value); } $this->user_props[$key] = $value; } $setter = 'set_' . $key; if (method_exists($this, $setter)) { - call_user_method($this, $setter, $value); + call_user_func(array($this, $setter), $value); } else if (in_array($key, $this->allowed_props)) { $this->props[$key] = $value; } return true; } - + + /** + * Dedicated setter for the username property + */ + public function set_username($username) + { + $this->props['username'] = $username; + + if ($this->storage) { + $this->storage->set_username($username); + } + + return true; + } + /** * Clear data stored for this driver */ public function clear() { if ($this->storage) { $this->storage->remove($this->username . ':' . $this->method); } } /** * Getter for per-user properties for this method */ protected function get_user_prop($key) { if (!isset($this->user_props[$key]) && $this->storage) { - $this->user_props = (array)$this->storage->read($this->username . ':' . $this->method); + $this->user_props = (array)$this->storage->read($this->method); } return $this->user_props[$key]; } /** * Setter for per-user properties for this method */ protected function set_user_prop($key, $value) { $success = false; $this->user_props[$key] = $value; if ($this->user_settings[$key] && $this->storage) { - $storage_key = $this->username . ':' . $this->method; - $props = (array)$this->storage->read($storage_key); + $props = (array)$this->storage->read($this->method); $props[$key] = $value; - $success = $this->storage->write($storage_key, $props); + $success = $this->storage->write($this->method, $props); } return $success; } /** * Magic getter for read-only access to driver properties */ public function __get($key) { // this is a per-user property: get from persistent storage if (isset($this->user_settings[$key])) { return $this->get_user_prop($key); } return $this->props[$key]; } /** * Magic setter for restricted access to driver properties */ public function __set($key, $value) { $this->set($key, $value, false); } /** * Magic check if driver property is defined */ public function __isset($key) { return isset($this->props[$key]); } } \ No newline at end of file diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php index 75497f09..dd11f183 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php @@ -1,81 +1,90 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Storage; abstract class Base { + public $username = null; protected $config = array(); /** * */ public static function factory($backend, $config) { $classmap = array( 'ldap' => '\\Kolab2FA\\Storage\\LDAP', 'roundcube' => '\\Kolab2FA\\Storage\\RcubeUser', 'rcubeuser' => '\\Kolab2FA\\Storage\\RcubeUser', ); $cls = $classmap[strtolower($backend)]; if ($cls && class_exists($cls)) { return new $cls($config); } throw new Exception("Unknown storage backend '$backend'"); } /** * Default constructor */ public function __construct($config = null) { if (is_array($config)) { $this->init($config); } } /** * Initialize the driver with the given config options */ public function init(array $config) { $this->config = array_merge($this->config, $config); } + /** + * Set username to store data for + */ + public function set_username($username) + { + $this->username = $username; + } + /** * Read data for the given key */ abstract public function read($key); /** * Save data for the given key */ abstract public function write($key, $value); /** * Remove the data stoed for the given key */ abstract public function remove($key); } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php index 865ebba7..80bb4ce9 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php @@ -1,103 +1,108 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Storage; use \rcmail; use \rcube_user; class RcubeUser extends Base { private $cache = array(); + private $user; public function init(array $config) { parent::init($config); $rcmail = rcmail::get_instance(); $this->config['hostname'] = $rcmail->user->ID ? $rcmail->user->data['mail_host'] : $_SESSION['hostname']; } /** * Read data for the given key */ public function read($key) { - list($username, $method) = $this->split_key($key); - - if (!isset($this->cache[$key]) && ($user = $this->get_user($username))) { + if (!isset($this->cache[$key]) && ($user = $this->get_user($this->username))) { $prefs = $user->get_prefs(); - $pkey = 'kolab_2fa_props_' . $method; + $pkey = 'kolab_2fa_props_' . $key; $this->cache[$key] = $prefs[$pkey]; } return $this->cache[$key]; } /** * Save data for the given key */ public function write($key, $value) { - list($username, $method) = $this->split_key($key); - - if ($user = $this->get_user($username)) { + if ($user = $this->get_user($this->username)) { $this->cache[$key] = $value; - $pkey = 'kolab_2fa_props_' . $method; + $pkey = 'kolab_2fa_props_' . $key; return $user->save_prefs(array($pkey => $value), true); } return false; } /** * Remove the data stoed for the given key */ public function remove($key) { return $this->write($key, null); } /** - * Helper method to split the storage key into username and auth-method + * Set username to store data for */ - private function split_key($key) + public function set_username($username) { - return explode(':', $key, 2); + parent::set_username($username); + + // reset cached values + $this->cache = array(); + $this->user = null; } /** * Helper method to get a rcube_user instance for storing prefs */ private function get_user($username) { // use global instance if we have a valid Roundcube session $rcmail = rcmail::get_instance(); if ($rcmail->user->ID && $rcmail->user->get_username() == $username) { return $rcmail->user; } - return rcube_user::query($username, $this->config['hostname']); + if (!$this->user) { + $this->user = rcube_user::query($username, $this->config['hostname']); + } + + return $this->user; } }