2015-06-04 15:53:04 +02:00
|
|
|
<?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;
|
|
|
|
|
2024-04-16 08:58:06 +02:00
|
|
|
/**
|
|
|
|
* Kolab 2-Factor-Authentication Driver base class
|
|
|
|
*/
|
2015-06-04 15:53:04 +02:00
|
|
|
abstract class Base
|
|
|
|
{
|
2016-01-28 17:12:23 +01:00
|
|
|
public $method;
|
|
|
|
public $id;
|
2015-06-04 15:53:04 +02:00
|
|
|
public $storage;
|
|
|
|
|
2024-01-24 11:24:41 +01:00
|
|
|
protected $config = [];
|
2024-09-06 12:39:58 +02:00
|
|
|
protected $config_keys = [];
|
2024-01-24 11:24:41 +01:00
|
|
|
protected $props = [];
|
|
|
|
protected $user_props = [];
|
2015-06-10 18:20:08 +02:00
|
|
|
protected $pending_changes = false;
|
2016-01-28 17:12:23 +01:00
|
|
|
protected $temporary = false;
|
2024-01-24 11:24:41 +01:00
|
|
|
protected $allowed_props = ['username'];
|
2015-06-04 15:53:04 +02:00
|
|
|
|
2024-01-24 11:24:41 +01:00
|
|
|
public $user_settings = [
|
|
|
|
'active' => [
|
2016-01-28 17:12:23 +01:00
|
|
|
'type' => 'boolean',
|
2015-06-10 18:20:08 +02:00
|
|
|
'editable' => false,
|
2016-01-28 17:12:23 +01:00
|
|
|
'hidden' => false,
|
|
|
|
'default' => false,
|
2024-01-24 11:24:41 +01:00
|
|
|
],
|
|
|
|
'label' => [
|
2016-01-28 17:12:23 +01:00
|
|
|
'type' => 'text',
|
|
|
|
'editable' => true,
|
|
|
|
'label' => 'label',
|
2015-06-10 18:20:08 +02:00
|
|
|
'generator' => 'default_label',
|
2024-01-24 11:24:41 +01:00
|
|
|
],
|
|
|
|
'created' => [
|
2016-01-28 17:12:23 +01:00
|
|
|
'type' => 'datetime',
|
|
|
|
'editable' => false,
|
|
|
|
'hidden' => false,
|
|
|
|
'label' => 'created',
|
2015-06-10 18:20:08 +02:00
|
|
|
'generator' => 'time',
|
2024-01-24 11:24:41 +01:00
|
|
|
],
|
|
|
|
];
|
2015-06-04 15:53:04 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Static factory method
|
|
|
|
*/
|
2024-09-06 12:39:58 +02:00
|
|
|
public static function factory($storage, $id, $config)
|
2015-06-04 15:53:04 +02:00
|
|
|
{
|
2024-01-24 11:24:41 +01:00
|
|
|
[$method] = explode(':', $id);
|
2015-06-10 18:20:08 +02:00
|
|
|
|
2024-01-24 11:24:41 +01:00
|
|
|
$classmap = [
|
2015-06-04 15:53:04 +02:00
|
|
|
'totp' => '\\Kolab2FA\\Driver\\TOTP',
|
|
|
|
'hotp' => '\\Kolab2FA\\Driver\\HOTP',
|
|
|
|
'yubikey' => '\\Kolab2FA\\Driver\\Yubikey',
|
2024-01-24 11:24:41 +01:00
|
|
|
];
|
2015-06-04 15:53:04 +02:00
|
|
|
|
|
|
|
$cls = $classmap[strtolower($method)];
|
|
|
|
if ($cls && class_exists($cls)) {
|
2024-09-06 12:39:58 +02:00
|
|
|
return new $cls($storage, $config, $id);
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
throw new Exception("Unknown 2FA driver '$method'");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default constructor
|
|
|
|
*/
|
2024-09-06 12:39:58 +02:00
|
|
|
public function __construct($storage, $config = null, $id = null)
|
2015-06-04 15:53:04 +02:00
|
|
|
{
|
2024-09-06 12:39:58 +02:00
|
|
|
if (!is_array($config)) {
|
|
|
|
$config = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->storage = $storage;
|
|
|
|
$this->props['username'] = (string) $storage->username;
|
2015-06-10 18:20:08 +02:00
|
|
|
|
|
|
|
if (!empty($id) && $id != $this->method) {
|
|
|
|
$this->id = $id;
|
2024-09-06 12:39:58 +02:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-24 11:24:41 +01:00
|
|
|
} else { // generate random ID
|
2015-06-10 18:20:08 +02:00
|
|
|
$this->id = $this->method . ':' . bin2hex(openssl_random_pseudo_bytes(12));
|
2015-06-11 16:38:47 +02:00
|
|
|
$this->temporary = true;
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
2024-09-06 12:39:58 +02:00
|
|
|
|
|
|
|
$this->init($config);
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the driver with the given config options
|
|
|
|
*/
|
2024-09-06 12:39:58 +02:00
|
|
|
protected function init($config)
|
2015-06-04 15:53:04 +02:00
|
|
|
{
|
2015-06-10 18:20:08 +02:00
|
|
|
if (is_array($config)) {
|
|
|
|
$this->config = array_merge($this->config, $config);
|
|
|
|
}
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Verify the submitted authentication code
|
|
|
|
*
|
2023-06-15 12:26:53 +02:00
|
|
|
* @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
|
2015-06-04 15:53:04 +02:00
|
|
|
*/
|
2024-01-24 11:24:41 +01:00
|
|
|
abstract public function verify($code, $timestamp = null);
|
2015-06-04 15:53:04 +02:00
|
|
|
|
2024-09-06 12:39:58 +02:00
|
|
|
/**
|
|
|
|
* Implement this method if the driver can be provisioned via QR code
|
|
|
|
*/
|
|
|
|
/* abstract function get_provisioning_uri(); */
|
|
|
|
|
2015-06-04 15:53:04 +02:00
|
|
|
/**
|
|
|
|
* Getter for user-visible properties
|
|
|
|
*/
|
|
|
|
public function props($force = false)
|
|
|
|
{
|
2024-01-24 11:24:41 +01:00
|
|
|
$data = [];
|
2015-06-04 15:53:04 +02:00
|
|
|
|
|
|
|
foreach ($this->user_settings as $key => $p) {
|
2023-06-14 15:05:00 +02:00
|
|
|
if (!empty($p['private'])) {
|
2015-06-04 15:53:04 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-01-24 11:24:41 +01:00
|
|
|
$data[$key] = [
|
2015-06-04 15:53:04 +02:00
|
|
|
'type' => $p['type'],
|
2023-06-14 15:10:51 +02:00
|
|
|
'editable' => $p['editable'] ?? false,
|
|
|
|
'hidden' => $p['hidden'] ?? false,
|
|
|
|
'label' => $p['label'] ?? '',
|
2015-06-04 15:53:04 +02:00
|
|
|
'value' => $this->get($key, $force),
|
2024-01-24 11:24:41 +01:00
|
|
|
];
|
2015-06-04 15:53:04 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2024-01-24 11:24:41 +01:00
|
|
|
// no break
|
2015-06-04 15:53:04 +02:00
|
|
|
default:
|
|
|
|
$data[$key]['text'] = $data[$key]['value'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate a random secret string
|
2025-02-25 12:53:26 +01:00
|
|
|
*
|
|
|
|
* A default of 32 characters results in 160bit security which is recommended by
|
|
|
|
* https://datatracker.ietf.org/doc/html/rfc6238
|
2015-06-04 15:53:04 +02:00
|
|
|
*/
|
2025-02-25 12:53:26 +01:00
|
|
|
public function generate_secret($length = 32)
|
2015-06-04 15:53:04 +02:00
|
|
|
{
|
|
|
|
// Base32 characters
|
2024-01-24 11:24:41 +01:00
|
|
|
$chars = [
|
2015-06-04 15:53:04 +02:00
|
|
|
'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
|
2024-01-24 11:24:41 +01:00
|
|
|
];
|
2015-06-04 15:53:04 +02:00
|
|
|
|
|
|
|
$secret = '';
|
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
|
|
$secret .= $chars[array_rand($chars)];
|
|
|
|
}
|
2023-06-15 12:26:53 +02:00
|
|
|
|
2015-06-04 15:53:04 +02:00
|
|
|
return $secret;
|
|
|
|
}
|
|
|
|
|
2015-06-10 18:20:08 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2015-06-04 15:53:04 +02:00
|
|
|
/**
|
|
|
|
* 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
|
2023-06-14 15:10:51 +02:00
|
|
|
if (!isset($value) && $force && !empty($this->user_settings[$key]['generator'])) {
|
2015-06-04 15:53:04 +02:00
|
|
|
$func = $this->user_settings[$key]['generator'];
|
|
|
|
if (is_string($func) && !is_callable($func)) {
|
2024-01-24 11:24:41 +01:00
|
|
|
$func = [$this, $func];
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
if (is_callable($func)) {
|
|
|
|
$value = call_user_func($func);
|
|
|
|
}
|
2015-06-10 18:20:08 +02:00
|
|
|
if (isset($value)) {
|
2015-06-04 15:53:04 +02:00
|
|
|
$this->set_user_prop($key, $value);
|
|
|
|
}
|
|
|
|
}
|
2024-01-24 11:24:41 +01:00
|
|
|
} else {
|
2024-09-06 12:39:58 +02:00
|
|
|
$value = $this->get_user_prop($key);
|
|
|
|
|
|
|
|
if ($value === null) {
|
|
|
|
$value = $this->props[$key] ?? null;
|
|
|
|
}
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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)) {
|
2024-01-24 11:24:41 +01:00
|
|
|
call_user_func([$this, $setter], $value);
|
|
|
|
} elseif (in_array($key, $this->allowed_props)) {
|
2015-06-04 15:53:04 +02:00
|
|
|
$this->props[$key] = $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2015-06-08 15:58:52 +02:00
|
|
|
|
2015-06-10 18:20:08 +02:00
|
|
|
/**
|
|
|
|
* Commit changes to storage
|
|
|
|
*/
|
|
|
|
public function commit()
|
|
|
|
{
|
|
|
|
if (!empty($this->user_props) && $this->storage && $this->pending_changes) {
|
2024-09-06 12:39:58 +02:00
|
|
|
$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)) {
|
2015-06-10 18:20:08 +02:00
|
|
|
$this->pending_changes = false;
|
2015-06-11 16:38:47 +02:00
|
|
|
$this->temporary = false;
|
2015-06-10 18:20:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return !$this->pending_changes;
|
|
|
|
}
|
|
|
|
|
2015-06-04 15:53:04 +02:00
|
|
|
/**
|
|
|
|
* Clear data stored for this driver
|
|
|
|
*/
|
|
|
|
public function clear()
|
|
|
|
{
|
|
|
|
if ($this->storage) {
|
2015-06-10 18:20:08 +02:00
|
|
|
return $this->storage->remove($this->id);
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
2015-06-10 18:20:08 +02:00
|
|
|
|
|
|
|
return false;
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
2023-06-15 12:26:53 +02:00
|
|
|
/**
|
|
|
|
* Checks that a string contains a semicolon
|
|
|
|
*/
|
|
|
|
protected function hasSemicolon($value)
|
|
|
|
{
|
|
|
|
return preg_match('/(:|%3A)/i', (string) $value) > 0;
|
|
|
|
}
|
|
|
|
|
2015-06-04 15:53:04 +02:00
|
|
|
/**
|
|
|
|
* Getter for per-user properties for this method
|
|
|
|
*/
|
|
|
|
protected function get_user_prop($key)
|
|
|
|
{
|
2015-06-11 16:38:47 +02:00
|
|
|
if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes && !$this->temporary) {
|
2015-06-10 18:20:08 +02:00
|
|
|
$this->user_props = (array)$this->storage->read($this->id);
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
2023-06-14 15:10:51 +02:00
|
|
|
return $this->user_props[$key] ?? null;
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Setter for per-user properties for this method
|
|
|
|
*/
|
|
|
|
protected function set_user_prop($key, $value)
|
|
|
|
{
|
2023-06-14 15:10:51 +02:00
|
|
|
$this->pending_changes |= (($this->user_props[$key] ?? null) !== $value);
|
2015-06-04 15:53:04 +02:00
|
|
|
$this->user_props[$key] = $value;
|
2015-06-11 16:38:47 +02:00
|
|
|
return true;
|
2015-06-04 15:53:04 +02:00
|
|
|
}
|
2016-01-28 17:12:23 +01:00
|
|
|
}
|