Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:darix:apps
roundcube-plugin-kolab-2fa
roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6.obscpio of Package roundcube-plugin-kolab-2fa
07070100000000000081A400000000000000000000000160E313F50000000A000000000000000000000000000000000000003C00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/.gitignorebin/data/ 07070100000001000081A400000000000000000000000160E313F500001075000000000000000000000000000000000000003B00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/README.mdIMPORTANT INFORMATION ===================== > This is a clone from https://git.kolab.org/diffusion/RPK/ > For bug reports and pull requests, please go to https://kolab.org/ How to upgrade this clone ========================= Execute the [synchronise_with_source.sh](bin/synchronise_with_source.sh) shell script. Kolab Multi-Factor Authentication Plugin ======================================== The plugin is designed to be a generic container for different 2nd factor authentication mechanisms paired with different ways to store the related data for Roundcube user accounts. Both drivers and storage backends are derived from abstract classes which define the common interface and are configurable. Drivers ------- Multiple methods for 2nd factor authentication can be enabled for the users to select from. The basic implementation covers TOTP, HOTP and Yubikey methods. TOTP (RFC 6238) and HOTP (RFC 4226) can be used in conjunction with freely available mobile phone apps like FreeOTP (TOTP only!) or Google Authenticator. To provision the app with your account settings, a QR code is displayed which can be scanned with the mobile phone camera. The Yubikey driver uses the Yubico Validation Service either by using the public YubiCloud API or another locally hosted verification server. The host(s) to use for validation are configurable. Storage Backends ---------------- Some authentication methods require to store secret data per user account on the server. For this, one of different storage backends can be selected: **Roundcube** The simplest way is to store authentication secrets and configuration in the user preferences of Roundcube itself. **LDAP** For an external storage option, the LDAP module can be used. This keeps the authentication data separated from the Roundcube user database. See //LDAP Storage// below for more information. The LDAP connection parameters are defined through the `kolab_2fa_storage_config` config option. Installation ------------ After placing the plugin contents into Roundcube's plugins directory, the 3rd party libraries need to be installed using Composer: ``` $ composer require "endroid/qrcode" "~1.5.0" --no-update $ composer require "spomky-labs/otphp" "~5.0.0" --no-update $ composer require "enygma/yubikey" "~3.2" ``` See the `composer.json` file for the actual module names and versions. Configuration ------------- Copy the sample `config.inc.php.dist` file into `config.inc.php` and adjust the settings according to your desired setup. All options are described with inline comments directly in the sample file. When using the LDAP storage together with a Kolab installation, you may want to save an additional LDAP lookup for authentication factors on every login, the LDAP driver can assign roles to the user record when registering authentication factors (see `user_roles` storage config option). With the following additions to the `kolab_auth` plugin config, these roles can be used to determine whether the user has multi-factor authentication enabled: ``` // Disable lokkups by default: $config['kolab_2fa_check'] = false; // Enable 2nd factor lookup on a role-by-role basis $config['kolab_auth_role_settings'] = array( 'cn=totp-user,dc=example,dc=org' => array( 'kolab_2fa_check' => array( 'mode' => 'override', 'value' => true, ), ), ); ``` LDAP Storage ------------ Define an `organizationalunit` with DN `ou=Tokens,dc=example,dc=org` to store all authentication tokens. For token records, the [[https://git.fedorahosted.org/cgit/freeipa.git/tree/install/share/70ipaotp.ldif | FreeIPA OTP schema]] can be used. Please install this schema in your Kolab LDAP directory. This is an example record for a TOTP token registered to user doe@example.org: ``` dn: ipatokenUniqueID=totp:c4a1ced768a0da55df662e73,ou=Tokens,dc=example,dc=org objectClass: top objectClass: ipaToken objectClass: ipatokenTOTP objectClass: ldapSubEntry cn: Mobile App (TOTP) ipatokenUniqueID: totp:c4a1ced768a0da55df662e73 ipatokenOwner: uid=doe,ou=People,dc=example,dc=org ipatokenNotBefore: 201506110211Z ipatokenOTPkey: 4T5CI7SOKWYQ5JTM ipatokenDisabled: TRUE ``` 07070100000002000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000003500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/bin07070100000003000081ED00000000000000000000000160E313F500000E79000000000000000000000000000000000000005000000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/bin/synchronise_with_source.sh#!/bin/bash #### # Dowloads latest state of source repository and updates this one. #### # @author stev leibelt <artodeto@bazzline.net> # @since 2021-02-01 #### function _cleanup_root () { local STRING_LENGTH_PATH_OF_THE_ROOT=$((${#PATH_TO_THE_ROOT} + 1)) #we have to add +1 to remove the "/" if [[ ${DEBUG} -eq 1 ]]; then echo ":: PATH_OF_THIS_FILE >>${PATH_OF_THIS_FILE}<<." echo ":: PATH_TO_THE_ROOT >>${PATH_TO_THE_ROOT}<<." echo ":: STRING_LENGTH_PATH_OF_THE_ROOT >>${STRING_LENGTH_PATH_OF_THE_ROOT}<<." fi for ITEM_PATH in ${PATH_TO_THE_ROOT}/* do ITEM_NAME="${ITEM_PATH:${STRING_LENGTH_PATH_OF_THE_ROOT}}" if [[ ${ITEM_NAME} != "bin" ]]; then if [[ ${DEBUG} -eq 1 ]]; then echo ":: Delete >>${ITEM_PATH}<<" fi rm -fr ${ITEM_PATH} else if [[ ${DEBUG} -eq 1 ]]; then echo ":: Keep >>${ITEM_PATH}<<" fi fi done } function _copy_source_to_root () { if [[ ${DEBUG} -eq 1 ]]; then echo ":: Copy content of >>${PATH_TO_THE_PLUGIN_SOURCE}<< to >>${PATH_TO_THE_ROOT}<<" fi cp -a ${PATH_TO_THE_PLUGIN_SOURCE}/* ${PATH_TO_THE_ROOT} } function _update_readme () { if [[ ${DEBUG} -eq 1 ]]; then echo ":: Prefixing >>${PATH_TO_THE_ROOT_README}<< with important information." fi cat > ${PATH_TO_THE_ROOT_README} <<DELIM IMPORTANT INFORMATION ===================== > This is a clone from https://git.kolab.org/diffusion/RPK/ > For bug reports and pull requests, please go to https://kolab.org/ How to upgrade this clone ========================= Execute the [synchronise_with_source.sh](bin/synchronise_with_source.sh) shell script. DELIM if [[ ${DEBUG} -eq 1 ]]; then echo ":: Attaching content of >>${PATH_TO_THE_SOURCE_README}<< to >>${PATH_TO_THE_ROOT}<<." fi cat ${PATH_TO_THE_SOURCE_README} >> ${PATH_TO_THE_ROOT_README} } function _update_source () { if [[ -d "${PATH_TO_THE_SOURCE}" ]]; then if [[ ${DEBUG} -eq 1 ]]; then echo ":: Source path exists." echo ":: Removing >>${PATH_TO_THE_SOURCE}<<." fi rm -fr "${PATH_TO_THE_SOURCE}" fi if [[ ${DEBUG} -eq 1 ]]; then echo ":: Creating path >>${PATH_TO_THE_SOURCE}<<." echo ":: Cloning repository into >>${PATH_TO_THE_SOURCE}<<." fi mkdir -p "${PATH_TO_THE_SOURCE}" cd "${PATH_TO_THE_SOURCE}" $(git clone https://git.kolab.org/diffusion/RPK/roundcubemail-plugins-kolab.git .) LATEST_SOURCE_TAG=$(git tag | grep roundcubemail | sort -n | tail -n 1) if [[ ${LATEST_SOURCE_TAG} == ${LATEST_TAG} ]]; then echo ":: No new tag available." echo " Latest source tag >>${LATEST_SOURCE_TAG}<<." echo " Is equal to latest tag >>${LATEST_TAG}<<." echo "" cd - exit echo " Aborting" fi if [[ ${DEBUG} -eq 1 ]]; then echo ":: Switching to latest tag >>${LATEST_SOURCE_TAG}<<." fi git checkout ${LATEST_SOURCE_TAG} cd - } function synchronise_from_source_repository () { #bo: variable declaration ##independent section local CURRENT_WORKING_DIRECTORY=$(pwd) local DEBUG=0 local PATH_OF_THIS_FILE=$(cd $(dirname "${BASH_SOURCE[0]}"); pwd) local LATEST_TAG=$(git tag | sort -n | tail -n 1) ##dependent section local PATH_TO_THE_ROOT=$(cd "${PATH_OF_THIS_FILE}/../"; pwd) local PATH_TO_THE_SOURCE="${PATH_OF_THIS_FILE}/data/source" local PATH_TO_THE_PLUGIN_SOURCE="${PATH_TO_THE_SOURCE}/plugins/kolab_2fa" local PATH_TO_THE_ROOT_README="${PATH_TO_THE_ROOT}/README.md" local PATH_TO_THE_SOURCE_README="${PATH_TO_THE_PLUGIN_SOURCE}/README.md" #eo: variable declaration #bo: business logic _cleanup_root _update_source _copy_source_to_root _update_readme echo "" echo ":: Done" echo " Please create latest tag >>${LATEST_SOURCE_TAG}<<." #eo: business logic } synchronise_from_source_repository 07070100000004000081A400000000000000000000000160E313F50000034F000000000000000000000000000000000000003F00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/composer.json{ "name": "kolab/kolab_2fa", "type": "roundcube-plugin", "description": "Kolab 2-Factor Authentication", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPL-3.0-or-later", "authors": [ { "name": "Thomas Bruederli", "email": "thomas@roundcube.net", "role": "Developer" }, { "name": "Aleksander Machniak", "email": "machniak@kolabsys.com", "role": "Lead" } ], "repositories": [ { "type": "composer", "url": "https://plugins.roundcube.net" } ], "require": { "php": ">=5.3.0", "roundcube/plugin-installer": "~0.2.0", "spomky-labs/otphp": "~5.0.0", "endroid/qr-code": "~1.6.5", "enygma/yubikey": "~3.2" } } 07070100000005000081A400000000000000000000000160E313F5000012BB000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/config.inc.php.dist<?php /** * Kolab 2-Factor-Authentication plugin configuration * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ // available methods/providers. Supported methods are: 'totp','hotp','yubikey' $config['kolab_2fa_drivers'] = array('totp'); // backend for storing 2-factor-auth related per-user settings // available backends are: 'roundcube', 'ldap', 'sql' $config['kolab_2fa_storage'] = 'roundcube'; // should the plugin enforce MFA enrollment // if this is enabled then the user will be redirected to the MFA configuration // page and has to complete enrollement before they can use roundcube again. $config['kolab_2fa_enforce'] = false; // additional config options for the above storage backend // here an example for the LDAP backend: $config['kolab_2fa_storage_config'] = array( 'debug' => false, 'hosts' => array('localhost'), 'port' => 389, 'bind_dn' => 'uid=kolab-auth-service,ou=Special Users,dc=example,dc=org', 'bind_pass' => 'Welcome2KolabSystems', 'base_dn' => 'ou=Tokens,dc=example,dc=org', // filter used to list stored factors for a user 'filter' => '(&(objectClass=ipaToken)(objectclass=ldapSubEntry)(ipatokenOwner=%fu))', 'scope' => 'sub', // translates driver properties to LDAP attributes 'fieldmap' => array( 'label' => 'cn', 'id' => 'ipatokenUniqueID', 'active' => 'ipatokenDisabled', 'created' => 'ipatokenNotBefore', 'userdn' => 'ipatokenOwner', 'secret' => 'ipatokenOTPkey', // HOTP attributes 'counter' => 'ipatokenHOTPcounter', 'digest' => 'ipatokenOTPalgorithm', 'digits' => 'ipatokenOTPdigits', ), // LDAP object classes derived from factor IDs (prefix) // will be translated into the %c placeholder 'classmap' => array( 'totp:' => 'ipatokenTOTP', 'hotp:' => 'ipatokenHOTP', '*' => 'ipaToken', ), // translates property values into LDAP attribute values and vice versa 'valuemap' => array( 'active' => array( false => 'TRUE', true => 'FALSE', ), ), // specify non-string data types for properties for implicit conversion 'attrtypes' => array( 'created' => 'datetime', 'counter' => 'integer', 'digits' => 'integer', ), // apply these default values to factor records if not specified by the drivers 'defaults' => array( 'active' => false, // these are required for ipatokenHOTP records and should match the kolab_2fa_hotp parameters 'digest' => 'sha1', 'digits' => 6, ), // use this LDAP attribute to compose DN values for factor entries 'rdn' => 'ipatokenUniqueID', // assign these object classes to new factor entries 'objectclass' => array( 'top', 'ipaToken', '%c', 'ldapSubEntry', ), // add these roles to the user's LDAP record if key prefix-matches a factor entry 'user_roles' => array( 'totp:' => 'cn=totp-user,dc=example,dc=org', 'hotp:' => 'cn=hotp-user,dc=example,dc=org', ), ); // force a lookup for active authentication factors for this user. // to be set by another plugin (e.g. kolab_auth based on LDAP roles) // $config['kolab_2fa_check'] = true; // timeout for 2nd factor auth submission (in seconds) $config['kolab_2fa_timeout'] = 120; // configuration parameters for TOTP (uncomment to adjust) $config['kolab_2fa_totp'] = array( // 'digits' => 6, // 'interval' => 30, // 'digest' => 'sha1', // 'issuer' => 'Roundcube', ); // configuration parameters for HOTP (uncomment to adjust) $config['kolab_2fa_hotp'] = array( // 'digits' => 6, // 'window' => 4, // 'digest' => 'sha1', ); // configuration parameters for Yubikey (uncomment to adjust) $config['kolab_2fa_yubikey'] = array( 'clientid' => '123456', 'apikey' => '<your-server-api-key>', // 'hosts' => array('api.myhost1.com','api2.myhost.com'), 'use_https' => true, // connect via https if set to true );07070100000006000081A400000000000000000000000160E313F500003369000000000000000000000000000000000000003D00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/kolab2fa.js/** * Kolab 2-Factor-Authentication plugin client functions * * @author Thomas Bruederli <bruederli@kolabsys.com> * * @licstart The following is the entire license notice for the * JavaScript code in this page. * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. * * @licend The above is the entire license notice * for the JavaScript code in this page. */ window.rcmail && rcmail.addEventListener('init', function(evt) { var highsec_call_stack = []; var highsec_dialog; var factor_dialog; if (!rcmail.env.kolab_2fa_factors) { rcmail.env.kolab_2fa_factors = {}; } /** * Equivalend of PHP time() */ function time() { return Math.round(new Date().getTime() / 1000); } /** * Render the settings UI */ function render() { var table = $('#kolab2fa-factors tbody'); table.html(''); var rows = 0; $.each(rcmail.env.kolab_2fa_factors, function(id, props) { if (props.active) { var tr = $('<tr>').addClass(props.method).appendTo(table), button = $('<a class="button icon delete">').attr({href: '#', rel: id}) .append($('<span class="inner">').text(rcmail.get_label('remove','kolab_2fa'))); $('<td>').addClass('name').text(props.label || props.name).appendTo(tr); $('<td>').addClass('created').text(props.created || '??').appendTo(tr); $('<td>').addClass('actions buttons-cell').append(button).appendTo(tr); rows++; } }); table.parent()[(rows > 0 ? 'show' : 'hide')](); } /** * Open dialog to add the given authentication factor */ function add_factor(method) { var lock, form = $('#kolab2fa-prop-' + method), props = rcmail.env.kolab_2fa_factors[method]; if (form.length) { form.get(0).reset(); form.find('img.qrcode').attr('src', 'data:image/gif;base64,R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7'); form.off('submit'); factor_dialog = rcmail.show_popup_dialog( form.show(), rcmail.get_label('addfactor', 'kolab_2fa'), [ { text: rcmail.gettext('save', 'kolab_2fa'), 'class': 'mainaction save', click: function(e) { save_data(method); } }, { text: rcmail.gettext('cancel'), 'class': 'cancel', click: function() { factor_dialog.dialog('close'); } } ], { open: function(event, ui) { $(event.target).find('input[name="_verify_code"]').keypress(function(e) { if (e.which == 13) { $(e.target).closest('.ui-dialog').find('button.mainaction').click(); } }); }, close: function(event, ui) { form.hide().appendTo(document.body); factor_dialog = null; } } ) .data('method', method) .data('timestamp', time()); form.on('submit', function(e) { save_data(method); return false; }); // load generated data lock = rcmail.set_busy(true, 'loading'); rcmail.http_post('plugin.kolab-2fa-data', { _method: method }, lock); } } /** * Remove the given factor from the account */ 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: id, _data: 'false' }, lock); } /** * Submit factor settings form */ function save_data(method) { var lock, data, form = $('#kolab2fa-prop-' + method), verify = form.find('input[name="_verify_code"]'); if (verify.length && !verify.val().length) { alert(rcmail.get_label('verifycodemissing','kolab_2fa')); verify.select(); return false; } data = form_data(form); lock = rcmail.set_busy(true, 'saving'); rcmail.http_post('plugin.kolab-2fa-save', { _method: data.id || method, _data: JSON.stringify(data), _verify_code: verify.val(), _timestamp: factor_dialog ? factor_dialog.data('timestamp') : null }, lock); } /** * Collect all factor properties from the form */ function form_data(form) { var data = {}; form.find('input, select').each(function(i, elem) { if (elem.name.indexOf('_prop') === 0) { k = elem.name.match(/\[([a-z0-9_.-]+)\]$/i) ? RegExp.$1 : null; if (k) { data[k] = elem.tagName == 'SELECT' ? $('option:selected', elem).val() : $(elem).val(); } } }); return data; } /** * Execute the given function after the user authorized the session with a 2nd factor */ 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(id, prop) { if (prop.active && !method || method == exclude) { method = id; name = prop.label || prop.name; if (!exclude || id !== exclude) { return true; } } }); // we have a registered factor, use it if (method) { highsec_call_stack.push(func); // TODO: list all active factors to choose from var html = String($('#kolab2fa-highsecuritydialog').html()).replace('$name', name); highsec_dialog = rcmail.show_popup_dialog( html, rcmail.get_label('highsecurityrequired', 'kolab_2fa'), [ { text: rcmail.gettext('enterhighsecurity', 'kolab_2fa'), click: function(e) { var lock, code = highsec_dialog.find('input[name="_code"]').val(); if (code && code.length) { lock = rcmail.set_busy(true, 'verifying'); rcmail.http_post('plugin.kolab-2fa-verify', { _method: method, _code: code, _session: 1, _timestamp: highsec_dialog.data('timestamp') }, lock); } else { highsec_dialog.find('input[name="_code"]').select(); } }, 'class': 'mainaction save' }, { text: rcmail.gettext('cancel'), 'class': 'cancel', click: function() { highsec_dialog.dialog('close'); } } ], { open: function(event, ui) { // submit code on <Enter> $(event.target).find('input[name="_code"]').keypress(function(e) { if (e.which == 13) { $(e.target).closest('.ui-dialog').find('button.mainaction').click(); } }).select(); }, close: function(event, ui) { $(this).remove(); highsec_dialog = null; highsec_call_stack.pop(); } } ).data('timestamp', time()); return false; } } // just trigger the callback func.call(this); }; // callback for factor data provided by the server rcmail.addEventListener('plugin.render_data', function(data) { var method = data.method, form = $('#kolab2fa-prop-' + method); if (form.length) { $.each(data, function(field, value) { form.find('[name="_prop[' + field + ']"]').val(value); }); if (data.qrcode) { $('img.qrcode[rel='+method+']').attr('src', "data:image/png;base64," + data.qrcode); } if (data.secret) { $('.qrcode_text_plain').text(data.secret); } } else if (window.console) { console.error("Cannot assign auth data", data); } }); // callback for save action rcmail.addEventListener('plugin.save_success', function(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) { factor_dialog.dialog('close'); } render(); }); // callback for verify action rcmail.addEventListener('plugin.verify_response', function(data) { // execute high-security call stack and close dialog if (data.success && highsec_dialog && highsec_dialog.is(':visible')) { var func; while (highsec_call_stack.length) { func = highsec_call_stack.pop(); func(); } highsec_dialog.dialog('close'); rcmail.env.session_secured = time(); } else { rcmail.display_message(data.message, data.success ? 'confirmation' : 'warning'); if (highsec_dialog && highsec_dialog.is(':visible')) { highsec_dialog.find('input[name="_code"]').val('').select(); } else { $('#kolab2fa-prop-' + data.method + ' input.k2fa-verify').val('').select(); } } }); // callback for save failure rcmail.addEventListener('plugin.reset_form', function(method) { if (rcmail.env.kolab_2fa_factors[method]) { rcmail.env.kolab_2fa_factors[method].active = false; } render(); }); // handler for selections $('#kolab2fa-add').change(function() { var method = $('option:selected', this).val(); // require auth verification require_high_security(function() { add_factor(method); }); this.selectedIndex = 0; }); // handler for delete button clicks $('#kolab2fa-factors tbody').on('click', '.button.delete', function(e) { var id = $(this).attr('rel'); // require auth verification require_high_security(function() { if (confirm(rcmail.get_label('authremoveconfirm', 'kolab_2fa'))) { remove_factor(id); } }, id); return false; }); // submit verification code on <Enter> $('.propform input.k2fa-verify').keypress(function(e) { if (e.which == 13) { $(this).closest('.propform').find('.button.verify').click(); } }); // render list initially render(); });07070100000007000081A400000000000000000000000160E313F500007A87000000000000000000000000000000000000003F00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/kolab_2fa.php<?php /** * Kolab 2-Factor-Authentication plugin * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ 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(); $plugin_actions = array('plugin.kolab-2fa','plugin.kolab-2fa-data', 'plugin.kolab-2fa-save', 'plugin.kolab-2fa-verify', 'refresh'); $plugin_internal_actions = array('plugin.kolab-2fa-data', 'plugin.kolab-2fa-save', 'plugin.kolab-2fa-verify', 'refresh'); // register library namespace to autoloader $loader = include(__DIR__ . '/vendor/autoload.php'); $loader->set('Kolab2FA', array($this->home . '/lib')); $minimum_count = $this->minimum_count(); 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')); if ($this->need_factors()) { if (!(in_array($args['action'], $plugin_actions))) { $this->api->output->redirect(array('_task' => 'settings', '_action' => 'plugin.kolab-2fa')); } else if (!(in_array($args['action'], $plugin_internal_actions))) { $this->api->output->show_message("MFA is enforced you need to have at least " . $minimum_count . " second factor configured.", 'error'); } } } else { if ($this->need_factors()) { $this->api->output->redirect(array('_task' => 'settings', '_action' => 'plugin.kolab-2fa')); } } return $args; } private function need_factors() { $rcmail = rcmail::get_instance(); if ( $rcmail->config->get('kolab_2fa_enforce', false) ) { $minimum_count = $this->minimum_count(); $a_host = parse_url($args['host']); $hostname = $_SESSION['hostname'] = $a_host['host'] ?: $args['host']; $lookup = $rcmail->plugins->exec_hook('kolab_2fa_lookup', array( 'user' => $rcmail->get_user_name(), 'host' => $hostname, 'factors' => $rcmail->config->get('kolab_2fa_factors'), 'check' => $rcmail->config->get('kolab_2fa_check', true), )); if (isset($lookup['factors'])) { $factors = (array)$lookup['factors']; } // 2b. check storage if this user has 2FA enabled else if ($lookup['check'] !== false && ($storage = $this->get_storage($args['user']))) { $factors = (array)$storage->enumerate(); } $factors_count = count($factors); return ($factors_count < $minimum_count); } return false; } private function minimum_count() { $rcmail = rcmail::get_instance(); $minimum_count = $rcmail->config->get('kolab_2fa_minimum_count', 1); if ( $rcmail->config->get('kolab_2fa_enforce', false) ) { // we are in enforce mode ... so catch this configuration error and enforce at least one if ($minimum_count < 1) { $minimum_count = 1; } } return $minimum_count; } /** * 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 = $_SESSION['hostname'] = $a_host['host'] ?: $args['host']; // Convert username to lowercase. Copied from rcmail::login() $login_lc = $rcmail->config->get('login_lc', 2); if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $args['user'] = mb_strtolower($args['user']); } else if (strpos($args['user'], '@')) { // lowercase domain name list($local, $domain) = explode('@', $args['user']); $args['user'] = $local . '@' . mb_strtolower($domain); } } // 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()); } // 2a. let plugins provide the list of active authentication factors $lookup = $rcmail->plugins->exec_hook('kolab_2fa_lookup', array( 'user' => $args['user'], 'host' => $hostname, 'factors' => $rcmail->config->get('kolab_2fa_factors'), 'check' => $rcmail->config->get('kolab_2fa_check', true), )); if (isset($lookup['factors'])) { $factors = (array)$lookup['factors']; } // 2b. check storage if this user has 2FA enabled else if ($lookup['check'] !== false && ($storage = $this->get_storage($args['user']))) { $factors = (array)$storage->enumerate(); } if (count($factors) > 0) { $args['abort'] = true; $factors = array_unique($factors); // 3. flag session for 2nd factor verification $_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['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']; $this->login_verified = false; $expired = $time < time() - $rcmail->config->get('kolab_2fa_timeout', 120); if (!empty($factors) && !empty($nonce) && !$expired) { // TODO: check signature // try to verify each configured factor 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($factor, $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']; $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))); // create HTML table with two cols $table = new html_table(array('cols' => 2)); $required = count($methods) > 1 ? null : 'required'; // render input for each configured auth method foreach ($methods as $i => $method) { if ($row++ > 0) { $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', 'data-icon' => 'key' // for Elastic ) + $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 .= $table->show(); // add submit button if (rcube_utils::get_boolean($attrib['submit'])) { $out .= html::p('formbuttons', html::tag('button', array( 'type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction save', ), $this->gettext('continue')) ); } // 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 authentication factor * * @param string $factor Factor identifier (<method>:<id>) * @return Kolab2FA\Driver\Base */ public function get_driver($factor) { list($method) = explode(':', $factor, 2); $rcmail = rcmail::get_instance(); if ($this->drivers[$factor]) { return $this->drivers[$factor]; } $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($factor, $config); // attach storage $driver->storage = $this->get_storage(); if ($rcmail->user->ID) { $driver->username = $rcmail->get_user_name(); } $this->drivers[$factor] = $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); $this->storage->set_logger(new \Kolab2FA\Log\RcubeLogger()); // set user properties from active session if (!empty($_SESSION['kolab_dn'])) { $this->storage->userdn = $_SESSION['kolab_dn']; } } 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' => 'twofactorauth', '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(); $attrib['id'] = 'kolab2fa-add'; $select = new html_select($attrib); $select->add($this->gettext('addfactor') . '...', ''); foreach ((array)$rcmail->config->get('kolab_2fa_drivers', array()) as $method) { $select->add($this->gettext($method), $method); } 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->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; $factors = array_combine( $factors, array_map(function($id) use ($me, &$env_methods) { $props = array('id' => $id); 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; }, $factors) ); $this->api->output->set_env('kolab_2fa_methods', $env_methods); $this->api->output->set_env('kolab_2fa_factors', !empty($factors) ? $factors : null); 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'); if ($driver = $this->get_driver($method)) { $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::div('explain form-text', $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::div('explain form-text', $this->gettext("qrcodeexplain$method")) . html::tag('img', array('src' => 'data:image/gif;base64,R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'class' => 'qrcode', 'rel' => $method)) . html::div('explain form-text', $this->gettext('explain_secret')) . html::p('qrcode_text_plain', 'Hello world') ); // 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::div('explain form-text', $this->gettext("verifycodeexplain$method")) ); } $input_id = new html_hiddenfield(array('name' => '_prop[id]', 'value' => '')); $out .= html::tag('form', array( 'method' => 'post', 'action' => '#', 'id' => 'kolab2fa-prop-' . $method, 'style' => 'display:none', 'class' => 'propform', ), html::tag('fieldset', array(), html::tag('legend', array(), $this->gettext($method)) . html::div('factorprop', $table->show()) . $input_id->show() ) ); } return $out; } /** * Render the high-security-dialog content */ 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)); $label = html::label(array('for' => $field_id, 'class' => 'col-form-label col-sm-4'), '$name'); return html::div($attrib, html::div('explain form-text', $this->gettext('highsecuritydialog')) . html::div('propform row form-group', $label . html::div('col-sm-8', $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(); $storage = $this->get_storage($rcmail->get_user_name()); $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 $success = $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( 'id' => $driver->id, 'method' => $driver->method, 'success' => false, 'message' => str_replace('$method', $this->gettext($driver->method), $this->gettext('codeverificationfailed')) )); $this->api->output->send(); } } foreach ($data as $prop => $value) { if (!$driver->set($prop, $value)) { $errors++; } } $driver->set('active', true); } // commit changes to the user properties if (!$errors) { if ($success = $driver->commit()) { $save_data = $data !== false ? $this->format_props($driver->props()) : array(); } else { $errors++; } } } 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, 'id' => $driver->id) + $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, 'id' => $driver->id); 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(); $parsed_uri = parse_url($uri); parse_str($parsed_uri['query'], $parsed_query_string); $secret = $parsed_query_string['secret']; $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()); $data['secret'] = $secret; } 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); $method = $driver->method; } // 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; 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; } } 07070100000008000041ED00000000000000000000000360E313F500000000000000000000000000000000000000000000003500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib07070100000009000041ED00000000000000000000000660E313F500000000000000000000000000000000000000000000003E00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA0707010000000A000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Driver0707010000000B000081A400000000000000000000000160E313F50000254A000000000000000000000000000000000000004E00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Driver/Base.php<?php /** * Kolab 2-Factor-Authentication Driver base class * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Driver; abstract class Base { public $method; public $id; public $storage; protected $config = array(); protected $props = array(); protected $user_props = array(); protected $pending_changes = false; protected $temporary = false; protected $allowed_props = array('username'); 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($id, $config) { list($method) = explode(':', $id); $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, $id); } throw new Exception("Unknown 2FA driver '$method'"); } /** * Default constructor */ public function __construct($config = null, $id = null) { $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)); $this->temporary = true; } } /** * Initialize the driver with the given config options */ public function init($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']); } } /** * 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; } /** * 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 */ 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_func(array($this, $setter), $value); } else if (in_array($key, $this->allowed_props)) { $this->props[$key] = $value; } 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; $this->temporary = false; } } return !$this->pending_changes; } /** * 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) { return $this->storage->remove($this->id); } return false; } /** * Getter for per-user properties for this method */ protected function get_user_prop($key) { if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes && !$this->temporary) { $this->user_props = (array)$this->storage->read($this->id); } return $this->user_props[$key]; } /** * Setter for per-user properties for this method */ protected function set_user_prop($key, $value) { $this->pending_changes |= ($this->user_props[$key] !== $value); $this->user_props[$key] = $value; return true; } /** * 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]); } } 0707010000000C000081A400000000000000000000000160E313F50000004F000000000000000000000000000000000000005300000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Driver/Exception.php<?php namespace Kolab2FA\Driver; class Exception extends \Exception { } 0707010000000D000081A400000000000000000000000160E313F500000EE4000000000000000000000000000000000000004E00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Driver/HOTP.php<?php /** * Kolab 2-Factor-Authentication HOTP driver implementation * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Driver; class HOTP extends Base { public $method = 'hotp'; protected $config = array( 'digits' => 6, 'window' => 4, 'digest' => 'sha1', ); protected $backend; /** * */ 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 ->setDigits($this->config['digits']) ->setDigest($this->config['digest']) ->setIssuer($this->config['issuer']) ->setIssuerIncludedAsParameter(true); } /** * */ public function verify($code, $timestamp = null) { // get my secret from the user storage $secret = $this->get('secret'); $counter = $this->get('counter'); if (!strlen($secret)) { // LOG: "no secret set for user $this->username" // rcube::console("VERIFY HOTP: no secret set for user $this->username"); return false; } try { $this->backend->setLabel($this->username)->setSecret($secret)->setCounter(intval($this->get('counter'))); $pass = $this->backend->verify($code, $counter, $this->config['window']); // store incremented counter value $this->set('counter', $this->backend->getCounter()); $this->commit(); } catch (\Exception $e) { // LOG: exception // rcube::console("VERIFY HOTP: $this->id, " . strval($e)); $pass = false; } // rcube::console('VERIFY HOTP', $this->username, $secret, $counter, $code, $pass); return $pass; } /** * */ public function get_provisioning_uri() { if (!$this->secret) { // generate new secret and store it $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? $this->backend->setLabel($this->username)->setSecret($this->secret)->setCounter(intval($this->get('counter'))); return $this->backend->getProvisioningUri(); } /** * Generate a random counter value */ public function random_counter() { return mt_rand(1, 999); } } 0707010000000E000081A400000000000000000000000160E313F500000DA4000000000000000000000000000000000000004E00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Driver/TOTP.php<?php /** * Kolab 2-Factor-Authentication TOTP driver implementation * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Driver; class TOTP extends Base { public $method = 'totp'; protected $config = array( 'digits' => 6, 'interval' => 30, 'digest' => 'sha1', ); protected $backend; /** * */ 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 ->setDigits($this->config['digits']) ->setInterval($this->config['interval']) ->setDigest($this->config['digest']) ->setIssuer($this->config['issuer']) ->setIssuerIncludedAsParameter(true); } /** * */ public function verify($code, $timestamp = null) { // get my secret from the user storage $secret = $this->get('secret'); if (!strlen($secret)) { // LOG: "no secret set for user $this->username" // rcube::console("VERIFY TOTP: no secret set for user $this->username"); return false; } $this->backend->setLabel($this->username)->setSecret($secret); // Pass a window to indicate the maximum timeslip between client (mobile // device) and server. $pass = $this->backend->verify($code, $timestamp, 150); // try all codes from $timestamp till now if (!$pass && $timestamp) { $now = time(); while (!$pass && $timestamp < $now) { $pass = $code === $this->backend->at($timestamp); $timestamp += $this->config['interval']; } } // rcube::console('VERIFY TOTP', $this->username, $secret, $code, $timestamp, $pass); return $pass; } /** * */ public function get_provisioning_uri() { // rcube::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)); // rcube::console('PROV2', $this->secret); $this->commit(); } // TODO: deny call if already active? $this->backend->setLabel($this->username)->setSecret($this->secret); return $this->backend->getProvisioningUri(); } } 0707010000000F000081A400000000000000000000000160E313F500000DE3000000000000000000000000000000000000005100000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Driver/Yubikey.php<?php /** * Kolab 2-Factor-Authentication Yubikey driver implementation * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Driver; class Yubikey extends Base { public $method = 'yubikey'; protected $backend; /** * */ public function init($config) { 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']); // set configured validation hosts if (!empty($this->config['hosts'])) { $this->backend->setHosts((array)$this->config['hosts']); } if (isset($this->config['use_https'])) { $this->backend->setUseSecure((bool)$this->config['use_https']); } } /** * */ public function verify($code, $timestamp = null) { // get my secret from the user storage $keyid = $this->get('yubikeyid'); $pass = false; if (!strlen($keyid)) { // LOG: "no key registered for user $this->username" return false; } // check key prefix with associated Yubikey ID if (strpos($code, $keyid) === 0) { try { $response = $this->backend->check($code); $pass = $response->success() === true; } catch (\Exception $e) { // TODO: log exception } } // rcube::console('VERIFY Yubikey', $this->username, $keyid, $code, $pass); return $pass; } /** * @override */ public function set($key, $value, $persistent = true) { if ($key == 'yubikeyid' && strlen($value) > 12) { // verify the submitted code try { $response = $this->backend->check($value); if ($response->success() !== true) { // TODO: report error return false; } } catch (\Exception $e) { return false; } // truncate the submitted yubikey code to 12 characters $value = substr($value, 0, 12); } return parent::set($key, $value, $persistent); } /** * @override */ protected function set_user_prop($key, $value) { // set created timestamp if ($key !== 'created' && !isset($this->created)) { parent::set_user_prop('created', $this->get('created', true)); } return parent::set_user_prop($key, $value); } } 07070100000010000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000004200000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Log07070100000011000081A400000000000000000000000160E313F5000004BD000000000000000000000000000000000000004D00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Log/Logger.php<?php /** * Logging interface for the Kolab 2-Factor-Authentication components * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Log; interface Logger { /** * Setter for the log name */ public function set_name($name); /** * Setter for the minimum log level */ public function set_level($level); /** * Do log the given message at the given level */ public function log($level, $message); } 07070100000012000081A400000000000000000000000160E313F500000811000000000000000000000000000000000000005200000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Log/RcubeLogger.php<?php /** * Kolab 2-Factor-Authentication Logging class to log messages * through the Roundcube logging facilities. * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Log; use \rcube; class RcubeLogger implements Logger { protected $name = null; protected $level = LOG_DEBUG; public function __construct($name = null) { if ($name !== null) { $this->set_name($name); } } public function set_name($name) { $this->name = $name; } public function set_level($name) { $this->level = $level; } public function log($level, $message) { if (!is_string($message)) { $message = var_export($message, true); } switch ($level) { case LOG_DEBUG: case LOG_INFO: case LOG_NOTICE: if ($level >= $this->level) { rcube::write_log($this->name ?: 'console', $message); } break; case LOG_EMERGE: case LOG_ALERT: case LOG_CRIT: case LOG_ERR: case LOG_WARNING: rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'message' => $message, ), true, false); break; } } } 07070100000013000081A400000000000000000000000160E313F5000005D2000000000000000000000000000000000000004D00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Log/Syslog.php<?php /** * Kolab 2-Factor-Authentication Logging class to log messages * through the Roundcube logging facilities. * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Log; use \rcube; class Syslog implements Logger { protected $name = 'Kolab2FA'; protected $level = LOG_INFO; public function set_name($name) { $this->name = $name; } public function set_level($name) { $this->level = $level; } public function log($level, $message) { if ($level >= $this->level) { if (!is_string($message)) { $message = var_export($message, true); } syslog($level, '[' . $this->name . '] ' . $message); } } }07070100000014000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000004200000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/OTP07070100000015000081A400000000000000000000000160E313F5000005FF000000000000000000000000000000000000004B00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/OTP/HOTP.php<?php /** * Kolab HOTP implementation based on Spomky-Labs/otphp * * This basically follows the exmaple implementation from * https://github.com/Spomky-Labs/otphp/tree/master/examples * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\OTP; use OTPHP\HOTP as Base; class HOTP extends Base { use OTP; protected $counter = 0; public function setCounter($counter) { if (!is_integer($counter) || $counter < 0) { throw new \Exception('Counter must be at least 0.'); } $this->counter = $counter; return $this; } public function getCounter() { return $this->counter; } public function updateCounter($counter) { $this->counter = $counter; return $this; } }07070100000016000081A400000000000000000000000160E313F500000CB2000000000000000000000000000000000000004A00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/OTP/OTP.php<?php /** * Kolab OTP trait based on Spomky-Labs/otphp * * This basically follows the exmaple implementation from * https://github.com/Spomky-Labs/otphp/tree/master/examples * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\OTP; trait OTP { protected $secret = null; protected $issuer = null; protected $issuer_included_as_parameter = false; protected $label = null; protected $digest = 'sha1'; protected $digits = 6; public function setSecret($secret) { $this->secret = $secret; return $this; } public function getSecret() { return $this->secret; } public function setLabel($label) { if ($this->hasSemicolon($label)) { throw new \Exception('Label must not contain a semi-colon.'); } $this->label = $label; return $this; } public function getLabel() { return $this->label; } public function setIssuer($issuer) { if ($this->hasSemicolon($issuer)) { throw new \Exception('Issuer must not contain a semi-colon.'); } $this->issuer = $issuer; return $this; } public function getIssuer() { return $this->issuer; } public function isIssuerIncludedAsParameter() { return $this->issuer_included_as_parameter; } public function setIssuerIncludedAsParameter($issuer_included_as_parameter) { $this->issuer_included_as_parameter = $issuer_included_as_parameter; return $this; } public function setDigits($digits) { if (!is_numeric($digits) || $digits < 1) { throw new \Exception('Digits must be at least 1.'); } $this->digits = $digits; return $this; } public function getDigits() { return $this->digits; } public function setDigest($digest) { if (!in_array($digest, array('md5', 'sha1', 'sha256', 'sha512'))) { throw new \Exception("'$digest' digest is not supported."); } $this->digest = $digest; return $this; } public function getDigest() { return $this->digest; } private function hasSemicolon($value) { $semicolons = array(':', '%3A', '%3a'); foreach ($semicolons as $semicolon) { if (false !== strpos($value, $semicolon)) { return true; } } return false; } }07070100000017000081A400000000000000000000000160E313F500000596000000000000000000000000000000000000004B00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/OTP/TOTP.php<?php /** * Kolab TOTP implementation based on Spomky-Labs/otphp * * This basically follows the exmaple implementation from * https://github.com/Spomky-Labs/otphp/tree/master/examples * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\OTP; use OTPHP\TOTP as Base; class TOTP extends Base { use OTP; protected $interval = 30; public function setInterval($interval) { if (!is_integer($interval) || $interval < 1) { throw new \Exception('Interval must be at least 1.'); } $this->interval = $interval; return $this; } public function getInterval() { return $this->interval; } }07070100000018000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000004600000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Storage07070100000019000081A400000000000000000000000160E313F500000C36000000000000000000000000000000000000004F00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Storage/Base.php<?php /** * Abstract storage backend class for the Kolab 2-Factor-Authentication plugin * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Storage; use \Kolab2FA\Log; abstract class Base { public $username = null; protected $config = array(); protected $logger; /** * */ 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); // use syslog logger by default $this->set_logger(new Log\Syslog()); } /** * */ public function set_logger(Log\Logger $logger) { $this->logger = $logger; if ($this->config['debug']) { $this->logger->set_level(LOG_DEBUG); } else if ($this->config['loglevel']) { $this->logger->set_level($this->config['loglevel']); } } /** * Set username to store data for */ public function set_username($username) { $this->username = $username; } /** * Send messager to the logging system */ protected function log($level, $message) { if ($this->logger) { $this->logger->log($level, $message); } } /** * List keys holding settings for 2-factor-authentication */ abstract public function enumerate(); /** * 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 stored for the given key */ abstract public function remove($key); } 0707010000001A000081A400000000000000000000000160E313F500000050000000000000000000000000000000000000005400000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Storage/Exception.php<?php namespace Kolab2FA\Storage; class Exception extends \Exception { } 0707010000001B000081A400000000000000000000000160E313F50000288D000000000000000000000000000000000000004F00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Storage/LDAP.php<?php /** * Storage backend to store 2-Factor-Authentication settings in LDAP * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Storage; use \Net_LDAP3; use \Kolab2FA\Log\Logger; class LDAP extends Base { public $userdn; private $cache = array(); private $ldapcache = array(); private $conn; private $error; public function init(array $config) { parent::init($config); $this->conn = new Net_LDAP3($config); $this->conn->config_set('log_hook', array($this, 'log')); $this->conn->connect(); $bind_pass = $this->config['bind_pass']; $bind_user = $this->config['bind_user']; $bind_dn = $this->config['bind_dn']; $this->ready = $this->conn->bind($bind_dn, $bind_pass); if (!$this->ready) { throw new Exception("LDAP storage not ready: " . $this->error); } } /** * List/set methods activated for this user */ public function enumerate($active = true) { $filter = $this->parse_vars($this->config['filter'], '*'); $base_dn = $this->parse_vars($this->config['base_dn'], '*'); $scope = $this->config['scope'] ?: 'sub'; $ids = array(); if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array($this->config['fieldmap']['id'], $this->config['fieldmap']['active'])))) { foreach ($result as $dn => $entry) { $rec = $this->field_mapping($dn, Net_LDAP3::normalize_entry($entry, true)); if (!empty($rec['id']) && ($active === null || $active == $rec['active'])) { $ids[] = $rec['id']; } } } // TODO: cache this in memory return $ids; } /** * Read data for the given key */ public function read($key) { if (!isset($this->cache[$key])) { $this->cache[$key] = $this->get_ldap_record($this->username, $key); } return $this->cache[$key]; } /** * Save data for the given key */ public function write($key, $value) { $success = false; $ldap_attrs = array(); if (is_array($value)) { // add some default values $value += (array)$this->config['defaults'] + array('active' => false, 'username' => $this->username, 'userdn' => $this->userdn); foreach ($value as $k => $val) { if ($attr = $this->config['fieldmap'][$k]) { $ldap_attrs[$attr] = $this->value_mapping($k, $val, false); } } } else { // invalid data structure return false; } // update existing record if ($rec = $this->get_ldap_record($this->username, $key)) { $old_attrs = $rec['_raw']; $new_attrs = array_merge($old_attrs, $ldap_attrs); $result = $this->conn->modify_entry($rec['_dn'], $old_attrs, $new_attrs); $success = !empty($result); } // insert new record else if ($this->ready) { $entry_dn = $this->get_entry_dn($this->username, $key); // add object class attribute $me = $this; $ldap_attrs['objectclass'] = array_map(function($cls) use ($me, $key) { return $me->parse_vars($cls, $key); }, (array)$this->config['objectclass']); $success = $this->conn->add_entry($entry_dn, $ldap_attrs); } if ($success) { $this->cache[$key] = $value; $this->ldapcache = array(); // cleanup: remove disabled/inactive/temporary entries if ($value['active']) { foreach ($this->enumerate(false) as $id) { if ($id != $key) { $this->remove($id); } } // set user roles according to active factors $this->set_user_roles(); } } return $success; } /** * Remove the data stored for the given key */ public function remove($key) { if ($this->ready) { $entry_dn = $this->get_entry_dn($this->username, $key); $success = $this->conn->delete_entry($entry_dn); // set user roles according to active factors if ($success) { $this->set_user_roles(); } return $success; } return false; } /** * Set username to store data for */ public function set_username($username) { parent::set_username($username); // reset cached values $this->cache = array(); $this->ldapcache = array(); } /** * */ protected function set_user_roles() { if (!$this->ready || !$this->userdn || empty($this->config['user_roles'])) { return false; } $auth_roles = array(); foreach ($this->enumerate(true) as $id) { foreach ($this->config['user_roles'] as $prefix => $role) { if (strpos($id, $prefix) === 0) { $auth_roles[] = $role; } } } $role_attr = $this->config['fieldmap']['roles'] ?: 'nsroledn'; if ($user_attrs = $this->conn->get_entry($this->userdn, array($role_attr))) { $internals = array_values($this->config['user_roles']); $new_attrs = $old_attrs = Net_LDAP3::normalize_entry($user_attrs); $new_attrs[$role_attr] = array_merge( array_unique($auth_roles), array_filter((array)$old_attrs[$role_attr], function($f) use ($internals) { return !in_array($f, $internals); }) ); $result = $this->conn->modify_entry($this->userdn, $old_attrs, $new_attrs); return !empty($result); } return false; } /** * Fetches user data from LDAP addressbook */ protected function get_ldap_record($user, $key) { $entry_dn = $this->get_entry_dn($user, $key); if (!isset($this->ldapcache[$entry_dn])) { $this->ldapcache[$entry_dn] = array(); if ($this->ready && ($entry = $this->conn->get_entry($entry_dn, array_values($this->config['fieldmap'])))) { $this->ldapcache[$entry_dn] = $this->field_mapping($entry_dn, Net_LDAP3::normalize_entry($entry, true)); } } return $this->ldapcache[$entry_dn]; } /** * Compose a full DN for the given record identifier */ protected function get_entry_dn($user, $key) { $base_dn = $this->parse_vars($this->config['base_dn'], $key); return sprintf('%s=%s,%s', $this->config['rdn'], Net_LDAP3::quote_string($key, true), $base_dn); } /** * Maps LDAP attributes to defined fields */ protected function field_mapping($dn, $entry) { $entry['_dn'] = $dn; $entry['_raw'] = $entry; // fields mapping foreach ($this->config['fieldmap'] as $field => $attr) { $attr_lc = strtolower($attr); if (isset($entry[$attr_lc])) { $entry[$field] = $this->value_mapping($field, $entry[$attr_lc], true); } else if (isset($entry[$attr])) { $entry[$field] = $this->value_mapping($field, $entry[$attr], true); } } return $entry; } /** * */ protected function value_mapping($attr, $value, $reverse = false) { if ($map = $this->config['valuemap'][$attr]) { if ($reverse) { $map = array_flip($map); } if (is_array($value)) { $value = array_filter(array_map(function($val) use ($map) { return $map[$val]; }, $value)); } else { $value = $map[$value]; } } // convert (date) type switch ($this->config['attrtypes'][$attr]) { case 'datetime': $ts = is_numeric($value) ? $value : strtotime($value); if ($ts) { $value = gmdate($reverse ? 'U' : 'YmdHi\Z', $ts); } break; case 'integer': $value = intval($value); break; } return $value; } /** * Prepares filter query for LDAP search */ protected function parse_vars($str, $key) { $user = $this->username; if (strpos($user, '@') > 0) { list($u, $d) = explode('@', $user); } else if ($this->userdn) { $u = $this->userdn; $d = trim(str_replace(',dc=', '.', substr($u, strpos($u, ',dc='))), '.'); } if ($this->userdn) { $user = $this->userdn; } // build hierarchal domain string $dc = $this->conn->domain_root_dn($d); $class = $this->config['classmap'] ? $this->config['classmap']['*'] : '*'; // map key to objectclass if (is_array($this->config['classmap'])) { foreach ($this->config['classmap'] as $k => $c) { if (strpos($key, $k) === 0) { $class = $c; break; } } } $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%c' => $class); return strtr($str, $replaces); } } 0707010000001C000081A400000000000000000000000160E313F50000147C000000000000000000000000000000000000005400000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/lib/Kolab2FA/Storage/RcubeUser.php<?php /** * Storage backend to use the Roundcube user prefs to store 2-Factor-Authentication settings * * @author Thomas Bruederli <bruederli@kolabsys.com> * * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> * * 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 <http://www.gnu.org/licenses/>. */ namespace Kolab2FA\Storage; use \rcmail; use \rcube_user; class RcubeUser extends Base { // sefault config protected $config = array( 'keymap' => array(), ); 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']; } /** * 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])) { $factors = $this->get_factors(); $this->log(LOG_DEBUG, 'RcubeUser::read() ' . $key); $this->cache[$key] = $factors[$key]; } return $this->cache[$key]; } /** * Save data for the given key */ public function write($key, $value) { $this->log(LOG_DEBUG, 'RcubeUser::write() ' . @json_encode($value)); if ($user = $this->get_user($this->username)) { $this->cache[$key] = $value; $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']); }) ); } $success = $user->save_prefs($save_data, true); if (!$success) { $this->log(LOG_WARNING, sprintf('Failed to save prefs for user %s', $this->username)); } return $success; } return false; } /** * Remove the data stored for the given key */ public function remove($key) { return $this->write($key, null); } /** * Set username to store data for */ public function set_username($username) { 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; } if (!$this->user) { $this->user = rcube_user::query($username, $this->config['hostname']); } if (!$this->user) { $this->log(LOG_WARNING, sprintf('No user record found for %s @ %s', $username, $this->config['hostname'])); } 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; } /** * */ private function key2property($key) { // map key to configured property name if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) { return $this->config['keymap'][$key]; } // default return 'kolab_2fa_' . $key; } } 0707010000001D000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000003E00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization0707010000001E000081A400000000000000000000000160E313F500000136000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/bg_BG.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Създаден'; $labels['remove'] = 'Изтриване'; $labels['label'] = 'Име'; 0707010000001F000081A400000000000000000000000160E313F50000011F000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/ca_ES.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Creat'; $labels['remove'] = 'Suprimeix'; $labels['label'] = 'Nom'; 07070100000020000081A400000000000000000000000160E313F500000CEA000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/cs_CZ.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['factor'] = 'Faktor'; $labels['secret'] = 'Tajný klíč'; $labels['created'] = 'Vytvořeno'; $labels['remove'] = 'Odstranit'; $labels['continue'] = 'Pokračovat'; $labels['settingslist'] = 'Multifaktorová autentizace'; $labels['settingstitle'] = 'Autentizační faktory'; $labels['totp'] = 'Mobilní appka (TOTP)'; $labels['hotp'] = 'Mobilní appka (HOTP)'; $labels['yubikey'] = 'Yubikey'; $labels['or'] = 'anebo'; $labels['yes'] = 'ano'; $labels['no'] = 'ne'; $labels['label'] = 'Název'; $labels['qrcode'] = 'QR kód'; $labels['showqrcode'] = 'Ukázat QR kód'; $labels['qrcodeexplaintotp'] = 'Stáhni si autentizační appku na svůj telefon. Dobře fungují appky <strong>FreeOTP</strong> a <strong>Google Authenticator</strong>, ale fungovat by měla i jakákoliv jiná TOTP appka.<br/><br/> Spusť appku na svém telefonu a přidej novou položku pro tuto službu. Pak si naskenuj níže zobrazený QR kód - až budeš k tomu vyzván. Tím dojde ke konfiguraci Tvé mobilní appky.'; $labels['qrcodeexplainhotp'] = 'Stáhni si autentizační appku na svůj telefon. Dobře fungují appka <strong>Google Authenticator</strong>, ale fungovat by měla i jakákoliv jiná HOTP appka.<br/><br/> Spusť appku na svém telefonu a přidej novou položku pro tuto službu. Pak si naskenuj níže zobrazený QR kód - až budeš k tomu vyzván. Tím dojde ke konfiguraci Tvé mobilní appky.'; $labels['yubikeyid'] = 'ID Tvého YubiKey'; $labels['yubikeyidexplainyubikey'] = 'Jednou stiskni tlačítko na svém YubiKey a pak submitni vygenerovaný kód'; $labels['addfactor'] = 'Přidat autentizační faktor'; $labels['testfactor'] = 'Ostestovat tento faktor'; $labels['verifycode'] = 'Verifikovat kód'; $labels['verifycodeexplaintotp'] = 'Po naskenování QR kódu zadej šestimístní verifikační kód, jenž vygenerovala autentizační appka.'; $labels['verifycodeexplainhotp'] = 'Po naskenování QR kódu zadej šestimístní verifikační kód, jenž vygenerovala autentizační appka.'; $labels['loginexpired'] = 'Žádost o přihlášení vypršela! Prosím, zkus to znovu.'; $labels['authremoveconfirm'] = 'Opravdu chceš tento autentizační faktor ze svého účtu trvale odstranit?'; $labels['verifycodemissing'] = 'Zadej prosím verifikační kód z Tvého zařízení'; $labels['factorsavesuccess'] = 'Autentizační faktor uložen'; $labels['factorremovesuccess'] = 'Autentizační faktor odstraněn'; $labels['factorsaveerror'] = 'Nepodařilo se uložit nastavení autentizačního faktoru'; $labels['codeverificationpassed'] = 'Verifikace kódu pro $method se podařila'; $labels['codeverificationfailed'] = 'Verifikace kódu pro $method selhala'; $labels['highsecurityrequired'] = 'Vstupuješ do módu vysokého zabezpečení - požaduje se verifikace'; $labels['highsecuritydialog'] = 'Mód vysokého zabezpečení chrání Tvůj účet před bezpečnostními hrozbami - např. krádežemi sessionů. Ke vstupu do módu vysokého zabezpečení dolož svou identitu přihlášením se s přidaným autentizačním faktorem:'; $labels['enterhighsecurity'] = 'Potvrdit'; 07070100000021000081A400000000000000000000000160E313F50000019A000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/da_DK.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Oprettet'; $labels['remove'] = 'Fjern'; $labels['continue'] = 'Fortsæt'; $labels['yes'] = 'ja'; $labels['no'] = 'nej'; $labels['label'] = 'Navn'; $labels['enterhighsecurity'] = 'Bekræft'; 07070100000022000081A400000000000000000000000160E313F5000001B8000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/de.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Erstellt'; $labels['remove'] = 'Entfernen'; $labels['continue'] = 'Weiter'; $labels['or'] = 'oder'; $labels['yes'] = 'Ja'; $labels['no'] = 'Nein'; $labels['label'] = 'Name'; $labels['enterhighsecurity'] = 'Bestätigen'; 07070100000023000081A400000000000000000000000160E313F50000019F000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/de_CH.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Erstellt'; $labels['remove'] = 'Löschen'; $labels['continue'] = 'Weiter'; $labels['yes'] = 'Ja'; $labels['no'] = 'Nein'; $labels['label'] = 'Name'; $labels['enterhighsecurity'] = 'Bestätigen'; 07070100000024000081A400000000000000000000000160E313F5000001B8000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/de_DE.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Erstellt'; $labels['remove'] = 'Entfernen'; $labels['continue'] = 'Weiter'; $labels['or'] = 'oder'; $labels['yes'] = 'Ja'; $labels['no'] = 'Nein'; $labels['label'] = 'Name'; $labels['enterhighsecurity'] = 'Bestätigen'; 07070100000025000081A400000000000000000000000160E313F500000177000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/el.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Δημιουργήθηκε'; $labels['remove'] = 'Αφαίρεση'; $labels['yes'] = 'ναι'; $labels['no'] = 'όχι'; $labels['label'] = 'Όνομα'; 07070100000026000081A400000000000000000000000160E313F500000C72000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/en_US.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['factor'] = 'Factor'; $labels['secret'] = 'Secret Key'; $labels['created'] = 'Created'; $labels['remove'] = 'Remove'; $labels['continue'] = 'Continue'; $labels['settingslist'] = 'Multi-Factor Auth'; $labels['settingstitle'] = 'Authentication Factors'; $labels['totp'] = 'Mobile App (TOTP)'; $labels['hotp'] = 'Mobile App (HOTP)'; $labels['yubikey'] = 'Yubikey'; $labels['or'] = 'or'; $labels['yes'] = 'yes'; $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 <strong>FreeOTP</strong> and <strong>Google Authenticator</strong>, but any other TOTP app should also work.<br/><br/> Launch the app on your phone, and add a new entry for this service. When prompted, scan the QR code below to configure your mobile app.'; $labels['qrcodeexplainhotp'] = 'Download an authenticator app on your phone. One app known to work well is <strong>Google Authenticator</strong>, but any other HOTP app should also work.<br/><br/> Launch the app on your phone, and add a new entry for this service. When prompted, scan the QR code below to configure your mobile app.'; $labels['explain_secret'] = "If you can not scan the QR code above, you can enter the secret code below into the app directly"; $labels['yubikeyid'] = 'Your YubiKey ID'; $labels['yubikeyidexplainyubikey'] = 'Press your YubiKey once and submit the generated code'; $labels['addfactor'] = 'Add Authentication Factor'; $labels['testfactor'] = 'Test this factor'; $labels['verifycode'] = 'Verify Code'; $labels['verifycodeexplaintotp'] = 'Once you have scanned the QR code, enter the 6-digit verification code generated by the authenticator app.'; $labels['verifycodeexplainhotp'] = 'Once you have scanned the QR code, enter the 6-digit verification code generated by the authenticator app.'; $labels['loginexpired'] = 'Login request expired! Please try again.'; $labels['authremoveconfirm'] = 'Do you really want to remove this authentication factor from your account?'; $labels['verifycodemissing'] = 'Please enter the verification code from your device'; $labels['factorsavesuccess'] = 'Successfully saved authentication factor'; $labels['factorremovesuccess'] = 'Successfully removed the authentication factor'; $labels['factorsaveerror'] = 'Failed to save authentication factor settings'; $labels['codeverificationpassed'] = 'Code verification for $method passed'; $labels['codeverificationfailed'] = 'Code verification for $method failed'; $labels['highsecurityrequired'] = 'Entering High Security - Verification required'; $labels['highsecuritydialog'] = 'High security mode helps protect your account from security threats, like session theft or someone messing with your stuff while you\'re away. To enter high security mode, confirm your credentials with the additional authentication factor:'; $labels['enterhighsecurity'] = 'Confirm'; 07070100000027000081A400000000000000000000000160E313F50000019E000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/es_AR.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Creado'; $labels['remove'] = 'Elminiar'; $labels['continue'] = 'Continuar'; $labels['yes'] = 'si'; $labels['no'] = 'no'; $labels['label'] = 'Nombre'; $labels['enterhighsecurity'] = 'Confirmar'; 07070100000028000081A400000000000000000000000160E313F500000150000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/es_ES.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Creado'; $labels['remove'] = 'Eliminar'; $labels['yes'] = 'sí'; $labels['no'] = 'no'; $labels['label'] = 'Nombre'; 07070100000029000081A400000000000000000000000160E313F50000014F000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/et_EE.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Loodud'; $labels['remove'] = 'Eemalda'; $labels['yes'] = 'jah'; $labels['no'] = 'ˇei'; $labels['label'] = 'Nimi'; 0707010000002A000081A400000000000000000000000160E313F5000005E2000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/fi_FI.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['factor'] = 'Tekijä'; $labels['secret'] = 'Salainen avain'; $labels['created'] = 'Luotu'; $labels['remove'] = 'Poista'; $labels['continue'] = 'Jatka'; $labels['settingslist'] = 'Monivaiheinen tunnistautuminen'; $labels['settingstitle'] = 'Tunnistautumistekijät'; $labels['totp'] = 'Mobiilisovellus (TOTP)'; $labels['hotp'] = 'Mobiilisovellus (HOTP)'; $labels['yubikey'] = 'Yubikey'; $labels['or'] = 'tai'; $labels['yes'] = 'kyllä'; $labels['no'] = 'ei'; $labels['label'] = 'Nimi'; $labels['qrcode'] = 'QR-koodi'; $labels['showqrcode'] = 'Näytä QR-koodi'; $labels['yubikeyid'] = 'YubiKey-tunnisteesi'; $labels['yubikeyidexplainyubikey'] = 'Paina YubiKeytasi kerran ja kirjoita luotu koodi'; $labels['addfactor'] = 'Lisää tunnistautumistekijä'; $labels['testfactor'] = 'Testaa tätä tekijää'; $labels['verifycode'] = 'Vahvista koodi'; $labels['loginexpired'] = 'Kirjautumispyyntö vanheni! Yritä uudelleen.'; $labels['authremoveconfirm'] = 'Haluatko varmasti poistaa tämän tunnistautumistekijän tililtäsi?'; $labels['verifycodemissing'] = 'Kirjoita vahvistuskoodi laitteeltasi'; $labels['factorsavesuccess'] = 'Tunnistautumistekijä tallennettu onnistuneesti'; $labels['factorremovesuccess'] = 'Tunnistautumistekijä poistettu onnistuneesti'; $labels['enterhighsecurity'] = 'Vahvista'; 0707010000002B000081A400000000000000000000000160E313F5000001B4000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/fr_FR.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Créé'; $labels['remove'] = 'Supprimer'; $labels['continue'] = 'Continuer'; $labels['or'] = 'ou'; $labels['yes'] = 'oui'; $labels['no'] = 'non'; $labels['label'] = 'Nom'; $labels['enterhighsecurity'] = 'Confirmer'; 0707010000002C000081A400000000000000000000000160E313F500000101000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/he_IL.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Created'; $labels['label'] = 'Name'; 0707010000002D000081A400000000000000000000000160E313F500000101000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/hr_HR.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Created'; $labels['label'] = 'Name'; 0707010000002E000081A400000000000000000000000160E313F5000001A1000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/hu_HU.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Created'; $labels['remove'] = 'Eltávolítás'; $labels['continue'] = 'Continue'; $labels['yes'] = 'yes'; $labels['no'] = 'no'; $labels['label'] = 'Név'; $labels['enterhighsecurity'] = 'Confirm'; 0707010000002F000081A400000000000000000000000160E313F50000019A000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/it_IT.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Creato'; $labels['remove'] = 'Elimina'; $labels['continue'] = 'Continua'; $labels['yes'] = 'sì'; $labels['no'] = 'no'; $labels['label'] = 'Nome'; $labels['enterhighsecurity'] = 'Conferma'; 07070100000030000081A400000000000000000000000160E313F5000001A7000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/ja_JP.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = '作成済'; $labels['remove'] = '削除'; $labels['continue'] = '続ける'; $labels['yes'] = 'はい'; $labels['no'] = 'いいえ'; $labels['label'] = '名前'; $labels['enterhighsecurity'] = '確認'; 07070100000031000081A400000000000000000000000160E313F500000105000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/ko_KR.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = '작성됨'; $labels['label'] = '이름'; 07070100000032000081A400000000000000000000000160E313F500000120000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/lv.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Izveidots'; $labels['yes'] = 'jā'; $labels['label'] = 'Nosaukums'; 07070100000033000081A400000000000000000000000160E313F50000013E000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/nl_NL.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Gemaakt op'; $labels['remove'] = 'Verwijderen'; $labels['yes'] = 'ja'; $labels['label'] = 'Naam'; 07070100000034000081A400000000000000000000000160E313F500000121000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/pl.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Utworzono'; $labels['remove'] = 'Usuń'; $labels['label'] = 'Nazwa'; 07070100000035000081A400000000000000000000000160E313F5000006B1000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/pl_PL.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['factor'] = 'Czynnik'; $labels['secret'] = 'Tajny klucz '; $labels['created'] = 'Utworzono'; $labels['remove'] = 'Usuń'; $labels['continue'] = 'Kontynuuj'; $labels['settingslist'] = 'Uwierzytelnianie wieloetapowe'; $labels['settingstitle'] = 'Czynniki uwierzytelniania'; $labels['totp'] = 'Aplikacja mobilna (TOTP)'; $labels['hotp'] = 'Aplikacja mobilna (HOTP)'; $labels['yubikey'] = 'Yubikey'; $labels['or'] = 'lub'; $labels['yes'] = 'tak'; $labels['no'] = 'nie'; $labels['label'] = 'Nazwa'; $labels['qrcode'] = 'Kod QR'; $labels['showqrcode'] = 'Pokaż kod QR'; $labels['qrcodeexplaintotp'] = 'Pobierz aplikację uwierzytelniającą na swój telefon. <strong>FreeOTP</strong> i <strong>Google Authenticator</strong> są przetestowane, ale dowolna aplikacja TOTP powinna również działać.<br/><br/> Uruchom aplikację na swoim telefonie i zarejestruj tą usługę. Gdy aplikacja o to poprosi, zeskanuj kod QR.'; $labels['qrcodeexplainhotp'] = 'Pobierz aplikację uwierzytelniającą na swój telefon. <strong>Google Authenticator</strong> jest znana z poprawnej pracy, ale dowolna aplikacja HOTP powinna również działać.<br/><br/> Uruchom aplikację na swoim telefonie i zarejestruj tą usługę. Gdy aplikacja o to poprosi, zeskanuj kod QR.'; $labels['yubikeyid'] = 'Twój identyfikator Yubikey'; $labels['yubikeyidexplainyubikey'] = 'Naciśnij swojego Yubikeya i wprowadź wygenerowany kod'; $labels['addfactor'] = 'Dodaj czynnik uwierzytelniający'; $labels['enterhighsecurity'] = 'Potwierdź'; 07070100000036000081A400000000000000000000000160E313F50000019E000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/pt_BR.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Criado'; $labels['remove'] = 'Remover'; $labels['continue'] = 'Continuar'; $labels['yes'] = 'sim'; $labels['no'] = 'não'; $labels['label'] = 'Nome'; $labels['enterhighsecurity'] = 'Confirmar'; 07070100000037000081A400000000000000000000000160E313F500000103000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/pt_PT.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Criado em'; $labels['label'] = 'Nome'; 07070100000038000081A400000000000000000000000160E313F500000355000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/ru_RU.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['secret'] = 'Секретный ключ'; $labels['created'] = 'Создана'; $labels['remove'] = 'Убрать'; $labels['continue'] = 'Продолжить'; $labels['settingstitle'] = 'Факторы авторизации'; $labels['or'] = 'или'; $labels['yes'] = 'да'; $labels['no'] = 'нет'; $labels['label'] = 'Имя'; $labels['qrcode'] = 'QR код'; $labels['showqrcode'] = 'Показать QR код'; $labels['verifycode'] = 'Подтвердить код'; $labels['factorsavesuccess'] = 'Успешно сохраненный фактор авторизации'; $labels['enterhighsecurity'] = 'Подтвердить'; 07070100000039000081A400000000000000000000000160E313F5000000E1000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/sk_SK.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['label'] = 'Meno'; 0707010000003A000081A400000000000000000000000160E313F500000123000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/sl_SI.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Ustvarjeno'; $labels['remove'] = 'Odstrani'; $labels['label'] = 'Ime'; 0707010000003B000081A400000000000000000000000160E313F500000100000000000000000000000000000000000000004500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/sv.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Skapad'; $labels['label'] = 'Namn'; 0707010000003C000081A400000000000000000000000160E313F50000019C000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/sv_SE.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Skapad'; $labels['remove'] = 'Ta bort'; $labels['continue'] = 'Fortsätt'; $labels['yes'] = 'ja'; $labels['no'] = 'nej'; $labels['label'] = 'Namn'; $labels['enterhighsecurity'] = 'Bekräfta'; 0707010000003D000081A400000000000000000000000160E313F50000017C000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/th_TH.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'สร้างเมื่อ'; $labels['remove'] = 'ลบ'; $labels['yes'] = 'ตกลง'; $labels['no'] = 'ไม่'; $labels['label'] = 'ชื่อ'; 0707010000003E000081A400000000000000000000000160E313F500000110000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/uk_UA.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Створена'; $labels['label'] = 'Назва'; 0707010000003F000081A400000000000000000000000160E313F500000101000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/vi_VN.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Created'; $labels['label'] = 'Name'; 07070100000040000081A400000000000000000000000160E313F500000152000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/zh_CN.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = '已创建'; $labels['remove'] = '移除'; $labels['yes'] = '是'; $labels['no'] = '否'; $labels['label'] = '名称'; 07070100000041000081A400000000000000000000000160E313F500000101000000000000000000000000000000000000004800000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/localization/zh_TW.inc<?php /** * Localizations for the Kolab 2-Factor-Auth plugin * * Copyright (C) 2015, Kolab Systems AG * * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_2fa/ */ $labels['created'] = 'Created'; $labels['label'] = 'Name'; 07070100000042000041ED00000000000000000000000460E313F500000000000000000000000000000000000000000000003700000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins07070100000043000041ED00000000000000000000000360E313F500000000000000000000000000000000000000000000003F00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/elastic07070100000044000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000004900000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/elastic/templates07070100000045000081A400000000000000000000000160E313F500000488000000000000000000000000000000000000005500000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/elastic/templates/config.html<roundcube:include file="includes/layout.html" /> <roundcube:include file="includes/menu.html" /> <roundcube:include file="includes/settings-menu.html" /> <h1 class="voice"><roundcube:label name="kolab_2fa.settingstitle" /></h1> <div id="layout-content" class="selected no-navbar" role="main"> <h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2> <div class="header" role="toolbar" aria-labelledby="aria-label-toolbar"> <a class="button icon back-list-button" href="#back"><span class="inner"><roundcube:label name="back" /></span></a> <span class="header-title"><roundcube:label name="kolab_2fa.settingstitle" /></span> <div class="toolbar menu"></div> </div> <div class="scroller frame-content"> <p class="addfactor"> <roundcube:object name="plugin.factoradder" class="form-control" /> </p> <roundcube:object name="plugin.settingslist" class="authentication-factors" /> </div> </div> <roundcube:object name="plugin.settingsform" class="propform" /> <roundcube:object name="plugin.highsecuritydialog" class="kolab2fa-highsecuritydialog hidden" /> <roundcube:include file="includes/footer.html" /> 07070100000046000041ED00000000000000000000000360E313F500000000000000000000000000000000000000000000003D00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/larry07070100000047000081A400000000000000000000000160E313F500000564000000000000000000000000000000000000004A00000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/larry/kolab2fa.css .propform .formbuttons { margin-top: 1em; } .propform .explain { max-width: 40em; margin-top: 0.4em; margin-bottom: 0.8em; } .propform .kolab2fa-showqr { font-weight: bold; } .propform tr.hidden { display: none; } table.propform thead th { text-align: left; padding: 6px 10px; } table.propform td.title { vertical-align: top; padding-top: 0.8em; } table.propform td.title > label { line-height: 1.8em; } .propform img.qrcode { min-width: 260px; min-height: 260px; } table.authentication-factors { width: 99%; max-width: 60em; border: 0; } table.authentication-factors td.name { width: 60%; font-weight: bold; } table.authentication-factors td.name:before { content: "\2714"; display: inline-block; margin-right: 1em; /* width: 1.5em; line-height: 1.5em; text-align: center; color: #c7e3ef; background: #004458; border-radius: 50%; */ } table.authentication-factors td.created { width: 12em; white-space: nowrap; } table.authentication-factors td.actions { width: 8em; white-space: nowrap; text-align: right; } table.authentication-factors td.actions a.button { padding: 2px 8px; cursor: default; } .kolab2fa-highsecuritydialog { display: none; } .ui-dialog-content .propform > label { display: inline-block; padding-top: 0.3em; min-width: 12em; font-weight: bold; } .ui-dialog-content .propform .verifycode { width: 20em; }07070100000048000041ED00000000000000000000000260E313F500000000000000000000000000000000000000000000004700000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/larry/templates07070100000049000081A400000000000000000000000160E313F5000003C0000000000000000000000000000000000000005300000000roundcube-plugin-kolab-2fa-3.5.5.2+git23.797ffc6/skins/larry/templates/config.html<roundcube:object name="doctype" value="html5" /> <html> <head> <title><roundcube:object name="pagetitle" /></title> <roundcube:include file="/includes/links.html" /> </head> <body class="noscroll"> <roundcube:include file="/includes/header.html" /> <div id="mainscreen" class="offset"> <roundcube:include file="/includes/settingstabs.html" /> <div id="pluginbody" class="uibox contentbox scroller"> <h1 class="boxtitle"><roundcube:object name="steptitle" /></h1> <div class="boxcontent propform"> <p class="addfactor"> <roundcube:object name="plugin.factoradder" /> </p> <roundcube:object name="plugin.settingslist" class="propform authentication-factors" /> </div> </div> </div> <roundcube:object name="plugin.settingsform" class="propform" /> <roundcube:object name="plugin.highsecuritydialog" class="kolab2fa-highsecuritydialog" /> <roundcube:include file="/includes/footer.html" /> </body> </html> 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!287 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor