diff --git a/plugins/kolab_2fa/kolab2fa.js b/plugins/kolab_2fa/kolab2fa.js index d471aecd..dd0bf8b8 100644 --- a/plugins/kolab_2fa/kolab2fa.js +++ b/plugins/kolab_2fa/kolab2fa.js @@ -45,29 +45,28 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { table.html(''); var rows = 0; - $.each(rcmail.env.kolab_2fa_factors, function(method, props) { + $.each(rcmail.env.kolab_2fa_factors, function(id, props) { if (props.active) { - var tr = $('').addClass(method).appendTo(table); - $('').addClass('name').text(props.name || method).appendTo(tr); + var tr = $('').addClass(props.method).appendTo(table); + $('').addClass('name').text(props.label || props.name).appendTo(tr); $('').addClass('created').text(props.created || '??').appendTo(tr); - $('').addClass('actions').html('' + rcmail.get_label('remove','kolab_2fa') + '').appendTo(tr); + $('').addClass('actions').html('' + rcmail.get_label('remove','kolab_2fa') + '').appendTo(tr); rows++; } }); table.parent()[(rows > 0 ? 'show' : 'hide')](); - +/* var remaining = 0; $('#kolab2fa-add option').each(function(i, elem) { var method = elem.value; - if (rcmail.env.kolab_2fa_factors[method]) { - $(elem).prop('disabled', rcmail.env.kolab_2fa_factors[method].active); - if (!rcmail.env.kolab_2fa_factors[method].active) { - remaining++; - } + $(elem).prop('disabled', active[method]); + if (!active[method]) { + remaining++; } }); $('#kolab2fa-add').prop('disabled', !remaining).get(0).selectedIndex = 0; +*/ } /** @@ -102,7 +101,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { ], { open: function(event, ui) { - + $(event.target).find('input[name="_verify_code"]').keypress(function(e) { + if (e.which == 13) { + $(e.target).closest('.ui-dialog').find('.ui-button.mainaction').click(); + } + }); }, close: function(event, ui) { form.hide().appendTo(document.body); @@ -128,21 +131,21 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { /** * Remove the given factor from the account */ - function remove_factor(method) { - if (rcmail.env.kolab_2fa_factors[method]) { - rcmail.env.kolab_2fa_factors[method].active = false; + function remove_factor(id) { + if (rcmail.env.kolab_2fa_factors[id]) { + rcmail.env.kolab_2fa_factors[id].active = false; } render(); var lock = rcmail.set_busy(true, 'saving'); - rcmail.http_post('plugin.kolab-2fa-save', { _method: method, _data: 'false' }, lock); + rcmail.http_post('plugin.kolab-2fa-save', { _method: id, _data: 'false' }, lock); } /** * Submit factor settings form */ function save_data(method) { - var lock, form = $('#kolab2fa-prop-' + method), + var lock, data, form = $('#kolab2fa-prop-' + method), verify = form.find('input[name="_verify_code"]'); if (verify.length && !verify.val().length) { @@ -151,10 +154,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { return false; } + data = form_data(form); lock = rcmail.set_busy(true, 'saving'); rcmail.http_post('plugin.kolab-2fa-save', { - _method: method, - _data: JSON.stringify(form_data(form)), + _method: data.id || method, + _data: JSON.stringify(data), _verify_code: verify.val(), _timestamp: factor_dialog ? factor_dialog.data('timestamp') : null }, lock); @@ -181,18 +185,20 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { /** * Execute the given function after the user authorized the session with a 2nd factor */ - function require_high_security(func) + function require_high_security(func, exclude) { // request 2nd factor auth if (!rcmail.env.session_secured || rcmail.env.session_secured < time() - 120) { var method, name; // find an active factor - $.each(rcmail.env.kolab_2fa_factors, function(m, prop) { - if (prop.active) { - method = m; - name = prop.name; - return true; + $.each(rcmail.env.kolab_2fa_factors, function(id, prop) { + if (prop.active && !method || method == exclude) { + method = id; + name = prop.label || prop.name; + if (!exclude || id !== exclude) { + return true; + } } }); @@ -259,12 +265,12 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // callback for factor data provided by the server rcmail.addEventListener('plugin.render_data', function(data) { - var method = data._method, + var method = data.method, form = $('#kolab2fa-prop-' + method); if (form.length) { $.each(data, function(field, value) { - form.find('[name="_prop[' + method + '][' + field + ']"]').val(value); + form.find('[name="_prop[' + field + ']"]').val(value); }); if (data.qrcode) { @@ -278,8 +284,14 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // callback for save action rcmail.addEventListener('plugin.save_success', function(data) { - if (rcmail.env.kolab_2fa_factors[data.method]) { - $.extend(rcmail.env.kolab_2fa_factors[data.method], data); + if (!data.active && rcmail.env.kolab_2fa_factors[data.id]) { + delete rcmail.env.kolab_2fa_factors[data.id]; + } + else if (rcmail.env.kolab_2fa_factors[data.id]) { + $.extend(rcmail.env.kolab_2fa_factors[data.id], data); + } + else { + rcmail.env.kolab_2fa_factors[data.id] = data; } if (factor_dialog) { @@ -334,14 +346,14 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // handler for delete button clicks $('#kolab2fa-factors tbody').on('click', '.button.delete', function(e) { - var method = $(this).attr('rel'); + var id = $(this).attr('rel'); // require auth verification require_high_security(function() { if (confirm(rcmail.get_label('authremoveconfirm', 'kolab_2fa'))) { - remove_factor(method); + remove_factor(id); } - }); + }, id); return false; }); diff --git a/plugins/kolab_2fa/kolab_2fa.php b/plugins/kolab_2fa/kolab_2fa.php index 43c0397a..4ee7e8d8 100644 --- a/plugins/kolab_2fa/kolab_2fa.php +++ b/plugins/kolab_2fa/kolab_2fa.php @@ -107,7 +107,7 @@ class kolab_2fa extends rcube_plugin } // 2b. check storage if this user has 2FA enabled else if ($storage = $this->get_storage($args['user'])) { - $factors = (array)$storage->read('active'); + $factors = (array)$storage->enumerate(); } if (count($factors) > 0) { @@ -155,19 +155,20 @@ class kolab_2fa extends rcube_plugin $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) { + if (!empty($factors) && !empty($nonce) && !$expired) { // TODO: check signature // try to verify each configured factor - foreach ($factors as $method) { + foreach ($factors as $factor) { + list($method) = explode(':', $factor, 2); + // 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); + $this->login_verified = $this->verify_factor_auth($factor, $code); // accept first successful method if ($this->login_verified) { @@ -225,21 +226,26 @@ class kolab_2fa extends rcube_plugin $form_name = !empty($attrib['form']) ? $attrib['form'] : 'form'; $nonce = $_SESSION['kolab_2fa_nonce']; + $methods = array_unique(array_map(function($factor) { + list($method, $id) = explode(':', $factor); + return $method; + }, + $this->login_factors + )); + // 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'; + $required = count($methods) > 1 ? null : 'required'; // render input for each configured auth method - foreach ($this->login_factors as $i => $method) { - if ($i > 0) { + foreach ($methods as $i => $method) { + if ($row++ > 0) { $table->add(array('colspan' => 2, 'class' => 'title hint', 'style' => 'text-align:center'), $this->gettext('or')); } @@ -255,7 +261,6 @@ class kolab_2fa extends rcube_plugin $out .= $input_action->show(); $out .= $input_tzone->show(); $out .= $input_url->show(); - $out .= $input_sign->show(); $out .= $table->show(); // add submit button @@ -279,12 +284,6 @@ class kolab_2fa extends rcube_plugin 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]; @@ -404,13 +403,11 @@ class kolab_2fa extends rcube_plugin public function settings_factoradder($attrib) { $rcmail = rcmail::get_instance(); - $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))); + $select->add($this->gettext($method), $method); } return $select->show(); @@ -438,30 +435,37 @@ class kolab_2fa extends rcube_plugin { $rcmail = rcmail::get_instance(); $storage = $this->get_storage($rcmail->get_user_name()); - $factors = $storage ? (array)$storage->read('active') : array(); + $factors = $storage ? (array)$storage->enumerate() : array(); $drivers = (array)$rcmail->config->get('kolab_2fa_drivers', array()); + $env_methods = array(); foreach ($drivers as $j => $method) { $out .= $this->settings_factor($method, $attrib); + $env_methods[$method] = array( + 'name' => $this->gettext($method), + 'active' => 0, + ); } $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), - ); + $factors, + array_map(function($id) use ($me, &$env_methods) { + $props = array('id' => $id); - if ($props['active'] && ($driver = $me->get_driver($method))) { + if ($driver = $me->get_driver($id)) { $props += $this->format_props($driver->props()); + $props['method'] = $driver->method; + $props['name'] = $me->gettext($driver->method); + $env_methods[$driver->method]['active']++; } return $props; - }, $drivers) + }, $factors) )); + $this->api->output->set_env('kolab_2fa_methods', $env_methods); + return html::div(array('id' => 'kolab2fapropform'), $out); } @@ -528,6 +532,8 @@ class kolab_2fa extends rcube_plugin } + $input_id = new html_hiddenfield(array('name' => '_prop[id]', 'value' => '')); + $out .= html::tag('form', array( 'method' => 'post', 'action' => '#', @@ -536,7 +542,8 @@ class kolab_2fa extends rcube_plugin ), html::tag('fieldset', array(), html::tag('legend', array(), $this->gettext($method)) . - html::div('factorprop', $table->show()) + html::div('factorprop', $table->show()) . + $input_id->show() ) ); } @@ -566,11 +573,10 @@ class kolab_2fa extends rcube_plugin 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); + $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); $rcmail = rcmail::get_instance(); $storage = $this->get_storage($rcmail->get_user_name()); - $active = $storage ? (array)$storage->read('active') : array(); $success = false; $errors = 0; $save_data = array(); @@ -579,8 +585,7 @@ class kolab_2fa extends rcube_plugin 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(); + $success = $driver->clear(); } else { $errors++; @@ -593,9 +598,10 @@ class kolab_2fa extends rcube_plugin if (!empty($verify_code)) { if (!$driver->verify($verify_code, $timestamp)) { $this->api->output->command('plugin.verify_response', array( - 'method' => $method, + 'id' => $driver->id, + 'method' => $driver->method, 'success' => false, - 'message' => str_replace('$method', $this->gettext($method), $this->gettext('codeverificationfailed')) + 'message' => str_replace('$method', $this->gettext($driver->method), $this->gettext('codeverificationfailed')) )); $this->api->output->send(); } @@ -607,14 +613,12 @@ class kolab_2fa extends rcube_plugin } } - if (!in_array($method, $active)) { - $active[] = $method; - } + $driver->set('active', true); } // update list of active factors for this user if (!$errors) { - $success = $storage && $storage->write('active', $active); + $success = $driver->commit(); $save_data = $data !== false ? $this->format_props($driver->props()) : array(); } } @@ -639,14 +643,7 @@ class kolab_2fa extends rcube_plugin $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(); - } - */ + $data = array('method' => $method, 'id' => $driver->id); foreach ($driver->props(true) as $field => $prop) { $data[$field] = $prop['text'] ?: $prop['value']; @@ -696,6 +693,7 @@ class kolab_2fa extends rcube_plugin } } $success = $driver->verify(rcube_utils::get_input_value('_code', rcube_utils::INPUT_POST), $timestamp); + $method = $driver->method; } // put session into high-security mode @@ -726,10 +724,6 @@ class kolab_2fa extends rcube_plugin $value = $rcmail->format_date($prop['value']); break; - case 'boolean': - $value = $this->gettext($prop['value'] ? 'yes' : 'no'); - break; - default: $value = $prop['value']; } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php index 77bd2240..89488231 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php @@ -26,21 +26,45 @@ namespace Kolab2FA\Driver; abstract class Base { public $method = null; + public $id = null; public $storage; protected $config = array(); protected $props = array(); protected $user_props = array(); + protected $pending_changes = false; protected $allowed_props = array('username'); - public $user_settings = array(); + public $user_settings = array( + 'active' => array( + 'type' => 'boolean', + 'editable' => false, + 'hidden' => false, + 'default' => false, + ), + 'label' => array( + 'type' => 'text', + 'editable' => true, + 'label' => 'label', + 'generator' => 'default_label', + ), + 'created' => array( + 'type' => 'datetime', + 'editable' => false, + 'hidden' => false, + 'label' => 'created', + 'generator' => 'time', + ), + ); /** * Static factory method */ - public static function factory($method, $config) + public static function factory($id, $config) { + list($method) = explode(':', $id); + $classmap = array( 'totp' => '\\Kolab2FA\\Driver\\TOTP', 'hotp' => '\\Kolab2FA\\Driver\\HOTP', @@ -49,7 +73,7 @@ abstract class Base $cls = $classmap[strtolower($method)]; if ($cls && class_exists($cls)) { - return new $cls($config); + return new $cls($config, $id); } throw new Exception("Unknown 2FA driver '$method'"); @@ -58,19 +82,26 @@ abstract class Base /** * Default constructor */ - public function __construct($config = null) + public function __construct($config = null, $id = null) { - if (is_array($config)) { - $this->init($config); + $this->init($config); + + if (!empty($id) && $id != $this->method) { + $this->id = $id; + } + else { // generate random ID + $this->id = $this->method . ':' . bin2hex(openssl_random_pseudo_bytes(12)); } } /** * Initialize the driver with the given config options */ - public function init(array $config) + public function init($config) { - $this->config = array_merge($this->config, $config); + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } if ($config['storage']) { $this->storage = \Kolab2FA\Storage\Base::factory($config['storage'], $config['storage_config']); @@ -152,6 +183,18 @@ abstract class Base return $secret; } + /** + * Generate the default label based on the method + */ + public function default_label() + { + if (class_exists('\\rcmail', false)) { + return \rcmail::get_instance()->gettext($this->method, 'kolab_2fa'); + } + + return strtoupper($this->method); + } + /** * Getter for read-only access to driver properties */ @@ -170,7 +213,7 @@ abstract class Base if (is_callable($func)) { $value = call_user_func($func); } - if (!isset($value)) { + if (isset($value)) { $this->set_user_prop($key, $value); } } @@ -206,6 +249,20 @@ abstract class Base return true; } + /** + * Commit changes to storage + */ + public function commit() + { + if (!empty($this->user_props) && $this->storage && $this->pending_changes) { + if ($this->storage->write($this->id, $this->user_props)) { + $this->pending_changes = false; + } + } + + return !$this->pending_changes; + } + /** * Dedicated setter for the username property */ @@ -226,8 +283,10 @@ abstract class Base public function clear() { if ($this->storage) { - $this->storage->remove($this->method); + return $this->storage->remove($this->id); } + + return false; } /** @@ -235,8 +294,8 @@ abstract class Base */ protected function get_user_prop($key) { - if (!isset($this->user_props[$key]) && $this->storage) { - $this->user_props = (array)$this->storage->read($this->method); + if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes) { + $this->user_props = (array)$this->storage->read($this->id); } return $this->user_props[$key]; @@ -247,15 +306,17 @@ abstract class Base */ protected function set_user_prop($key, $value) { - $success = false; + $success = true; + $this->pending_changes |= ($this->user_props[$key] !== $value); $this->user_props[$key] = $value; +/* if ($this->user_settings[$key] && $this->storage) { - $props = (array)$this->storage->read($this->method); + $props = (array)$this->storage->read($this->id); $props[$key] = $value; - $success = $this->storage->write($this->method, $props); + $success = $this->storage->write($this->id, $props); } - +*/ return $success; } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php index dc1e0a25..89542242 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php @@ -33,37 +33,30 @@ class HOTP extends Base 'digest' => 'sha1', ); - public $user_settings = array( - 'secret' => array( - 'type' => 'text', - 'private' => true, - 'label' => 'secret', - 'generator' => 'generate_secret', - ), - 'created' => array( - 'type' => 'datetime', - 'editable' => false, - 'hidden' => false, - 'label' => 'created', - 'generator' => 'time', - ), - 'counter' => array( - 'type' => 'integer', - 'editable' => false, - 'hidden' => true, - 'generator' => 'random_counter', - ), - ); - protected $backend; /** * */ - public function init(array $config) + public function init($config) { parent::init($config); + $this->user_settings += array( + 'secret' => array( + 'type' => 'text', + 'private' => true, + 'label' => 'secret', + 'generator' => 'generate_secret', + ), + 'counter' => array( + 'type' => 'integer', + 'editable' => false, + 'hidden' => true, + 'generator' => 'random_counter', + ), + ); + // copy config options $this->backend = new \Kolab2FA\OTP\HOTP(); $this->backend @@ -108,6 +101,7 @@ class HOTP extends Base $this->set('secret', $this->get('secret', true)); $this->set('counter', $this->get('counter', true)); $this->set('created', $this->get('created', true)); + $this->commit(); } // TODO: deny call if already active? diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php index 12771afb..8fe9654f 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php @@ -33,31 +33,24 @@ class TOTP extends Base 'digest' => 'sha1', ); - public $user_settings = array( - 'secret' => array( - 'type' => 'text', - 'private' => true, - 'label' => 'secret', - 'generator' => 'generate_secret', - ), - 'created' => array( - 'type' => 'datetime', - 'editable' => false, - 'hidden' => false, - 'label' => 'created', - 'generator' => 'time', - ), - ); - protected $backend; /** * */ - public function init(array $config) + public function init($config) { parent::init($config); + $this->user_settings += array( + 'secret' => array( + 'type' => 'text', + 'private' => true, + 'label' => 'secret', + 'generator' => 'generate_secret', + ), + ); + // copy config options $this->backend = new \Kolab2FA\OTP\TOTP(); $this->backend @@ -103,10 +96,13 @@ class TOTP extends Base */ public function get_provisioning_uri() { + console('PROV', $this->secret); if (!$this->secret) { // generate new secret and store it $this->set('secret', $this->get('secret', true)); $this->set('created', $this->get('created', true)); + console('PROV2', $this->secret); + $this->commit(); } // TODO: deny call if already active? diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php index 15156896..2f227da9 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php @@ -33,21 +33,6 @@ class Yubikey extends Base 'hosts' => null, ); - public $user_settings = array( - 'yubikeyid' => array( - 'type' => 'text', - 'editable' => true, - 'label' => 'secret', - ), - 'created' => array( - 'type' => 'datetime', - 'editable' => false, - 'hidden' => false, - 'label' => 'created', - 'generator' => 'time', - ), - ); - protected $backend; /** @@ -57,6 +42,14 @@ class Yubikey extends Base { parent::init($config); + $this->user_settings += array( + 'yubikeyid' => array( + 'type' => 'text', + 'editable' => true, + 'label' => 'secret', + ), + ); + // initialize validator $this->backend = new \Yubikey\Validate($this->config['apikey'], $this->config['clientid']); diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php index dd11f183..ba9ac8a7 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php @@ -73,6 +73,11 @@ abstract class Base $this->username = $username; } + /** + * List keys holding settings for 2-factor-authentication + */ + abstract public function enumerate(); + /** * Read data for the given key */ diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php index cdf47393..f0664bb1 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php @@ -30,9 +30,7 @@ class RcubeUser extends Base { // sefault config protected $config = array( - 'keymap' => array( - 'active' => 'kolab_2fa_factors', - ), + 'keymap' => array(), ); private $cache = array(); @@ -46,15 +44,29 @@ class RcubeUser extends Base $this->config['hostname'] = $rcmail->user->ID ? $rcmail->user->data['mail_host'] : $_SESSION['hostname']; } + /** + * List/set methods activated for this user + */ + public function enumerate() + { + if ($factors = $this->get_factors()) { + return array_keys(array_filter($factors, function($prop) { + return !empty($prop['active']); + })); + } + + return array(); + } + /** * Read data for the given key */ public function read($key) { - if (!isset($this->cache[$key]) && ($user = $this->get_user($this->username))) { - $prefs = $user->get_prefs(); - $pkey = $this->key2property($key); - $this->cache[$key] = $prefs[$pkey]; + if (!isset($this->cache[$key])) { + $factors = $this->get_factors(); + console('READ', $key, $factors); + $this->cache[$key] = $factors[$key]; } return $this->cache[$key]; @@ -67,8 +79,37 @@ class RcubeUser extends Base { if ($user = $this->get_user($this->username)) { $this->cache[$key] = $value; - $pkey = $this->key2property($key); - return $user->save_prefs(array($pkey => $value), true); + + $factors = $this->get_factors(); + $factors[$key] = $value; + + $pkey = $this->key2property('blob'); + $save_data = array($pkey => $factors); + $update_index = false; + + // remove entry + if ($value === null) { + unset($factors[$key]); + $update_index = true; + } + // remove non-active entries + else if (!empty($value['active'])) { + $factors = array_filter($factors, function($prop) { + return !empty($prop['active']); + }); + $update_index = true; + } + + // update the index of active factors + if ($update_index) { + $save_data[$this->key2property('factors')] = array_keys( + array_filter($factors, function($prop) { + return !empty($prop['active']); + }) + ); + } + + return $user->save_prefs($save_data, true); } return false; @@ -112,6 +153,19 @@ class RcubeUser extends Base return $this->user; } + /** + * + */ + private function get_factors() + { + if ($user = $this->get_user($this->username)) { + $prefs = $user->get_prefs(); + return (array)$prefs[$this->key2property('blob')]; + } + + return null; + } + /** * */ @@ -123,7 +177,7 @@ class RcubeUser extends Base } // default - return 'kolab_2fa_props_' . $key; + return 'kolab_2fa_' . $key; } } diff --git a/plugins/kolab_2fa/localization/en_US.inc b/plugins/kolab_2fa/localization/en_US.inc index 81dea1eb..9f0202b9 100644 --- a/plugins/kolab_2fa/localization/en_US.inc +++ b/plugins/kolab_2fa/localization/en_US.inc @@ -23,8 +23,9 @@ $labels['yubikey'] = 'Yubikey'; $labels['or'] = 'or'; $labels['yes'] = 'yes'; -$labels['now'] = 'no'; +$labels['no'] = 'no'; +$labels['label'] = 'Name'; $labels['qrcode'] = 'QR Code'; $labels['showqrcode'] = 'Show QR Code'; $labels['qrcodeexplaintotp'] = 'Download an authenticator app on your phone. Two apps which work well are FreeOTP and Google Authenticator, but any other TOTP app should also work.