diff --git a/src/app/Auth/SecondFactor.php b/src/app/Auth/SecondFactor.php index ed83377a..e67cc71b 100644 --- a/src/app/Auth/SecondFactor.php +++ b/src/app/Auth/SecondFactor.php @@ -1,322 +1,322 @@ [], ]; /** * Class constructor * * @param \App\User $user User object */ public function __construct($user) { $this->user = $user; + $this->username = $this->user->email; parent::__construct(); } /** * Validate 2-factor authentication code * * @param string $secondfactor The 2-factor authentication code. * * @throws \Exception on validation failure */ public function validate($secondfactor): void { // get list of configured authentication factors $factors = $this->factors(); // do nothing if no factors configured if (empty($factors)) { return; } if (empty($secondfactor) || !is_string($secondfactor)) { throw new \Exception(\trans('validation.2fareq')); } // try to verify each configured factor foreach ($factors as $factor) { // verify the submitted code if ($this->verify($factor, $secondfactor)) { return; } } throw new \Exception(\trans('validation.2fainvalid')); } /** * Validate 2-factor authentication code * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\JsonResponse|null */ public function requestHandler(\Illuminate\Http\Request $request) { try { $this->validate($request->secondfactor); } catch (\Exception $e) { $errors = ['secondfactor' => $e->getMessage()]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } return null; } /** * Remove all configured 2FA methods for the current user * * @return bool True on success, False otherwise */ public function removeFactors(): bool { $this->cache = []; $prefs = []; $prefs[$this->key2property('blob')] = null; $prefs[$this->key2property('factors')] = null; return $this->savePrefs($prefs); } /** * Returns a list of 2nd factor methods configured for the user */ public function factors(): array { // First check if the user has the 2FA SKU if ($this->user->hasSku('2fa')) { $factors = (array) $this->enumerate(); $factors = array_unique($factors); return $factors; } return []; } /** * Helper method to verify the given method/code tuple * * @param string $factor Factor identifier (:) * @param string $code Authentication code * * @return bool True on successful validation */ protected function verify($factor, $code): bool { $driver = $this->getDriver($factor); return $driver->verify($code, time()); } /** * Load driver class for the given authentication factor * * @param string $factor Factor identifier (:) * - * @return \Kolab2FA\Driver\Base + * @return DriverBase */ protected function getDriver(string $factor) { list($method) = explode(':', $factor, 2); $config = \config('2fa.' . $method, []); - $driver = \Kolab2FA\Driver\Base::factory($factor, $config); - - // configure driver - $driver->storage = $this; - $driver->username = $this->user->email; - - return $driver; + return DriverBase::factory($this, $factor, $config); } /** * Helper for seeding a Roundcube account with 2FA setup * for testing. * * @param string $email Email address */ public static function seed(string $email): void { $config = [ 'kolab_2fa_blob' => [ 'totp:8132a46b1f741f88de25f47e' => [ 'label' => 'Mobile app (TOTP)', 'created' => 1584573552, 'secret' => 'UAF477LDHZNWVLNA', + 'digest' => 'sha1', 'active' => true, ], // 'dummy:dummy' => [ // 'active' => true, // ], ], 'kolab_2fa_factors' => [ 'totp:8132a46b1f741f88de25f47e', // 'dummy:dummy', ] ]; self::dbh()->table('users')->updateOrInsert( ['username' => $email, 'mail_host' => '127.0.0.1'], ['preferences' => serialize($config)] ); } /** * Helper for generating current TOTP code for a test user * * @param string $email Email address * * @return string Generated code */ public static function code(string $email): string { $sf = new self(\App\User::where('email', $email)->first()); + + /** @var \Kolab2FA\Driver\TOTP $driver */ $driver = $sf->getDriver('totp:8132a46b1f741f88de25f47e'); return (string) $driver->get_code(); } //****************************************************** // Methods required by Kolab2FA Storage Base //****************************************************** /** * Initialize the storage driver with the given config options */ public function init(array $config) { $this->config = array_merge($this->config, $config); } /** * List methods activated for this user */ public function enumerate() { if ($factors = $this->getFactors()) { return array_keys(array_filter($factors, function ($prop) { return !empty($prop['active']); })); } return []; } /** * Read data for the given key */ public function read($key) { if (!isset($this->cache[$key])) { $factors = $this->getFactors(); $this->cache[$key] = isset($factors[$key]) ? $factors[$key] : null; } return $this->cache[$key]; } /** * Save data for the given key */ public function write($key, $value) { \Log::debug(__METHOD__ . ' ' . @json_encode($value)); // TODO: Not implemented return false; } /** * Remove the data stored for the given key */ public function remove($key) { return $this->write($key, null); } /** * */ protected function getFactors(): array { $prefs = $this->getPrefs(); $key = $this->key2property('blob'); return isset($prefs[$key]) ? (array) $prefs[$key] : []; } /** * */ protected 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; } /** * Gets user preferences from Roundcube users table */ protected function getPrefs() { $user = $this->dbh()->table('users') ->select('preferences') ->where('username', strtolower($this->user->email)) ->first(); return $user ? (array) unserialize($user->preferences) : null; } /** * Saves user preferences in Roundcube users table. * This will merge into old preferences */ protected function savePrefs($prefs) { $old_prefs = $this->getPrefs(); if (!is_array($old_prefs)) { return false; } $prefs = array_merge($old_prefs, $prefs); + $prefs = array_filter($prefs, fn($v) => !is_null($v)); $this->dbh()->table('users') ->where('username', strtolower($this->user->email)) ->update(['preferences' => serialize($prefs)]); return true; } /** * Init connection to the Roundcube database */ public static function dbh() { return \App\Backends\Roundcube::dbh(); } } diff --git a/src/include/Kolab2FA/Driver/Base.php b/src/include/Kolab2FA/Driver/Base.php index 7673f068..273dc170 100644 --- a/src/include/Kolab2FA/Driver/Base.php +++ b/src/include/Kolab2FA/Driver/Base.php @@ -1,354 +1,338 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Driver; +/** + * Kolab 2-Factor-Authentication Driver base class + */ abstract class Base { public $method; public $id; public $storage; - public $username; - protected $config = array(); - protected $props = array(); - protected $user_props = array(); + protected $config = []; + protected $config_keys = []; + protected $props = []; + protected $user_props = []; protected $pending_changes = false; protected $temporary = false; - protected $allowed_props = array('username'); + protected $allowed_props = ['username']; - public $user_settings = array( - 'active' => array( + public $user_settings = [ + 'active' => [ 'type' => 'boolean', 'editable' => false, 'hidden' => false, 'default' => false, - ), - 'label' => array( + ], + 'label' => [ 'type' => 'text', 'editable' => true, 'label' => 'label', 'generator' => 'default_label', - ), - 'created' => array( + ], + 'created' => [ 'type' => 'datetime', 'editable' => false, 'hidden' => false, 'label' => 'created', 'generator' => 'time', - ), - ); + ], + ]; /** * Static factory method */ - public static function factory($id, $config) + public static function factory($storage, $id, $config) { - list($method) = explode(':', $id); + [$method] = explode(':', $id); - $classmap = array( + $classmap = [ '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); + return new $cls($storage, $config, $id); } throw new Exception("Unknown 2FA driver '$method'"); } /** * Default constructor */ - public function __construct($config = null, $id = null) + public function __construct($storage, $config = null, $id = null) { - $this->init($config); + if (!is_array($config)) { + $config = []; + } + + $this->storage = $storage; + $this->props['username'] = (string) $storage->username; if (!empty($id) && $id != $this->method) { $this->id = $id; - } - else { // generate random ID + if ($this->storage) { + $this->user_props = (array) $this->storage->read($this->id); + foreach ($this->config_keys as $key) { + if (isset($this->user_props[$key])) { + $config[$key] = $this->user_props[$key]; + } + } + } + } else { // generate random ID $this->id = $this->method . ':' . bin2hex(openssl_random_pseudo_bytes(12)); $this->temporary = true; } + + $this->init($config); } /** * Initialize the driver with the given config options */ - public function init($config) + protected function init($config) { if (is_array($config)) { $this->config = array_merge($this->config, $config); } - - if (!empty($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 + * @param string $code The 2nd authentication factor to verify + * @param int $timestamp Timestamp of authentication process (window start) + * + * @return bool True if valid, false otherwise */ - abstract function verify($code, $timestamp = null); + abstract public function verify($code, $timestamp = null); + + /** + * Implement this method if the driver can be provisioned via QR code + */ + /* abstract function get_provisioning_uri(); */ /** * Getter for user-visible properties */ public function props($force = false) { - $data = array(); + $data = []; foreach ($this->user_settings as $key => $p) { if (!empty($p['private'])) { continue; } - $data[$key] = array( + $data[$key] = [ 'type' => $p['type'], - 'editable' => $p['editable'], - 'hidden' => $p['hidden'], - 'label' => $p['label'], + 'editable' => $p['editable'] ?? false, + 'hidden' => $p['hidden'] ?? false, + '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; } + // no 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( + $chars = [ '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); } - /** - * Get current code (for testing) - */ - public function get_code() - { - // to be overriden by a driver - return ''; - } - /** * 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']) { + if (!isset($value) && $force && !empty($this->user_settings[$key]['generator'])) { $func = $this->user_settings[$key]['generator']; if (is_string($func) && !is_callable($func)) { - $func = array($this, $func); + $func = [$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]; + } else { + $value = $this->get_user_prop($key); + + if ($value === null) { + $value = $this->props[$key] ?? null; + } } 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)) { + call_user_func([$this, $setter], $value); + } elseif (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)) { + $props = $this->user_props; + + // Remamber the driver config too. It will be used to verify the code. + // The configured one may be different than the one used on code creation. + foreach ($this->config_keys as $key) { + if (isset($this->config[$key])) { + $props[$key] = $this->config[$key]; + } + } + + if ($this->storage->write($this->id, $props)) { $this->pending_changes = false; $this->temporary = false; } } return !$this->pending_changes; } /** - * Dedicated setter for the username property + * Clear data stored for this driver */ - public function set_username($username) + public function clear() { - $this->props['username'] = $username; - if ($this->storage) { - $this->storage->set_username($username); + return $this->storage->remove($this->id); } - return true; + return false; } /** - * Clear data stored for this driver + * Checks that a string contains a semicolon */ - public function clear() + protected function hasSemicolon($value) { - if ($this->storage) { - return $this->storage->remove($this->id); - } - - return false; + return preg_match('/(:|%3A)/i', (string) $value) > 0; } /** * 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] ?? null; } /** * Setter for per-user properties for this method */ protected function set_user_prop($key, $value) { - $this->pending_changes |= ($this->user_props[$key] !== $value); + $this->pending_changes |= (($this->user_props[$key] ?? null) !== $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]); - } } diff --git a/src/include/Kolab2FA/Driver/Exception.php b/src/include/Kolab2FA/Driver/Exception.php index 627cb447..d555e61c 100644 --- a/src/include/Kolab2FA/Driver/Exception.php +++ b/src/include/Kolab2FA/Driver/Exception.php @@ -1,8 +1,7 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Driver; class HOTP extends Base { public $method = 'hotp'; - protected $config = array( + protected $config = [ 'digits' => 6, 'window' => 4, 'digest' => 'sha1', - ); + ]; + protected $config_keys = ['digits', 'digest']; protected $backend; /** * */ public function init($config) { parent::init($config); - $this->user_settings += array( - 'secret' => array( + $this->user_settings += [ + 'secret' => [ 'type' => 'text', 'private' => true, 'label' => 'secret', 'generator' => 'generate_secret', - ), - 'counter' => array( + ], + 'counter' => [ 'type' => 'integer', 'editable' => false, 'hidden' => true, 'generator' => 'random_counter', - ), - ); + ], + ]; + + if (!in_array($this->config['digest'], ['md5', 'sha1', 'sha256', 'sha512'])) { + throw new \Exception("'{$this->config['digest']}' digest is not supported."); + } + + if (!is_numeric($this->config['digits']) || $this->config['digits'] < 1) { + throw new \Exception('Digits must be at least 1.'); + } + + if ($this->hasSemicolon($this->config['issuer'])) { + throw new \Exception('Issuer must not contain a semi-colon.'); + } // copy config options $this->backend = \OTPHP\HOTP::create( - null, - 0, - $this->config['digest'], - $this->config['digits'] + null, // secret + 0, // counter + $this->config['digest'], // digest + $this->config['digits'] // digits ); $this->backend->setIssuer($this->config['issuer']); $this->backend->setIssuerIncludedAsParameter(true); } /** * */ public function verify($code, $timestamp = null) { // get my secret from the user storage $secret = $this->get('secret'); - $counter = (int) $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); + $this->backend->setLabel($this->get('username')); $this->backend->setSecret($secret); - $this->backend->setParameter('counter', $counter); - $pass = $this->backend->verify($code, $counter, $this->config['window']); + $pass = $this->backend->verify($code, $this->get('counter'), (int) $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)); + } catch (\Exception $e) { $pass = false; } - // rcube::console('VERIFY HOTP', $this->username, $secret, $counter, $code, $pass); return $pass; } /** - * + * Get the provisioning URI. */ public function get_provisioning_uri() { - if (!$this->secret) { + if (!$this->get('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); - $this->backend->setSecret($this->secret); - $this->backend->setParameter('counter', (int) $this->get('counter')); + $this->backend->setLabel($this->get('username')); + $this->backend->setSecret($this->get('secret')); + $this->backend->setCounter(intval($this->get('counter'))); return $this->backend->getProvisioningUri(); } /** * Generate a random counter value */ public function random_counter() { return mt_rand(1, 999); } } diff --git a/src/include/Kolab2FA/Driver/TOTP.php b/src/include/Kolab2FA/Driver/TOTP.php index 272d5553..8a90f5b0 100644 --- a/src/include/Kolab2FA/Driver/TOTP.php +++ b/src/include/Kolab2FA/Driver/TOTP.php @@ -1,138 +1,151 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Driver; class TOTP extends Base { public $method = 'totp'; - protected $config = array( + protected $config = [ 'digits' => 6, 'interval' => 30, 'digest' => 'sha1', - ); + ]; + protected $config_keys = ['digits', 'digest']; protected $backend; /** * */ public function init($config) { parent::init($config); - $this->user_settings += array( - 'secret' => array( + $this->user_settings += [ + 'secret' => [ 'type' => 'text', 'private' => true, 'label' => 'secret', 'generator' => 'generate_secret', - ), - ); + ], + ]; + + if (!in_array($this->config['digest'], ['md5', 'sha1', 'sha256', 'sha512'])) { + throw new \Exception("'{$this->config['digest']}' digest is not supported."); + } + + if (!is_numeric($this->config['digits']) || $this->config['digits'] < 1) { + throw new \Exception('Digits must be at least 1.'); + } + + if (!is_numeric($this->config['interval']) || $this->config['interval'] < 1) { + throw new \Exception('Interval must be at least 1.'); + } + + if ($this->hasSemicolon($this->config['issuer'])) { + throw new \Exception('Issuer must not contain a semi-colon.'); + } // copy config options $this->backend = \OTPHP\TOTP::create( - null, - $this->config['interval'], - $this->config['digest'], - $this->config['digits'] + null, //secret + $this->config['interval'], // period + $this->config['digest'], // digest + $this->config['digits'] // digits ); $this->backend->setIssuer($this->config['issuer']); $this->backend->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); - $this->backend->setParameter('secret', $secret); + $this->backend->setLabel($this->get('username')); + $this->backend->setSecret($secret); - // Pass a window to indicate the maximum timeslip between client (device) and server. - $pass = $this->backend->verify((string) $code, $timestamp, 150); + // Pass a window to indicate the maximum timeslip between client (mobile + // device) and server. + $pass = $this->backend->verify($code, (int) $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; } /** - * Get current code (for testing) + * Get the provisioning URI. */ - public function get_code() + public function get_provisioning_uri() { - // get my secret from the user storage - $secret = $this->get('secret'); - - if (!strlen($secret)) { - return; + if (!$this->get('secret')) { + // generate new secret and store it + $this->set('secret', $this->get('secret', true)); + $this->set('created', $this->get('created', true)); + $this->commit(); } - $this->backend->setLabel($this->username); - $this->backend->setParameter('secret', $secret); + // TODO: deny call if already active? - return $this->backend->at(time()); + $this->backend->setLabel($this->get('username')); + $this->backend->setSecret($this->get('secret')); + + return $this->backend->getProvisioningUri(); } /** - * + * Get current code (for testing) */ - public function get_provisioning_uri() + public function get_code() { - // 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(); - } + // get my secret from the user storage + $secret = $this->get('secret'); - // TODO: deny call if already active? + if (!strlen($secret)) { + return; + } - $this->backend->setLabel($this->username); + $this->backend->setLabel($this->get('username')); $this->backend->setParameter('secret', $secret); - return $this->backend->getProvisioningUri(); + return $this->backend->at(time()); } } diff --git a/src/include/Kolab2FA/Driver/Yubikey.php b/src/include/Kolab2FA/Driver/Yubikey.php index dd10bb33..a2c3501b 100644 --- a/src/include/Kolab2FA/Driver/Yubikey.php +++ b/src/include/Kolab2FA/Driver/Yubikey.php @@ -1,126 +1,126 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Driver; class Yubikey extends Base { public $method = 'yubikey'; protected $backend; /** * */ public function init($config) { parent::init($config); - $this->user_settings += array( - 'yubikeyid' => array( + $this->user_settings += [ + 'yubikeyid' => [ '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) { + } catch (\Exception $e) { // TODO: log exception } } - // rcube::console('VERIFY Yubikey', $this->username, $keyid, $code, $pass); return $pass; } /** * @override */ - public function set($key, $value) + 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) { + } catch (\Exception $e) { return false; } // truncate the submitted yubikey code to 12 characters $value = substr($value, 0, 12); } + // invalid or no yubikey token provided + elseif ($key == 'yubikeyid') { + return false; + } - return parent::set($key, $value); + 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); } }