roundcubemail-plugins-kolab/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php

342 lines
9.7 KiB
PHP
Raw Normal View History

<?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
*/
abstract class Base
{
2016-01-28 17:12:23 +01:00
public $method;
public $id;
public $storage;
2024-01-24 11:24:41 +01:00
protected $config = [];
protected $config_keys = [];
2024-01-24 11:24:41 +01:00
protected $props = [];
protected $user_props = [];
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'];
2024-01-24 11:24:41 +01:00
public $user_settings = [
'active' => [
2016-01-28 17:12:23 +01:00
'type' => 'boolean',
'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',
'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',
'generator' => 'time',
2024-01-24 11:24:41 +01:00
],
];
/**
* Static factory method
*/
public static function factory($storage, $id, $config)
{
2024-01-24 11:24:41 +01:00
[$method] = explode(':', $id);
2024-01-24 11:24:41 +01:00
$classmap = [
'totp' => '\\Kolab2FA\\Driver\\TOTP',
'hotp' => '\\Kolab2FA\\Driver\\HOTP',
'yubikey' => '\\Kolab2FA\\Driver\\Yubikey',
2024-01-24 11:24:41 +01:00
];
$cls = $classmap[strtolower($method)];
if ($cls && class_exists($cls)) {
return new $cls($storage, $config, $id);
}
throw new Exception("Unknown 2FA driver '$method'");
}
/**
* Default constructor
*/
public function __construct($storage, $config = null, $id = null)
{
if (!is_array($config)) {
$config = [];
}
$this->storage = $storage;
$this->props['username'] = (string) $storage->username;
if (!empty($id) && $id != $this->method) {
$this->id = $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];
}
}
}
2024-01-24 11:24:41 +01:00
} 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
*/
protected function init($config)
{
if (is_array($config)) {
$this->config = array_merge($this->config, $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 bool True if valid, false otherwise
*/
2024-01-24 11:24:41 +01:00
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)
{
2024-01-24 11:24:41 +01:00
$data = [];
foreach ($this->user_settings as $key => $p) {
2023-06-14 15:05:00 +02:00
if (!empty($p['private'])) {
continue;
}
2024-01-24 11:24:41 +01:00
$data[$key] = [
'type' => $p['type'],
2023-06-14 15:10:51 +02:00
'editable' => $p['editable'] ?? false,
'hidden' => $p['hidden'] ?? false,
'label' => $p['label'] ?? '',
'value' => $this->get($key, $force),
2024-01-24 11:24:41 +01: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
default:
$data[$key]['text'] = $data[$key]['value'];
}
}
return $data;
}
/**
* Generate a random secret string
*
* A default of 32 characters results in 160bit security which is recommended by
* https://datatracker.ietf.org/doc/html/rfc6238
*/
public function generate_secret($length = 32)
{
// Base32 characters
2024-01-24 11:24:41 +01:00
$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
2024-01-24 11:24:41 +01:00
];
$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
2023-06-14 15:10:51 +02:00
if (!isset($value) && $force && !empty($this->user_settings[$key]['generator'])) {
$func = $this->user_settings[$key]['generator'];
if (is_string($func) && !is_callable($func)) {
2024-01-24 11:24:41 +01:00
$func = [$this, $func];
}
if (is_callable($func)) {
$value = call_user_func($func);
}
if (isset($value)) {
$this->set_user_prop($key, $value);
}
}
2024-01-24 11:24:41 +01:00
} 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)) {
2024-01-24 11:24:41 +01:00
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) {
$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;
}
/**
* Clear data stored for this driver
*/
public function clear()
{
if ($this->storage) {
return $this->storage->remove($this->id);
}
return false;
}
/**
* Checks that a string contains a semicolon
*/
protected function hasSemicolon($value)
{
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);
}
2023-06-14 15:10:51 +02:00
return $this->user_props[$key] ?? null;
}
/**
* 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);
$this->user_props[$key] = $value;
return true;
}
2016-01-28 17:12:23 +01:00
}