diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php index b6a7080a..ec90239f 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php @@ -1,346 +1,344 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab2FA\Driver; abstract class Base { - public $method = null; - public $id = null; + public $method; + public $id; public $storage; - protected $config = array(); - protected $props = array(); - protected $user_props = array(); + protected $config = array(); + protected $props = array(); + protected $user_props = array(); protected $pending_changes = false; - protected $temporary = false; - - protected $allowed_props = array('username'); + protected $temporary = false; + protected $allowed_props = array('username'); public $user_settings = array( 'active' => array( - 'type' => 'boolean', + 'type' => 'boolean', 'editable' => false, - 'hidden' => false, - 'default' => false, + 'hidden' => false, + 'default' => false, ), 'label' => array( - 'type' => 'text', - 'editable' => true, - 'label' => 'label', + 'type' => 'text', + 'editable' => true, + 'label' => 'label', 'generator' => 'default_label', ), 'created' => array( - 'type' => 'datetime', - 'editable' => false, - 'hidden' => false, - 'label' => 'created', + '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]); } - -} \ No newline at end of file +} diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php index b51e0052..8093c11c 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php @@ -1,129 +1,128 @@ * * 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( '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', + 'type' => 'text', + 'private' => true, + 'label' => 'secret', 'generator' => 'generate_secret', ), 'counter' => array( - 'type' => 'integer', - 'editable' => false, - 'hidden' => true, + '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'); + $secret = $this->get('secret'); $counter = $this->get('counter'); if (!strlen($secret)) { // LOG: "no secret set for user $this->username" - console("VERIFY HOTP: 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 - console("VERIFY HOTP: $this->id, " . strval($e)); + // rcube::console("VERIFY HOTP: $this->id, " . strval($e)); $pass = false; } - // console('VERIFY HOTP', $this->username, $secret, $counter, $code, $pass); + // 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); } - } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php index 7caa4c6b..0efd071e 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/TOTP.php @@ -1,120 +1,120 @@ * * 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( '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', + '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" - console("VERIFY TOTP: 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); // PHP gets a string, but we're comparing integers. $code = (int)$code; // 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']; } } - // console('VERIFY TOTP', $this->username, $secret, $code, $timestamp, $pass); + // rcube::console('VERIFY TOTP', $this->username, $secret, $code, $timestamp, $pass); return $pass; } /** * */ public function get_provisioning_uri() { - console('PROV', $this->secret); + // 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)); - console('PROV2', $this->secret); + // 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(); } } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php index 2f227da9..638e65b3 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Yubikey.php @@ -1,128 +1,128 @@ * * 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 $config = array( 'clientid' => '42', 'apikey' => 'FOOBAR=', 'hosts' => null, ); protected $backend; /** * */ public function init(array $config) { parent::init($config); $this->user_settings += array( 'yubikeyid' => array( - 'type' => 'text', + 'type' => 'text', 'editable' => true, - 'label' => 'secret', + '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']); } } /** * */ public function verify($code, $timestamp = null) { // get my secret from the user storage $keyid = $this->get('yubikeyid'); - $pass = false; + $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; + $pass = $response->success() === true; } catch (\Exception $e) { // TODO: log exception } } - console('VERIFY TOTP', $this->username, $keyid, $code, $pass); + // rcube::console('VERIFY TOTP', $this->username, $keyid, $code, $pass); return $pass; } /** * @override */ public function set($key, $value) { 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); } /** * @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); } }