Merge branch 'MDL-79678_upgrade-otphp' of https://github.com/ziegenberg/moodle

This commit is contained in:
Jun Pataleta
2024-08-21 16:52:33 +08:00
21 changed files with 593 additions and 3506 deletions
+53 -38
View File
@@ -25,16 +25,16 @@ require_once(__DIR__.'/../extlib/OTPHP/ParameterTrait.php');
require_once(__DIR__.'/../extlib/OTPHP/OTP.php');
require_once(__DIR__.'/../extlib/OTPHP/TOTP.php');
require_once(__DIR__.'/../extlib/Assert/Assertion.php');
require_once(__DIR__.'/../extlib/Assert/AssertionFailedException.php');
require_once(__DIR__.'/../extlib/Assert/InvalidArgumentException.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/EncoderInterface.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Binary.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Base32.php');
use MoodleQuickForm;
use tool_mfa\local\factor\object_factor_base;
use OTPHP\TOTP;
use stdClass;
use core\clock;
use core\di;
/**
* TOTP factor class.
@@ -65,6 +65,19 @@ class factor extends object_factor_base {
/** @var string Factor icon */
protected $icon = 'fa-mobile-screen';
/** @var clock */
private readonly clock $clock;
/**
* Constructor.
*
* @param string $name
*/
public function __construct(string $name) {
parent::__construct($name);
$this->clock = di::get(clock::class);
}
/**
* Generates TOTP URI for given secret key.
@@ -79,7 +92,7 @@ class factor extends object_factor_base {
$host = parse_url($CFG->wwwroot, PHP_URL_HOST);
$sitename = str_replace(':', '', $SITE->fullname);
$issuer = $sitename.' '.$host;
$totp = TOTP::create($secret);
$totp = TOTP::create($secret, clock: $this->clock);
$totp->setLabel($USER->username);
$totp->setIssuer($issuer);
return $totp->getProvisioningUri();
@@ -119,10 +132,10 @@ class factor extends object_factor_base {
/**
* TOTP Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @param MoodleQuickForm $mform
* @return MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
public function setup_factor_form_definition(MoodleQuickForm $mform): MoodleQuickForm {
$secret = $this->generate_secret_code();
$mform->addElement('hidden', 'secret', $secret);
$mform->setType('secret', PARAM_ALPHANUM);
@@ -133,10 +146,10 @@ class factor extends object_factor_base {
/**
* TOTP Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @param MoodleQuickForm $mform
* @return MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
public function setup_factor_form_definition_after_data(MoodleQuickForm $mform): MoodleQuickForm {
global $OUTPUT, $SITE, $USER;
// Array of elements to allow XSS.
@@ -234,8 +247,8 @@ class factor extends object_factor_base {
public function setup_factor_form_validation(array $data): array {
$errors = [];
$totp = TOTP::create($data['secret']);
if (!$totp->verify($data['verificationcode'], time(), 1)) {
$totp = TOTP::create($data['secret'], clock: $this->clock);
if (!$totp->verify($data['verificationcode'], $this->clock->time(), 1)) {
$errors['verificationcode'] = get_string('error:wrongverification', 'factor_totp');
}
@@ -245,10 +258,10 @@ class factor extends object_factor_base {
/**
* TOTP Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @param MoodleQuickForm $mform
* @return MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
public function login_form_definition(MoodleQuickForm $mform): MoodleQuickForm {
$mform->disable_form_change_checker();
$mform->addElement(new \tool_mfa\local\form\verification_field());
@@ -267,14 +280,12 @@ class factor extends object_factor_base {
global $USER;
$factors = $this->get_active_user_factors($USER);
$result = ['verificationcode' => get_string('error:wrongverification', 'factor_totp')];
$windowconfig = get_config('factor_totp', 'window');
$window = get_config('factor_totp', 'window');
foreach ($factors as $factor) {
$totp = TOTP::create($factor->secret);
// Convert seconds to windows.
$window = (int) floor($windowconfig / $totp->getPeriod());
$totp = TOTP::create($factor->secret, clock: $this->clock);
$factorresult = $this->validate_code($data['verificationcode'], $window, $totp, $factor);
$time = userdate(time(), get_string('systimeformat', 'factor_totp'));
$time = userdate($this->clock->time(), get_string('systimeformat', 'factor_totp'));
switch ($factorresult) {
case self::TOTP_USED:
@@ -314,23 +325,27 @@ class factor extends object_factor_base {
return self::TOTP_USED;
}
// The window in which to check for clock skew, 5 increments past valid window.
$skewwindow = $window + 5;
$pasttimestamp = time() - ($skewwindow * $totp->getPeriod());
$futuretimestamp = time() + ($skewwindow * $totp->getPeriod());
if ($totp->verify($code, time(), $window)) {
// Check if the code is valid, returning early.
if ($totp->verify($code, $this->clock->time(), $window)) {
return self::TOTP_VALID;
} else if ($totp->verify($code, $pasttimestamp, $skewwindow)) {
// Check for clock skew in the past 10 periods.
return self::TOTP_OLD;
} else if ($totp->verify($code, $futuretimestamp, $skewwindow)) {
// Check for clock skew in the future 10 periods.
return self::TOTP_FUTURE;
} else {
// In all other cases, code is invalid.
return self::TOTP_INVALID;
}
// Check for clock skew in the past and future 10 periods.
for ($i = 1; $i <= 10; $i++) {
$pasttimestamp = $this->clock->time() - $i * $totp->getPeriod();
$futuretimestamp = $this->clock->time() + $i * $totp->getPeriod();
if ($totp->verify($code, $pasttimestamp, $window)) {
return self::TOTP_OLD;
}
if ($totp->verify($code, $futuretimestamp, $window)) {
return self::TOTP_FUTURE;
}
}
// In all other cases, the code is invalid.
return self::TOTP_INVALID;
}
/**
@@ -339,7 +354,7 @@ class factor extends object_factor_base {
* @return string
*/
public function generate_secret_code(): string {
$totp = TOTP::create();
$totp = TOTP::create(clock: $this->clock);
return substr($totp->getSecret(), 0, 16);
}
@@ -358,9 +373,9 @@ class factor extends object_factor_base {
$row->factor = $this->name;
$row->secret = $data->secret;
$row->label = $data->devicename;
$row->timecreated = time();
$row->timecreated = $this->clock->time();
$row->createdfromip = $USER->lastip;
$row->timemodified = time();
$row->timemodified = $this->clock->time();
$row->lastverified = 0;
$row->revoked = 0;
+43
View File
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* factor_totp upgrade library.
*
* @package factor_totp
* @copyright 2024 Daniel Ziegenberg <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Factor totp upgrade helper function
*
* @param int $oldversion
*/
function xmldb_factor_totp_upgrade($oldversion): bool {
if ($oldversion < 2024081600) {
$window = get_config('factor_totp', 'window');
if ($window && $window >= 30) {
set_config('window', 29, 'factor_totp');
}
// Savepoint reached.
upgrade_plugin_savepoint(true, 2024081600, 'factor', 'auth');
}
return true;
}
File diff suppressed because it is too large Load Diff
@@ -1,35 +0,0 @@
<?php
/**
* Assert
*
* LICENSE
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so I can send you a copy immediately.
*/
namespace Assert;
use Throwable;
interface AssertionFailedException extends Throwable
{
/**
* @return string|null
*/
public function getPropertyPath();
/**
* @return mixed
*/
public function getValue();
/**
* @return array
*/
public function getConstraints(): array;
}
@@ -1,76 +0,0 @@
<?php
/**
* Assert
*
* LICENSE
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so I can send you a copy immediately.
*/
namespace Assert;
class InvalidArgumentException extends \InvalidArgumentException implements AssertionFailedException
{
/**
* @var string|null
*/
private $propertyPath;
/**
* @var mixed
*/
private $value;
/**
* @var array
*/
private $constraints;
public function __construct($message, $code, string $propertyPath = null, $value = null, array $constraints = [])
{
parent::__construct($message, $code);
$this->propertyPath = $propertyPath;
$this->value = $value;
$this->constraints = $constraints;
}
/**
* User controlled way to define a sub-property causing
* the failure of a currently asserted objects.
*
* Useful to transport information about the nature of the error
* back to higher layers.
*
* @return string|null
*/
public function getPropertyPath()
{
return $this->propertyPath;
}
/**
* Get the value that caused the assertion to fail.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Get the constraints that applied to the failed assertion.
*
* @return array
*/
public function getConstraints(): array
{
return $this->constraints;
}
}
@@ -1,11 +0,0 @@
Copyright (c) 2011-2013, Benjamin Eberlei
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
@@ -1,23 +0,0 @@
{
"name": "beberlei/assert",
"description": "Thin assertion library for input validation in business models.",
"authors": [
{"name": "Benjamin Eberlei", "email": "[email protected]"}
],
"license": "BSD-2-Clause",
"keywords": ["assert", "assertion", "validation"],
"require": {
"ext-mbstring": "*"
},
"autoload": {
"psr-0": {
"Assert": "lib/"
},
"files": ["lib/Assert/functions.php"]
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
}
}
@@ -1,17 +0,0 @@
Assert 2.1
--------------
https://github.com/beberlei/assert/releases/tag/v2.1
Instructions to import WebAuthn into Moodle:
1. Download the latest release from https://github.com/beberlei/assert/releases/tag/vx.x
(choose "Source code")
2. Unzip the source code
3. Copy the following files from assert-x.x/lib/Assert into admin/tool/mfa/factor/totp/extlib/Assert:
1. Assertion.php
2. AssertionFailedException.php
3. InvalidArgumentException.php
4. Copy the following files from assert-x.x into admin/tool/mfa/factor/totp/extlib/Assert:
1. LICENSE
2. composer.json
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
/**
* @internal
*/
final class InternalClock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}
+91 -87
View File
@@ -2,42 +2,33 @@
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
use Assert\Assertion;
use Exception;
use InvalidArgumentException;
use ParagonIE\ConstantTime\Base32;
use RuntimeException;
use function assert;
use function chr;
use function count;
use function is_string;
use const STR_PAD_LEFT;
abstract class OTP implements OTPInterface
{
use ParameterTrait;
/**
* OTP constructor.
*
* @param string|null $secret
* @param string $digest
* @param int $digits
*/
protected function __construct($secret, string $digest, int $digits)
{
$this->setSecret($secret);
$this->setDigest($digest);
$this->setDigits($digits);
}
private const DEFAULT_SECRET_SIZE = 64;
/**
* {@inheritdoc}
* @param non-empty-string $secret
*/
public function getQrCodeUri(string $uri = 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl={PROVISIONING_URI}', string $placeholder = '{PROVISIONING_URI}'): string
protected function __construct(string $secret)
{
$this->setSecret($secret);
}
public function getQrCodeUri(string $uri, string $placeholder): string
{
$provisioning_uri = urlencode($this->getProvisioningUri());
@@ -45,38 +36,52 @@ abstract class OTP implements OTPInterface
}
/**
* @param int $input
* @param 0|positive-int $input
*/
public function at(int $input): string
{
return $this->generateOTP($input);
}
/**
* @return non-empty-string
*/
final protected static function generateSecret(): string
{
return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
}
/**
* The OTP at the specified input.
*
* @return string The OTP at the specified input
* @param 0|positive-int $input
*
* @return non-empty-string
*/
protected function generateOTP(int $input): string
{
$hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret());
$hmac = [];
foreach (str_split($hash, 2) as $hex) {
$hmac[] = hexdec($hex);
}
$offset = $hmac[count($hmac) - 1] & 0xF;
$code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
$otp = $code % pow(10, $this->getDigits());
$hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
$unpacked = unpack('C*', $hash);
$unpacked !== false || throw new InvalidArgumentException('Invalid data.');
$hmac = array_values($unpacked);
$offset = ($hmac[count($hmac) - 1] & 0xF);
$code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
$otp = $code % (10 ** $this->getDigits());
return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);
}
/**
* {@inheritdoc}
* @param array<non-empty-string, mixed> $options
*/
public function at(int $timestamp): string
protected function filterOptions(array &$options): void
{
return $this->generateOTP($timestamp);
}
/**
* @param array $options
*/
protected function filterOptions(array &$options)
{
foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) {
foreach ([
'algorithm' => 'sha1',
'period' => 30,
'digits' => 6,
] as $key => $default) {
if (isset($options[$key]) && $default === $options[$key]) {
unset($options[$key]);
}
@@ -86,61 +91,60 @@ abstract class OTP implements OTPInterface
}
/**
* @param string $type
* @param array $options
* @param non-empty-string $type
* @param array<non-empty-string, mixed> $options
*
* @return string
* @return non-empty-string
*/
protected function generateURI(string $type, array $options): string
{
$label = $this->getLabel();
Assertion::string($label, 'The label is not set.');
Assertion::false($this->hasColon($label), 'Label must not contain a colon.');
$options = array_merge($options, $this->getParameters());
is_string($label) || throw new InvalidArgumentException('The label is not set.');
$this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');
$options = [...$options, ...$this->getParameters()];
$this->filterOptions($options);
$params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params);
return sprintf(
'otpauth://%s/%s?%s',
$type,
rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),
$params
);
}
/**
* @return string
*/
private function getDecodedSecret(): string
{
try {
$secret = Base32::decodeUpper($this->getSecret());
} catch (\Exception $e) {
throw new \RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
}
return $secret;
}
/**
* @param int $int
*
* @return string
*/
private function intToByteString(int $int): string
{
$result = [];
while (0 !== $int) {
$result[] = chr($int & 0xFF);
$int >>= 8;
}
return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
/**
* @param string $safe
* @param string $user
*
* @return bool
* @param non-empty-string $safe
* @param non-empty-string $user
*/
protected function compareOTP(string $safe, string $user): bool
{
return hash_equals($safe, $user);
}
/**
* @return non-empty-string
*/
private function getDecodedSecret(): string
{
try {
$decoded = Base32::decodeUpper($this->getSecret());
} catch (Exception) {
throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
}
assert($decoded !== '');
return $decoded;
}
private function intToByteString(int $int): string
{
$result = [];
while ($int !== 0) {
$result[] = chr($int & 0xFF);
$int >>= 8;
}
return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
}
@@ -1,123 +1,132 @@
<?php
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
declare(strict_types=1);
namespace OTPHP;
interface OTPInterface
{
/**
* @param int $timestamp
*
* @return string Return the OTP at the specified timestamp
*/
public function at(int $timestamp): string;
public const DEFAULT_DIGITS = 6;
public const DEFAULT_DIGEST = 'sha1';
/**
* Verify that the OTP is valid with the specified input.
* If no input is provided, the input is set to a default value or false is returned.
* Create a OTP object from an existing secret.
*
* @param string $otp
* @param int|null $input
* @param int|null $window
*
* @return bool
* @param non-empty-string $secret
*/
public function verify(string $otp, $input = null, $window = null): bool;
public static function createFromSecret(string $secret): self;
/**
* @return string The secret of the OTP
* Create a new OTP object. A random 64 bytes secret will be generated.
*/
public static function generate(): self;
/**
* @param non-empty-string $secret
*/
public function setSecret(string $secret): void;
public function setDigits(int $digits): void;
/**
* @param non-empty-string $digest
*/
public function setDigest(string $digest): void;
/**
* Generate the OTP at the specified input.
*
* @param 0|positive-int $input
*
* @return non-empty-string Return the OTP at the specified timestamp
*/
public function at(int $input): string;
/**
* Verify that the OTP is valid with the specified input. If no input is provided, the input is set to a default
* value or false is returned.
*
* @param non-empty-string $otp
* @param null|0|positive-int $input
* @param null|0|positive-int $window
*/
public function verify(string $otp, null|int $input = null, null|int $window = null): bool;
/**
* @return non-empty-string The secret of the OTP
*/
public function getSecret(): string;
/**
* @param string $label The label of the OTP
* @param non-empty-string $label The label of the OTP
*/
public function setLabel(string $label);
public function setLabel(string $label): void;
/**
* @return string|null The label of the OTP
* @return non-empty-string|null The label of the OTP
*/
public function getLabel();
public function getLabel(): null|string;
/**
* @return string|null The issuer
* @return non-empty-string|null The issuer
*/
public function getIssuer();
public function getIssuer(): ?string;
/**
* @param string $issuer
*
* @throws \InvalidArgumentException
* @param non-empty-string $issuer
*/
public function setIssuer(string $issuer);
public function setIssuer(string $issuer): void;
/**
* @return bool If true, the issuer will be added as a parameter in the provisioning URI
*/
public function isIssuerIncludedAsParameter(): bool;
/**
* @param bool $issuer_included_as_parameter
*
* @return $this
*/
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter);
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;
/**
* @return int Number of digits in the OTP
* @return positive-int Number of digits in the OTP
*/
public function getDigits(): int;
/**
* @return string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'
* @return non-empty-string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'
*/
public function getDigest(): string;
/**
* @param string $parameter
*
* @return null|mixed
* @param non-empty-string $parameter
*/
public function getParameter(string $parameter);
public function getParameter(string $parameter): mixed;
/**
* @param string $parameter
*
* @return bool
* @param non-empty-string $parameter
*/
public function hasParameter(string $parameter): bool;
/**
* @return array
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array;
/**
* @param string $parameter
* @param mixed $value
*
* @return $this
* @param non-empty-string $parameter
*/
public function setParameter(string $parameter, $value);
public function setParameter(string $parameter, mixed $value): void;
/**
* @return string Get the provisioning URI
* Get the provisioning URI.
*
* @return non-empty-string
*/
public function getProvisioningUri(): string;
/**
* @param string $uri The Uri of the QRCode generator with all parameters. By default the Googgle Chart API is used. This Uri MUST contain a placeholder that will be replaced by the method.
* @param string $placeholder The placeholder to be replaced in the QR Code generator URI. Default value is {PROVISIONING_URI}.
* Get the provisioning URI.
*
* @return string Get the provisioning URI
* @param non-empty-string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method.
* @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI
*/
public function getQrCodeUri(string $uri = 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl={PROVISIONING_URI}', string $placeholder = '{PROVISIONING_URI}'): string;
public function getQrCodeUri(string $uri, string $placeholder): string;
}
@@ -2,228 +2,181 @@
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
use Assert\Assertion;
use ParagonIE\ConstantTime\Base32;
use InvalidArgumentException;
use function array_key_exists;
use function assert;
use function in_array;
use function is_int;
use function is_string;
trait ParameterTrait
{
/**
* @var array
* @var array<non-empty-string, mixed>
*/
private $parameters = [];
private array $parameters = [];
/**
* @var string|null
* @var non-empty-string|null
*/
private $issuer = null;
private null|string $issuer = null;
/**
* @var string|null
* @var non-empty-string|null
*/
private $label = null;
private null|string $label = null;
private bool $issuer_included_as_parameter = true;
/**
* @var bool
*/
private $issuer_included_as_parameter = true;
/**
* @return array
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array
{
$parameters = $this->parameters;
if (null !== $this->getIssuer() && $this->isIssuerIncludedAsParameter() === true) {
if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
return $parameters;
}
/**
* @return string
*/
public function getSecret(): string
{
return $this->getParameter('secret');
$value = $this->getParameter('secret');
(is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "secret" parameter.');
return $value;
}
/**
* @param string|null $secret
*/
private function setSecret($secret)
{
$this->setParameter('secret', $secret);
}
/**
* @return string|null
*/
public function getLabel()
public function getLabel(): null|string
{
return $this->label;
}
/**
* @param string $label
*/
public function setLabel(string $label)
public function setLabel(string $label): void
{
$this->setParameter('label', $label);
}
/**
* @return string|null
*/
public function getIssuer()
public function getIssuer(): null|string
{
return $this->issuer;
}
/**
* @param string $issuer
*/
public function setIssuer(string $issuer)
public function setIssuer(string $issuer): void
{
$this->setParameter('issuer', $issuer);
}
/**
* @return bool
*/
public function isIssuerIncludedAsParameter(): bool
{
return $this->issuer_included_as_parameter;
}
/**
* @param bool $issuer_included_as_parameter
*/
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter)
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
{
$this->issuer_included_as_parameter = $issuer_included_as_parameter;
}
/**
* @return int
*/
public function getDigits(): int
{
return $this->getParameter('digits');
$value = $this->getParameter('digits');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "digits" parameter.');
return $value;
}
/**
* @param int $digits
*/
private function setDigits(int $digits)
{
$this->setParameter('digits', $digits);
}
/**
* @return string
*/
public function getDigest(): string
{
return $this->getParameter('algorithm');
$value = $this->getParameter('algorithm');
(is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "algorithm" parameter.');
return $value;
}
/**
* @param string $digest
*/
private function setDigest(string $digest)
{
$this->setParameter('algorithm', $digest);
}
/**
* @param string $parameter
*
* @return bool
*/
public function hasParameter(string $parameter): bool
{
return array_key_exists($parameter, $this->parameters);
}
/**
* @param string $parameter
*
* @return mixed
*/
public function getParameter(string $parameter)
public function getParameter(string $parameter): mixed
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
}
throw new \InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
}
/**
* @param string $parameter
* @param mixed $value
*/
public function setParameter(string $parameter, $value)
public function setParameter(string $parameter, mixed $value): void
{
$map = $this->getParameterMap();
if (true === array_key_exists($parameter, $map)) {
if (array_key_exists($parameter, $map) === true) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (property_exists($this, $parameter)) {
$this->$parameter = $value;
$this->{$parameter} = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
public function setSecret(string $secret): void
{
$this->setParameter('secret', $secret);
}
public function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
public function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
/**
* @return array
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
'label' => function ($value) {
Assertion::false($this->hasColon($value), 'Label must not contain a colon.');
'label' => function (string $value): string {
assert($value !== '');
$this->hasColon($value) === false || throw new InvalidArgumentException(
'Label must not contain a colon.'
);
return $value;
},
'secret' => function ($value) {
if (null === $value) {
$value = Base32::encodeUpper(random_bytes(64));
}
$value = trim(strtoupper($value), '=');
'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')),
'algorithm' => static function (string $value): string {
$value = mb_strtolower($value);
in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
'The "%s" digest is not supported.',
$value
));
return $value;
},
'algorithm' => function ($value) {
$value = strtolower($value);
Assertion::inArray($value, hash_algos(), sprintf('The "%s" digest is not supported.', $value));
return $value;
},
'digits' => function ($value) {
Assertion::greaterThan($value, 0, 'Digits must be at least 1.');
'digits' => static function ($value): int {
$value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');
return (int) $value;
},
'issuer' => function ($value) {
Assertion::false($this->hasColon($value), 'Issuer must not contain a colon.');
'issuer' => function (string $value): string {
assert($value !== '');
$this->hasColon($value) === false || throw new InvalidArgumentException(
'Issuer must not contain a colon.'
);
return $value;
},
@@ -231,15 +184,13 @@ trait ParameterTrait
}
/**
* @param string $value
*
* @return bool
* @param non-empty-string $value
*/
private function hasColon($value)
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (false !== strpos($value, $colon)) {
if (str_contains($value, $colon)) {
return true;
}
}
+138 -140
View File
@@ -2,216 +2,214 @@
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
use Assert\Assertion;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use function assert;
use function is_int;
/**
* @see \OTPHP\Test\TOTPTest
*/
final class TOTP extends OTP implements TOTPInterface
{
/**
* TOTP constructor.
*
* @param string|null $secret
* @param int $period
* @param string $digest
* @param int $digits
* @param int $epoch
*/
protected function __construct($secret, int $period, string $digest, int $digits, int $epoch = 0)
private readonly ClockInterface $clock;
public function __construct(string $secret, ?ClockInterface $clock = null)
{
parent::__construct($secret, $digest, $digits);
$this->setPeriod($period);
$this->setEpoch($epoch);
parent::__construct($secret);
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$this->clock = $clock;
}
/**
* TOTP constructor.
*
* @param string|null $secret
* @param int $period
* @param string $digest
* @param int $digits
* @param int $epoch
*
* @return self
*/
public static function create($secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6, int $epoch = 0): self
{
return new self($secret, $period, $digest, $digits, $epoch);
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH,
?ClockInterface $clock = null
): self {
$totp = $secret !== null
? self::createFromSecret($secret, $clock)
: self::generate($clock)
;
$totp->setPeriod($period);
$totp->setDigest($digest);
$totp->setDigits($digits);
$totp->setEpoch($epoch);
return $totp;
}
/**
* @param int $period
*/
protected function setPeriod(int $period)
public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self
{
$this->setParameter('period', $period);
$totp = new self($secret, $clock);
$totp->setPeriod(self::DEFAULT_PERIOD);
$totp->setDigest(self::DEFAULT_DIGEST);
$totp->setDigits(self::DEFAULT_DIGITS);
$totp->setEpoch(self::DEFAULT_EPOCH);
return $totp;
}
public static function generate(?ClockInterface $clock = null): self
{
return self::createFromSecret(self::generateSecret(), $clock);
}
/**
* {@inheritdoc}
*/
public function getPeriod(): int
{
return $this->getParameter('period');
$value = $this->getParameter('period');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
return $value;
}
/**
* @param int $epoch
*/
private function setEpoch(int $epoch)
{
$this->setParameter('epoch', $epoch);
}
/**
* {@inheritdoc}
*/
public function getEpoch(): int
{
return $this->getParameter('epoch');
$value = $this->getParameter('epoch');
(is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
return $value;
}
/**
* {@inheritdoc}
*/
public function at(int $timestamp): string
public function expiresIn(): int
{
return $this->generateOTP($this->timecode($timestamp));
$period = $this->getPeriod();
return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
}
/**
* {@inheritdoc}
* The OTP at the specified input.
*
* @param 0|positive-int $input
*/
public function at(int $input): string
{
return $this->generateOTP($this->timecode($input));
}
public function now(): string
{
return $this->at(time());
$timestamp = $this->clock->now()
->getTimestamp();
assert($timestamp >= 0, 'The timestamp must return a positive integer.');
return $this->at($timestamp);
}
/**
* If no timestamp is provided, the OTP is verified at the actual timestamp
* {@inheritdoc}
* If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
* allow time drift. The passed value is in seconds.
*
* @param 0|positive-int $timestamp
* @param null|0|positive-int $leeway
*/
public function verify(string $otp, $timestamp = null, $window = null): bool
public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
{
$timestamp = $this->getTimestamp($timestamp);
$timestamp ??= $this->clock->now()
->getTimestamp();
$timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
if (null === $window) {
if ($leeway === null) {
return $this->compareOTP($this->at($timestamp), $otp);
}
return $this->verifyOtpWithWindow($otp, $timestamp, $window);
$leeway = abs($leeway);
$leeway < $this->getPeriod() || throw new InvalidArgumentException(
'The leeway must be lower than the TOTP period'
);
$timestampMinusLeeway = $timestamp - $leeway;
$timestampMinusLeeway >= 0 || throw new InvalidArgumentException(
'The timestamp must be greater than or equal to the leeway.'
);
return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
|| $this->compareOTP($this->at($timestamp), $otp)
|| $this->compareOTP($this->at($timestamp + $leeway), $otp);
}
/**
* @param string $otp
* @param int $timestamp
* @param int $window
*
* @return bool
*/
private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool
{
$window = abs($window);
for ($i = 0; $i <= $window; $i++) {
$next = (int) $i * $this->getPeriod() + $timestamp;
$previous = (int) -$i * $this->getPeriod() + $timestamp;
$valid = $this->compareOTP($this->at($next), $otp) ||
$this->compareOTP($this->at($previous), $otp);
if ($valid) {
return true;
}
}
return false;
}
/**
* @param int|null $timestamp
*
* @return int
*/
private function getTimestamp($timestamp): int
{
$timestamp = $timestamp ?? time();
Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.');
return (int) $timestamp;
}
/**
* {@inheritdoc}
*/
public function getProvisioningUri(): string
{
$params = [];
if (30 !== $this->getPeriod()) {
if ($this->getPeriod() !== 30) {
$params['period'] = $this->getPeriod();
}
if (0 !== $this->getEpoch()) {
if ($this->getEpoch() !== 0) {
$params['epoch'] = $this->getEpoch();
}
return $this->generateURI('totp', $params);
}
/**
* @param int $timestamp
*
* @return int
*/
private function timecode(int $timestamp): int
public function setPeriod(int $period): void
{
return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
$this->setParameter('period', $period);
}
public function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
/**
* {@inheritdoc}
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
$v = array_merge(
parent::getParameterMap(),
[
'period' => function ($value) {
Assertion::greaterThan((int) $value, 0, 'Period must be at least 1.');
return [
...parent::getParameterMap(),
'period' => static function ($value): int {
(int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');
return (int) $value;
},
'epoch' => function ($value) {
Assertion::greaterOrEqualThan((int) $value, 0, 'Epoch must be greater than or equal to 0.');
return (int) $value;
},
'epoch' => static function ($value): int {
(int) $value >= 0 || throw new InvalidArgumentException(
'Epoch must be greater than or equal to 0.'
);
return (int) $value;
},
]
);
return $v;
return (int) $value;
},
];
}
/**
* {@inheritdoc}
* @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options)
protected function filterOptions(array &$options): void
{
parent::filterOptions($options);
if (isset($options['epoch']) && 0 === $options['epoch']) {
if (isset($options['epoch']) && $options['epoch'] === 0) {
unset($options['epoch']);
}
ksort($options);
}
/**
* @param 0|positive-int $timestamp
*
* @return 0|positive-int
*/
private function timecode(int $timestamp): int
{
$timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
assert($timecode >= 0);
return $timecode;
}
}
@@ -2,26 +2,50 @@
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
interface TOTPInterface extends OTPInterface
{
public const DEFAULT_PERIOD = 30;
public const DEFAULT_EPOCH = 0;
/**
* @return string Return the TOTP at the current time
* Create a new TOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param positive-int $period
* @param non-empty-string $digest
* @param positive-int $digits
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
): self;
public function setPeriod(int $period): void;
public function setEpoch(int $epoch): void;
/**
* Return the TOTP at the current time.
*
* @return non-empty-string
*/
public function now(): string;
/**
* @return int Get the period of time for OTP generation (a non-null positive integer, in second)
* Get the period of time for OTP generation (a non-null positive integer, in second).
*/
public function getPeriod(): int;
public function expiresIn(): int;
public function getEpoch(): int;
}
@@ -16,15 +16,25 @@
}
],
"require": {
"php": "^7.1",
"paragonie/constant_time_encoding": "^2.0",
"beberlei/assert": "^2.4"
"php": ">=8.1",
"ext-mbstring": "*",
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"satooshi/php-coveralls": "^1.0"
},
"suggest": {
"ekino/phpstan-banned-code": "^1.0",
"infection/infection": "^0.26|^0.27|^0.28|^0.29",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5.26|^10.0|^11.0",
"qossmic/deptrac-shim": "^1.0",
"rector/rector": "^1.0",
"symfony/phpunit-bridge": "^6.1|^7.0",
"symplify/easy-coding-standard": "^12.0"
},
"autoload": {
"psr-4": { "OTPHP\\": "src/" }
@@ -32,9 +42,18 @@
"autoload-dev": {
"psr-4": { "OTPHP\\Test\\": "tests/" }
},
"extra": {
"branch-alias": {
"dev-master": "9.0.x-dev"
}
"config": {
"allow-plugins": {
"phpstan/extension-installer": true,
"infection/extension-installer": true,
"composer/package-versions-deprecated": true,
"symfony/flex": true,
"symfony/runtime": true
},
"optimize-autoloader": true,
"preferred-install": {
"*": "dist"
},
"sort-packages": true
}
}
@@ -1,18 +1,18 @@
OTPHP 9.1.1
OTPHP
--------------
https://github.com/Spomky-Labs/otphp/releases/tag/v9.1.1
Instructions to import WebAuthn into Moodle:
Instructions to import OTPHP into Moodle:
1. Download the latest release from https://github.com/Spomky-Labs/otphp/releases/tag/vx.x.x
(choose "Source code")
2. Unzip the source code
3. Copy the following files from otphp-x.x/lib/OTPHP into admin/tool/mfa/factor/totp/extlib/OTPHP:
1. OTP.php
2. OTPInterface.php
3. ParameterTrait.php
4. TOTP.php
5. TOTPInterface.php
3. Copy the following files from otphp-x.x/src into admin/tool/mfa/factor/totp/extlib/OTPHP:
1. InternalClock.php
2. OTP.php
3. OTPInterface.php
4. ParameterTrait.php
5. TOTP.php
6. TOTPInterface.php
4. Copy the following files from otphp-x.x into admin/tool/mfa/factor/totp/extlib/OTPHP:
1. LICENSE
@@ -54,8 +54,11 @@ $string['revokefactorconfirmation'] = 'Remove \'{$a}\' authenticator app?';
$string['settings:totplink'] = 'Show mobile app setup link';
$string['settings:totplink_help'] = 'If enabled the user will see a 3rd setup option with a direct otpauth:// link';
$string['settings:window'] = 'TOTP verification window';
$string['settings:window_help'] = 'How long each code is valid for. You can set this to a higher value as a workaround if your users device clocks are often slightly wrong.
Rounded down to the nearest 30 seconds, which is the time between new generated codes.';
$string['settings:window_help'] = 'The window of TOTP acts as time drift and specifies how long each code is valid for.
The period, which is the time between newly generated codes, is 30 seconds.
If the window is 15 (the default) and the current timestamp is 147682209, the OTP tested are within 147682194 (147682209 - 15), 147682209 and 147682224 (147682209 + 15).
The window shall be lower than 30. Therefore, this test includes the previous OTP but not the next one.
You can set this to a higher value (up to 29) as a workaround if your user\'s device clocks are often slightly wrong.';
$string['setupfactor'] = 'Set up authenticator app';
$string['setupfactorbutton'] = 'Set up';
$string['setupfactor:account'] = 'Account:';
+4 -2
View File
@@ -38,9 +38,11 @@ $settings->add(new admin_setting_configtext('factor_totp/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configduration('factor_totp/window',
$window = new admin_setting_configduration('factor_totp/window',
new lang_string('settings:window', 'factor_totp'),
new lang_string('settings:window_help', 'factor_totp'), 30));
new lang_string('settings:window_help', 'factor_totp'), 15);
$window->set_max_duration(29);
$settings->add($window);
$settings->add(new admin_setting_configcheckbox('factor_totp/totplink',
new lang_string('settings:totplink', 'factor_totp'),
@@ -21,12 +21,10 @@ defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/../extlib/OTPHP/OTPInterface.php');
require_once(__DIR__.'/../extlib/OTPHP/TOTPInterface.php');
require_once(__DIR__.'/../extlib/OTPHP/ParameterTrait.php');
require_once(__DIR__.'/../extlib/OTPHP/InternalClock.php');
require_once(__DIR__.'/../extlib/OTPHP/OTP.php');
require_once(__DIR__.'/../extlib/OTPHP/TOTP.php');
require_once(__DIR__.'/../extlib/Assert/Assertion.php');
require_once(__DIR__.'/../extlib/Assert/AssertionFailedException.php');
require_once(__DIR__.'/../extlib/Assert/InvalidArgumentException.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/EncoderInterface.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Binary.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Base32.php');
@@ -40,7 +38,7 @@ require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Base32.php');
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
final class factor_test extends \advanced_testcase {
/**
* Test code validation of the TOTP factor
@@ -48,12 +46,14 @@ class factor_test extends \advanced_testcase {
public function test_validate_code(): void {
global $DB;
$clock = $this->mock_clock_with_frozen(10000000);
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Setup test staples.
$totp = \OTPHP\TOTP::create('fakekey');
$window = 10;
$totp = \OTPHP\TOTP::create('fakekey', clock: $clock);
$window = 29;
set_config('enabled', 1, 'factor_totp');
$totpfactor = \tool_mfa\plugininfo\factor::get_factor('totp');
@@ -64,50 +64,44 @@ class factor_test extends \advanced_testcase {
$factorinstance = $totpfactor->setup_user_factor((object) $totpdata);
// First check that a valid code is actually valid.
$code = $totp->at(time());
$code = $totp->at($clock->time());
// Manually set timeverified of factor.
$DB->set_field('tool_mfa', 'lastverified', time() - WEEKSECS, ['id' => $factorinstance->id]);
$DB->set_field('tool_mfa', 'lastverified', $clock->time() - WEEKSECS, ['id' => $factorinstance->id]);
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_VALID, $result);
// Now update timeverified to 2 mins ago, and check codes within window are blocked.
$code = $totp->at(time() - (2 * MINSECS));
$DB->set_field('tool_mfa', 'lastverified', time() - (2 * MINSECS), ['id' => $factorinstance->id]);
// Now update timeverified to 20 seconds ago, and check codes within window is blocked.
$code = $totp->at($clock->time() - (20));
$DB->set_field('tool_mfa', 'lastverified', $clock->time() - (20), ['id' => $factorinstance->id]);
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_USED, $result);
// Now update timeverified to 2 mins ago, and check codes within window are blocked.
$code = $totp->at(time());
$DB->set_field('tool_mfa', 'lastverified', time() - (2 * MINSECS), ['id' => $factorinstance->id]);
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_USED, $result);
// Now update timeverified to 2 mins ago, and check codes within window are blocked.
$code = $totp->at(time() - (4 * MINSECS));
$DB->set_field('tool_mfa', 'lastverified', time() - (2 * MINSECS), ['id' => $factorinstance->id]);
// Now update timeverified to 20 seconds ago, and check code from current increment within window is blocked.
$code = $totp->at($clock->time());
$DB->set_field('tool_mfa', 'lastverified', $clock->time() - (20), ['id' => $factorinstance->id]);
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_USED, $result);
// Now check future codes.
$window = 1;
$code = $totp->at(time() + (2 * MINSECS));
$DB->set_field('tool_mfa', 'lastverified', time() - WEEKSECS, ['id' => $factorinstance->id]);
$code = $totp->at($clock->time() + (2 * MINSECS));
$DB->set_field('tool_mfa', 'lastverified', $clock->time() - WEEKSECS, ['id' => $factorinstance->id]);
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_FUTURE, $result);
// Codes in far future are invalid.
$code = $totp->at(time() + (20 * MINSECS));
$code = $totp->at($clock->time() + (20 * MINSECS));
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_INVALID, $result);
// Do the same for past codes.
$window = 1;
$code = $totp->at(time() - (2 * MINSECS));
$code = $totp->at($clock->time() - (2 * MINSECS));
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_OLD, $result);
// Codes in far future are invalid.
$code = $totp->at(time() - (20 * MINSECS));
$code = $totp->at($clock->time() - (20 * MINSECS));
$result = $totpfactor->validate_code($code, $window, $totp, $factorinstance);
$this->assertEquals($totpfactor::TOTP_INVALID, $result);
@@ -1,16 +1,9 @@
<?xml version="1.0"?>
<libraries>
<library>
<location>extlib/Assert</location>
<name>Assert</name>
<version>2.1</version>
<license>MIT</license>
<repository>https://github.com/beberlei/assert</repository>
</library>
<library>
<location>extlib/OTPHP</location>
<name>OTPHP</name>
<version>9.1.1</version>
<version>11.3.0</version>
<license>MIT</license>
<repository>https://github.com/Spomky-Labs/otphp</repository>
</library>
+2 -2
View File
@@ -26,7 +26,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->version = 2024081600; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_totp'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;