├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── publish-to-redaxo-org.yml
├── .gitignore
├── update.php
├── composer.json
├── pages
├── index.php
├── users.php
├── verify.php
├── settings.php
└── setup.php
├── lib
├── exception.php
├── method_interface.php
├── setup.php
├── method_totp.php
├── console
│ └── 2factor_auth
│ │ ├── status.php
│ │ ├── user.php
│ │ └── enforce.php
├── one_time_password.php
├── method_email.php
└── one_time_password_config.php
├── vendor
├── composer
│ ├── autoload_namespaces.php
│ ├── autoload_classmap.php
│ ├── autoload_files.php
│ ├── autoload_psr4.php
│ ├── platform_check.php
│ ├── LICENSE
│ ├── autoload_static.php
│ ├── autoload_real.php
│ ├── installed.php
│ └── installed.json
├── symfony
│ └── deprecation-contracts
│ │ ├── CHANGELOG.md
│ │ ├── composer.json
│ │ ├── function.php
│ │ ├── LICENSE
│ │ └── README.md
├── psr
│ └── clock
│ │ ├── CHANGELOG.md
│ │ ├── src
│ │ └── ClockInterface.php
│ │ ├── composer.json
│ │ ├── LICENSE
│ │ └── README.md
├── spomky-labs
│ └── otphp
│ │ ├── src
│ │ ├── InternalClock.php
│ │ ├── FactoryInterface.php
│ │ ├── HOTPInterface.php
│ │ ├── TOTPInterface.php
│ │ ├── Url.php
│ │ ├── Factory.php
│ │ ├── HOTP.php
│ │ ├── OTPInterface.php
│ │ ├── OTP.php
│ │ ├── ParameterTrait.php
│ │ └── TOTP.php
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── composer.json
│ │ └── SECURITY.md
├── autoload.php
└── paragonie
│ └── constant_time_encoding
│ ├── composer.json
│ ├── src
│ ├── EncoderInterface.php
│ ├── Base64DotSlashOrdered.php
│ ├── Binary.php
│ ├── Base64DotSlash.php
│ ├── Base64UrlSafe.php
│ ├── Base32Hex.php
│ ├── Hex.php
│ ├── RFC4648.php
│ ├── Encoding.php
│ └── Base64.php
│ ├── LICENSE.txt
│ └── README.md
├── uninstall.php
├── .travis.yml
├── install.php
├── LICENSE
├── package.yml
├── fragments
├── 2fa.setup-email.php
├── 2fa.login.php
└── 2fa.setup-totp.php
├── .php_cs.dist
├── boot.php
├── README.md
├── lang
└── de_de.lang
├── assets
└── clipboard-copy-element.js
└── composer.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [staabm]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .php_cs.cache
3 | vendor/.DS_Store
4 |
--------------------------------------------------------------------------------
/update.php:
--------------------------------------------------------------------------------
1 | includeFile(__DIR__.'/install.php');
4 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "php": ">=8.1",
4 | "spomky-labs/otphp": "^11.0"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/pages/index.php:
--------------------------------------------------------------------------------
1 | removeColumn('one_time_password_config')
5 | ->removeColumn('one_time_password_tries')
6 | ->removeColumn('one_time_password_lasttry')
7 | ->ensure();
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: spomky-labs/otphp
10 | versions:
11 | - ">= 10.a, < 11"
12 |
--------------------------------------------------------------------------------
/vendor/psr/clock/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file, in reverse chronological order by release.
4 |
5 | ## 1.0.0
6 |
7 | First stable release after PSR-20 acceptance
8 |
9 | ## 0.1.0
10 |
11 | First release
12 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_classmap.php:
--------------------------------------------------------------------------------
1 | $vendorDir . '/composer/InstalledVersions.php',
10 | );
11 |
--------------------------------------------------------------------------------
/vendor/psr/clock/src/ClockInterface.php:
--------------------------------------------------------------------------------
1 | $vendorDir . '/symfony/deprecation-contracts/function.php',
10 | );
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - '7.1'
5 |
6 | cache:
7 | directories:
8 | - $HOME/.composer/cache
9 |
10 | before_install:
11 | - phpenv config-rm xdebug.ini || echo "xdebug not available"
12 |
13 | script:
14 | - composer require --dev friendsofredaxo/linter
15 | - vendor/bin/rexlint
16 |
17 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/InternalClock.php:
--------------------------------------------------------------------------------
1 | array($vendorDir . '/psr/clock/src'),
10 | 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
11 | 'OTPHP\\' => array($vendorDir . '/spomky-labs/otphp/src'),
12 | );
13 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/FactoryInterface.php:
--------------------------------------------------------------------------------
1 | getPath('/lib/setup.php');
7 | }
8 |
9 | rex_sql_table::get(rex::getTable('user'))
10 | ->ensureColumn(new rex_sql_column('one_time_password_config', 'text'), 'revision')
11 | ->ensureColumn(new rex_sql_column('one_time_password_tries', 'tinyint'), 'one_time_password_config')
12 | ->ensureColumn(new rex_sql_column('one_time_password_lasttry', 'int'), 'one_time_password_tries')
13 | ->ensure();
14 |
15 | setup::install();
16 |
--------------------------------------------------------------------------------
/lib/method_interface.php:
--------------------------------------------------------------------------------
1 | =8.1"
19 | },
20 | "autoload": {
21 | "files": [
22 | "function.php"
23 | ]
24 | },
25 | "minimum-stability": "dev",
26 | "extra": {
27 | "branch-alias": {
28 | "dev-main": "3.5-dev"
29 | },
30 | "thanks": {
31 | "name": "symfony/contracts",
32 | "url": "https://github.com/symfony/contracts"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/HOTPInterface.php:
--------------------------------------------------------------------------------
1 | = 80100)) {
8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
9 | }
10 |
11 | if ($issues) {
12 | if (!headers_sent()) {
13 | header('HTTP/1.1 500 Internal Server Error');
14 | }
15 | if (!ini_get('display_errors')) {
16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
18 | } elseif (!headers_sent()) {
19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
20 | }
21 | }
22 | trigger_error(
23 | 'Composer detected issues in your platform: ' . implode(' ', $issues),
24 | E_USER_ERROR
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Friends Of REDAXO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/vendor/composer/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright (c) Nils Adermann, Jordi Boggiano
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is furnished
9 | to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/vendor/psr/clock/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 PHP Framework Interoperability Group
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/vendor/symfony/deprecation-contracts/function.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | if (!function_exists('trigger_deprecation')) {
13 | /**
14 | * Triggers a silenced deprecation notice.
15 | *
16 | * @param string $package The name of the Composer package that is triggering the deprecation
17 | * @param string $version The version of the package that introduced the deprecation
18 | * @param string $message The message of the deprecation
19 | * @param mixed ...$args Values to insert in the message using printf() formatting
20 | *
21 | * @author Nicolas Grekas
22 | */
23 | function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
24 | {
25 | @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/vendor/symfony/deprecation-contracts/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020-present Fabien Potencier
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2016 Florent Morselli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/package.yml:
--------------------------------------------------------------------------------
1 | package: 2factor_auth
2 | version: '2.3'
3 | author: Friends Of REDAXO
4 |
5 | page:
6 | title: 'translate:2factor_auth'
7 | icon: rex-icon fa-lock
8 | subpages:
9 | setup:
10 | title: 'translate:2factor_auth_private_setup'
11 | docs:
12 | title: translate:readme
13 | subPath: README.md
14 | icon: rex-icon fa-info-circle
15 | itemClass: pull-right
16 | users:
17 | title: translate:users
18 | perm: admin[]
19 | itemClass: pull-right
20 | settings:
21 | title: 'translate:2factor_auth_settings'
22 | perm: admin[]
23 | itemClass: pull-right
24 |
25 | pages:
26 | 2factor_auth_verify:
27 | title: 'translate:OTP-Verifizierung'
28 | hasNavigation: false
29 | main: true
30 | hidden: true
31 | path: pages/verify.php
32 |
33 | console_commands:
34 | 2factor_auth:enforce: rex_command_2factor_auth_enforce
35 | 2factor_auth:user: rex_command_2factor_auth_user
36 | 2factor_auth:status: rex_command_2factor_auth_status
37 |
38 | requires:
39 | php:
40 | version: '>=8.1'
41 | redaxo: ^5.14.0
42 |
43 | load: early
44 |
--------------------------------------------------------------------------------
/vendor/symfony/deprecation-contracts/README.md:
--------------------------------------------------------------------------------
1 | Symfony Deprecation Contracts
2 | =============================
3 |
4 | A generic function and convention to trigger deprecation notices.
5 |
6 | This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
7 |
8 | By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
9 | the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
10 |
11 | The function requires at least 3 arguments:
12 | - the name of the Composer package that is triggering the deprecation
13 | - the version of the package that introduced the deprecation
14 | - the message of the deprecation
15 | - more arguments can be provided: they will be inserted in the message using `printf()` formatting
16 |
17 | Example:
18 | ```php
19 | trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
20 | ```
21 |
22 | This will generate the following message:
23 | `Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
24 |
25 | While not recommended, the deprecation notices can be completely ignored by declaring an empty
26 | `function trigger_deprecation() {}` in your application.
27 |
--------------------------------------------------------------------------------
/fragments/2fa.setup-email.php:
--------------------------------------------------------------------------------
1 |
3 |
4 | message) : ?>
5 | = $this->message ?>
6 |
7 |
8 | success) : ?>
9 |
10 |
11 |
2. = $this->addon->i18n('2fa_verify_headline') ?>
12 |
13 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/TOTPInterface.php:
--------------------------------------------------------------------------------
1 | verify($otp);
26 | }
27 |
28 | public static function getPeriod(): int
29 | {
30 | // default period is 30s and digest is sha1. Google Authenticator is restricted to this settings
31 | return 30;
32 | }
33 |
34 | public static function getloginTries(): int
35 | {
36 | return 10;
37 | }
38 |
39 | public function getProvisioningUri(rex_user $user): string
40 | {
41 | // create a uri with a random secret
42 | $otp = TOTP::create(null, self::getPeriod());
43 |
44 | // the label rendered in "Google Authenticator" or similar app
45 | $label = $user->getLogin() . '@' . rex::getServerName() . ' (' . $_SERVER['HTTP_HOST'] . ')';
46 | $label = str_replace(':', '_', $label); // colon is forbidden
47 | $otp->setLabel($label);
48 | $otp->setIssuer(str_replace(':', '_', $user->getLogin()));
49 |
50 | return $otp->getProvisioningUri();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "paragonie/constant_time_encoding",
3 | "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
4 | "keywords": [
5 | "base64",
6 | "encoding",
7 | "rfc4648",
8 | "base32",
9 | "base16",
10 | "hex",
11 | "bin2hex",
12 | "hex2bin",
13 | "base64_encode",
14 | "base64_decode",
15 | "base32_encode",
16 | "base32_decode"
17 | ],
18 | "license": "MIT",
19 | "type": "library",
20 | "authors": [
21 | {
22 | "name": "Paragon Initiative Enterprises",
23 | "email": "security@paragonie.com",
24 | "homepage": "https://paragonie.com",
25 | "role": "Maintainer"
26 | },
27 | {
28 | "name": "Steve 'Sc00bz' Thomas",
29 | "email": "steve@tobtu.com",
30 | "homepage": "https://www.tobtu.com",
31 | "role": "Original Developer"
32 | }
33 | ],
34 | "support": {
35 | "issues": "https://github.com/paragonie/constant_time_encoding/issues",
36 | "email": "info@paragonie.com",
37 | "source": "https://github.com/paragonie/constant_time_encoding"
38 | },
39 | "require": {
40 | "php": "^8"
41 | },
42 | "require-dev": {
43 | "phpunit/phpunit": "^9",
44 | "vimeo/psalm": "^4|^5"
45 | },
46 | "autoload": {
47 | "psr-4": {
48 | "ParagonIE\\ConstantTime\\": "src/"
49 | }
50 | },
51 | "autoload-dev": {
52 | "psr-4": {
53 | "ParagonIE\\ConstantTime\\Tests\\": "tests/"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.php_cs.dist:
--------------------------------------------------------------------------------
1 | setFinder(
5 | PhpCsFixer\Finder::create()
6 | ->in(__DIR__)
7 | )
8 | ->setUsingCache(true)
9 | ->setRiskyAllowed(true)
10 | ->setRules([
11 | '@Symfony' => true,
12 | '@Symfony:risky' => true,
13 | 'array_indentation' => true,
14 | 'array_syntax' => ['syntax' => 'short'],
15 | 'blank_line_before_statement' => false,
16 | 'braces' => ['allow_single_line_closure' => false],
17 | 'concat_space' => false,
18 | 'function_to_constant' => ['functions' => ['get_class', 'get_called_class', 'php_sapi_name', 'phpversion', 'pi']],
19 | 'heredoc_to_nowdoc' => true,
20 | 'logical_operators' => true,
21 | 'native_constant_invocation' => false,
22 | 'no_blank_lines_after_phpdoc' => false,
23 | 'no_php4_constructor' => true,
24 | 'no_superfluous_elseif' => true,
25 | 'no_unneeded_final_method' => false,
26 | 'no_unreachable_default_argument_value' => true,
27 | 'no_useless_else' => true,
28 | 'no_useless_return' => true,
29 | 'phpdoc_annotation_without_dot' => false,
30 | 'phpdoc_no_package' => false,
31 | 'phpdoc_to_comment' => false,
32 | 'phpdoc_trim_consecutive_blank_line_separation' => true,
33 | 'phpdoc_var_without_name' => false,
34 | 'psr4' => false,
35 | 'semicolon_after_instruction' => false,
36 | 'space_after_semicolon' => true,
37 | 'string_line_ending' => true,
38 | 'yoda_style' => false,
39 | ])
40 | ;
41 |
--------------------------------------------------------------------------------
/vendor/psr/clock/README.md:
--------------------------------------------------------------------------------
1 | # PSR Clock
2 |
3 | This repository holds the interface for [PSR-20][psr-url].
4 |
5 | Note that this is not a clock of its own. It is merely an interface that
6 | describes a clock. See the specification for more details.
7 |
8 | ## Installation
9 |
10 | ```bash
11 | composer require psr/clock
12 | ```
13 |
14 | ## Usage
15 |
16 | If you need a clock, you can use the interface like this:
17 |
18 | ```php
19 | clock = $clock;
30 | }
31 |
32 | public function doSomething()
33 | {
34 | /** @var DateTimeImmutable $currentDateAndTime */
35 | $currentDateAndTime = $this->clock->now();
36 | // do something useful with that information
37 | }
38 | }
39 | ```
40 |
41 | You can then pick one of the [implementations][implementation-url] of the interface to get a clock.
42 |
43 | If you want to implement the interface, you can require this package and
44 | implement `Psr\Clock\ClockInterface` in your code.
45 |
46 | Don't forget to add `psr/clock-implementation` to your `composer.json`s `provides`-section like this:
47 |
48 | ```json
49 | {
50 | "provides": {
51 | "psr/clock-implementation": "1.0"
52 | }
53 | }
54 | ```
55 |
56 | And please read the [specification text][specification-url] for details on the interface.
57 |
58 | [psr-url]: https://www.php-fig.org/psr/psr-20
59 | [package-url]: https://packagist.org/packages/psr/clock
60 | [implementation-url]: https://packagist.org/providers/psr/clock-implementation
61 | [specification-url]: https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md
62 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_static.php:
--------------------------------------------------------------------------------
1 | __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
11 | );
12 |
13 | public static $prefixLengthsPsr4 = array (
14 | 'P' =>
15 | array (
16 | 'Psr\\Clock\\' => 10,
17 | 'ParagonIE\\ConstantTime\\' => 23,
18 | ),
19 | 'O' =>
20 | array (
21 | 'OTPHP\\' => 6,
22 | ),
23 | );
24 |
25 | public static $prefixDirsPsr4 = array (
26 | 'Psr\\Clock\\' =>
27 | array (
28 | 0 => __DIR__ . '/..' . '/psr/clock/src',
29 | ),
30 | 'ParagonIE\\ConstantTime\\' =>
31 | array (
32 | 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
33 | ),
34 | 'OTPHP\\' =>
35 | array (
36 | 0 => __DIR__ . '/..' . '/spomky-labs/otphp/src',
37 | ),
38 | );
39 |
40 | public static $classMap = array (
41 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
42 | );
43 |
44 | public static function getInitializer(ClassLoader $loader)
45 | {
46 | return \Closure::bind(function () use ($loader) {
47 | $loader->prefixLengthsPsr4 = ComposerStaticInitd487eb39062b37336c507c1263b844e3::$prefixLengthsPsr4;
48 | $loader->prefixDirsPsr4 = ComposerStaticInitd487eb39062b37336c507c1263b844e3::$prefixDirsPsr4;
49 | $loader->classMap = ComposerStaticInitd487eb39062b37336c507c1263b844e3::$classMap;
50 |
51 | }, null, ClassLoader::class);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_real.php:
--------------------------------------------------------------------------------
1 | register(true);
35 |
36 | $filesToLoad = \Composer\Autoload\ComposerStaticInitd487eb39062b37336c507c1263b844e3::$files;
37 | $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
38 | if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
39 | $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
40 |
41 | require $file;
42 | }
43 | }, null, null);
44 | foreach ($filesToLoad as $fileIdentifier => $file) {
45 | $requireFile($fileIdentifier, $file);
46 | }
47 |
48 | return $loader;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/boot.php:
--------------------------------------------------------------------------------
1 | getAssetsUrl('qrious.min.js'));
10 | rex_view::addJsFile($addon->getAssetsUrl('clipboard-copy-element.js'));
11 | }
12 |
13 | $otp = one_time_password::getInstance();
14 |
15 | // den benutzer auf das setup leiten, weil erwzungen aber noch nicht durchgefuehrt
16 | if (!$otp->isEnabled()) {
17 | if (one_time_password::ENFORCED_ALL === $otp->isEnforced()
18 | || one_time_password::ENFORCED_ADMINS === $otp->isEnforced() && rex::getUser()->isAdmin()) {
19 | rex_be_controller::setCurrentPage('2factor_auth/setup');
20 | return;
21 | }
22 | }
23 |
24 | // den benutzer zur einmal passwort eingabe leiten, weil one-time-passwort aktiv
25 | // und bisher fuer die session noch nicht eingegeben
26 | if ($otp->isEnabled()) {
27 | if (!$otp->isVerified()) {
28 | rex_extension::register('PAGES_PREPARED', static function (rex_extension_point $ep) {
29 | $profilePage = rex_be_controller::getCurrentPageObject('profile');
30 | if (!$profilePage) {
31 | return;
32 | }
33 | $profilePage->setPath(rex_path::addon('2factor_auth', 'pages/verify.php'));
34 | $profilePage->setHasNavigation(false);
35 | $profilePage->setPjax(false);
36 | rex_extension::register('PAGE_BODY_ATTR', static function (rex_extension_point $ep) {
37 | $attributes = $ep->getSubject();
38 | /** add rex-page-login id */
39 | $attributes['id'] = ['rex-page-login'];
40 | $ep->setSubject($attributes);
41 | });
42 | });
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/console/2factor_auth/status.php:
--------------------------------------------------------------------------------
1 | setDescription('List of users with status info')
19 | ;
20 | }
21 |
22 | protected function execute(InputInterface $input, OutputInterface $output)
23 | {
24 | $io = $this->getStyle($input, $output);
25 |
26 | $opt = one_time_password::getInstance();
27 | $status = '-';
28 | switch ($opt->isEnforced()) {
29 | case one_time_password::ENFORCED_ALL:
30 | $status = 'all';
31 | break;
32 | case one_time_password::ENFORCED_ADMINS:
33 | $status = 'admins';
34 | break;
35 | case one_time_password::ENFORCED_DISABLED:
36 | $status = 'nobody';
37 | break;
38 | }
39 |
40 | $io->text('2Factor-Auth is mandatory for: ' . $status . '');
41 | $io->text('');
42 |
43 | $users = rex_sql::factory();
44 | $users
45 | ->setTable(rex::getTable('user'))
46 | ->select();
47 |
48 | $userRows = [];
49 | foreach ($users as $user) {
50 | $user = rex_user::fromSql($user);
51 | $config = one_time_password_config::forUser($user);
52 | $userRows[] = [
53 | $user->getId(),
54 | $user->getLogin(),
55 | $config->enabled ? 'on' : 'off',
56 | $config->method,
57 | ];
58 | }
59 |
60 | $io->table([
61 | 'id',
62 | 'login',
63 | 'status',
64 | 'method',
65 | ], $userRows);
66 |
67 | return 0;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/EncoderInterface.php:
--------------------------------------------------------------------------------
1 | =8.1",
20 | "ext-mbstring": "*",
21 | "paragonie/constant_time_encoding": "^2.0 || ^3.0",
22 | "psr/clock": "^1.0",
23 | "symfony/deprecation-contracts": "^3.2"
24 | },
25 | "require-dev": {
26 | "ekino/phpstan-banned-code": "^1.0",
27 | "infection/infection": "^0.26|^0.27|^0.28|^0.29",
28 | "php-parallel-lint/php-parallel-lint": "^1.3",
29 | "phpstan/phpstan": "^1.0",
30 | "phpstan/phpstan-deprecation-rules": "^1.0",
31 | "phpstan/phpstan-phpunit": "^1.0",
32 | "phpstan/phpstan-strict-rules": "^1.0",
33 | "phpunit/phpunit": "^9.5.26|^10.0|^11.0",
34 | "qossmic/deptrac-shim": "^1.0",
35 | "rector/rector": "^1.0",
36 | "symfony/phpunit-bridge": "^6.1|^7.0",
37 | "symplify/easy-coding-standard": "^12.0"
38 | },
39 | "autoload": {
40 | "psr-4": { "OTPHP\\": "src/" }
41 | },
42 | "autoload-dev": {
43 | "psr-4": { "OTPHP\\Test\\": "tests/" }
44 | },
45 | "config": {
46 | "allow-plugins": {
47 | "phpstan/extension-installer": true,
48 | "infection/extension-installer": true,
49 | "composer/package-versions-deprecated": true,
50 | "symfony/flex": true,
51 | "symfony/runtime": true
52 | },
53 | "optimize-autoloader": true,
54 | "preferred-install": {
55 | "*": "dist"
56 | },
57 | "sort-packages": true
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pages/users.php:
--------------------------------------------------------------------------------
1 | disable();
10 |
11 | echo rex_view::success('User ' . $user->getLogin() . ' deactivated');
12 | } catch (Exception $e) {
13 | echo rex_view::error($e->getMessage());
14 | }
15 | }
16 |
17 | $users = rex_sql::factory();
18 | $users
19 | ->setTable(rex::getTable('user'))
20 | ->select();
21 |
22 | $content = '';
23 |
24 | $content .= '';
25 | $content .= '';
26 | $content .= '| id | ';
27 | $content .= 'login | ';
28 | $content .= 'status | ';
29 | $content .= 'method | ';
30 | $content .= 'tries | ';
31 | $content .= 'last_try | ';
32 | $content .= 'actions | ';
33 | $content .= '
';
34 | $content .= '';
35 |
36 | $content .= '';
37 |
38 | $trs = [];
39 | foreach ($users as $user) {
40 | $user = rex_user::fromSql($user);
41 | $config = one_time_password_config::forUser($user);
42 | $trs[] = '
43 | | ' . rex_escape($user->getId()) . ' |
44 | ' . rex_escape($user->getLogin()) . ' |
45 | ' . rex_escape($config->enabled ? 'on' : 'off') . ' |
46 | ' . rex_escape($config->method) . ' |
47 | ' . rex_escape($user->getValue('one_time_password_tries')) . ' |
48 | ' . rex_escape($user->getValue('one_time_password_lasttry')) . ' |
49 | deactivate |
50 |
';
51 | }
52 |
53 | $content .= implode('', $trs);
54 |
55 | $content .= '';
56 | $content .= '
';
57 |
58 | $fragment = new rex_fragment();
59 | $fragment->setVar('title', $this->i18n('users'), false);
60 | $fragment->setVar('content', $content, false);
61 | echo $fragment->parse('core/page/section.php');
62 |
--------------------------------------------------------------------------------
/vendor/composer/installed.php:
--------------------------------------------------------------------------------
1 | array(
3 | 'name' => '__root__',
4 | 'pretty_version' => 'dev-master',
5 | 'version' => 'dev-master',
6 | 'reference' => 'dd3d84c5da821a2975df978ecc662191fee51fa1',
7 | 'type' => 'library',
8 | 'install_path' => __DIR__ . '/../../',
9 | 'aliases' => array(),
10 | 'dev' => true,
11 | ),
12 | 'versions' => array(
13 | '__root__' => array(
14 | 'pretty_version' => 'dev-master',
15 | 'version' => 'dev-master',
16 | 'reference' => 'dd3d84c5da821a2975df978ecc662191fee51fa1',
17 | 'type' => 'library',
18 | 'install_path' => __DIR__ . '/../../',
19 | 'aliases' => array(),
20 | 'dev_requirement' => false,
21 | ),
22 | 'paragonie/constant_time_encoding' => array(
23 | 'pretty_version' => 'v3.0.0',
24 | 'version' => '3.0.0.0',
25 | 'reference' => 'df1e7fde177501eee2037dd159cf04f5f301a512',
26 | 'type' => 'library',
27 | 'install_path' => __DIR__ . '/../paragonie/constant_time_encoding',
28 | 'aliases' => array(),
29 | 'dev_requirement' => false,
30 | ),
31 | 'psr/clock' => array(
32 | 'pretty_version' => '1.0.0',
33 | 'version' => '1.0.0.0',
34 | 'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
35 | 'type' => 'library',
36 | 'install_path' => __DIR__ . '/../psr/clock',
37 | 'aliases' => array(),
38 | 'dev_requirement' => false,
39 | ),
40 | 'spomky-labs/otphp' => array(
41 | 'pretty_version' => '11.3.0',
42 | 'version' => '11.3.0.0',
43 | 'reference' => '2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33',
44 | 'type' => 'library',
45 | 'install_path' => __DIR__ . '/../spomky-labs/otphp',
46 | 'aliases' => array(),
47 | 'dev_requirement' => false,
48 | ),
49 | 'symfony/deprecation-contracts' => array(
50 | 'pretty_version' => 'v3.5.0',
51 | 'version' => '3.5.0.0',
52 | 'reference' => '0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1',
53 | 'type' => 'library',
54 | 'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
55 | 'aliases' => array(),
56 | 'dev_requirement' => false,
57 | ),
58 | ),
59 | );
60 |
--------------------------------------------------------------------------------
/lib/console/2factor_auth/user.php:
--------------------------------------------------------------------------------
1 | setDescription('Deaktivates a 2factor_auth for a user')
21 | ->addArgument('user', InputArgument::REQUIRED, 'Username')
22 | ->addOption('disable', 'd', InputOption::VALUE_NONE, 'Disable')
23 | ->addOption('enable', 'e', InputOption::VALUE_NONE, 'Enable')
24 | ;
25 | }
26 |
27 | protected function execute(InputInterface $input, OutputInterface $output)
28 | {
29 | $io = $this->getStyle($input, $output);
30 |
31 | $username = $input->getArgument('user');
32 |
33 | $user = rex_sql::factory();
34 | $user
35 | ->setTable(rex::getTable('user'))
36 | ->setWhere(['login' => $username])
37 | ->select();
38 |
39 | if (1 != $user->getRows()) {
40 | throw new InvalidArgumentException(sprintf('User "%s" does not exist.', $username));
41 | }
42 |
43 | $user = rex_user::fromSql($user);
44 | $config = one_time_password_config::forUser($user);
45 |
46 | $io->info(
47 | 'User found: ' . $user->getLogin() .
48 | "\n" . 'Method: ' . $config->method .
49 | "\n" . 'Status: ' . ($config->enabled ? 'enabled' : 'disabled'));
50 |
51 | $enable = $input->getOption('enable');
52 | $disable = $input->getOption('disable');
53 |
54 | if ($enable && $disable) {
55 | $io->warning('Please decide: (--enable) or (--disable) for disabling 2factor_auth');
56 | return 0;
57 | }
58 |
59 | if ($enable) {
60 | $config->enable();
61 | $io->success('2factor_auth for User `' . $user->getLogin() . '` has been enabled');
62 | }
63 |
64 | if ($disable) {
65 | $config->disable();
66 | $io->success('2factor_auth for User `' . $user->getLogin() . '` has been disabled');
67 | }
68 |
69 | return 0;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/console/2factor_auth/enforce.php:
--------------------------------------------------------------------------------
1 | setDescription('Enforce/Disable a 2factor_auth for users/admins or admins')
19 | ->addOption('all', null, InputOption::VALUE_NONE, 'All')
20 | ->addOption('admins', null, InputOption::VALUE_NONE, 'Admins only')
21 | ->addOption('disable', 'd', InputOption::VALUE_NONE, 'Disable')
22 | ;
23 | }
24 |
25 | protected function execute(InputInterface $input, OutputInterface $output)
26 | {
27 | $io = $this->getStyle($input, $output);
28 |
29 | $all = $input->getOption('all');
30 | $admins = $input->getOption('admins');
31 | $disable = $input->getOption('disable');
32 |
33 | $opt = one_time_password::getInstance();
34 | $status = '';
35 | switch ($opt->isEnforced()) {
36 | case one_time_password::ENFORCED_ALL:
37 | $status = 'all';
38 | break;
39 | case one_time_password::ENFORCED_ADMINS:
40 | $status = 'admins';
41 | break;
42 | case one_time_password::ENFORCED_DISABLED:
43 | $status = 'disabled';
44 | break;
45 | }
46 |
47 | if ((!$all && !$admins && !$disable) || ($disable && ($all || $admins)) || ($all && $admins)) {
48 | $io->info('Please decide: (--all) for all, (--admins) for admins or (--disable) without admin or all for disabling 2factor_auth enforcement.
49 | Current Status: ' . $status);
50 | return 0;
51 | }
52 |
53 | if ($all) {
54 | $value = one_time_password::ENFORCED_ALL;
55 | $io->success('2factor_auth is now enforced for all users');
56 | } elseif ($admins) {
57 | $value = one_time_password::ENFORCED_ADMINS;
58 | $io->success('2factor_auth is now enforced for admins only');
59 | } else {
60 | $value = one_time_password::ENFORCED_DISABLED;
61 | $io->success('Enforcement of 2factor_auth for all has been disabled');
62 | }
63 | $opt->enforce($value);
64 |
65 | return 0;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 - 2022 Paragon Initiative Enterprises
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | ------------------------------------------------------------------------------
24 | This library was based on the work of Steve "Sc00bz" Thomas.
25 | ------------------------------------------------------------------------------
26 |
27 | The MIT License (MIT)
28 |
29 | Copyright (c) 2014 Steve Thomas
30 |
31 | Permission is hereby granted, free of charge, to any person obtaining a copy
32 | of this software and associated documentation files (the "Software"), to deal
33 | in the Software without restriction, including without limitation the rights
34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35 | copies of the Software, and to permit persons to whom the Software is
36 | furnished to do so, subject to the following conditions:
37 |
38 | The above copyright notice and this permission notice shall be included in all
39 | copies or substantial portions of the Software.
40 |
41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47 | SOFTWARE.
48 |
49 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php:
--------------------------------------------------------------------------------
1 | 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45
52 | $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45);
53 |
54 | // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52
55 | $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52);
56 |
57 | // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58
58 | $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58);
59 |
60 | return $ret;
61 | }
62 |
63 | /**
64 | * Uses bitwise operators instead of table-lookups to turn 8-bit integers
65 | * into 6-bit integers.
66 | *
67 | * @param int $src
68 | * @return string
69 | */
70 | protected static function encode6Bits(int $src): string
71 | {
72 | $src += 0x2e;
73 |
74 | // if ($src > 0x39) $src += 0x41 - 0x3a; // 7
75 | $src += ((0x39 - $src) >> 8) & 7;
76 |
77 | // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
78 | $src += ((0x5a - $src) >> 8) & 6;
79 |
80 | return \pack('C', $src);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/fragments/2fa.login.php:
--------------------------------------------------------------------------------
1 |
58 |
59 |
64 |
65 | parse('core/login_background.php');
68 | ?>
69 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Binary.php:
--------------------------------------------------------------------------------
1 | $query
22 | */
23 | public function __construct(
24 | private readonly string $scheme,
25 | private readonly string $host,
26 | private readonly string $path,
27 | private readonly string $secret,
28 | private readonly array $query
29 | ) {
30 | }
31 |
32 | /**
33 | * @return non-empty-string
34 | */
35 | public function getScheme(): string
36 | {
37 | return $this->scheme;
38 | }
39 |
40 | /**
41 | * @return non-empty-string
42 | */
43 | public function getHost(): string
44 | {
45 | return $this->host;
46 | }
47 |
48 | /**
49 | * @return non-empty-string
50 | */
51 | public function getPath(): string
52 | {
53 | return $this->path;
54 | }
55 |
56 | /**
57 | * @return non-empty-string
58 | */
59 | public function getSecret(): string
60 | {
61 | return $this->secret;
62 | }
63 |
64 | /**
65 | * @return array
66 | */
67 | public function getQuery(): array
68 | {
69 | return $this->query;
70 | }
71 |
72 | /**
73 | * @param non-empty-string $uri
74 | */
75 | public static function fromString(string $uri): self
76 | {
77 | $parsed_url = parse_url($uri);
78 | $parsed_url !== false || throw new InvalidArgumentException('Invalid URI.');
79 | foreach (['scheme', 'host', 'path', 'query'] as $key) {
80 | array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(
81 | 'Not a valid OTP provisioning URI'
82 | );
83 | }
84 | $scheme = $parsed_url['scheme'] ?? null;
85 | $host = $parsed_url['host'] ?? null;
86 | $path = $parsed_url['path'] ?? null;
87 | $query = $parsed_url['query'] ?? null;
88 | $scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI');
89 | is_string($host) || throw new InvalidArgumentException('Invalid URI.');
90 | is_string($path) || throw new InvalidArgumentException('Invalid URI.');
91 | is_string($query) || throw new InvalidArgumentException('Invalid URI.');
92 | $parsedQuery = [];
93 | parse_str($query, $parsedQuery);
94 | array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException(
95 | 'Not a valid OTP provisioning URI'
96 | );
97 | $secret = $parsedQuery['secret'];
98 | unset($parsedQuery['secret']);
99 |
100 | return new self($scheme, $host, $path, $secret, $parsedQuery);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/lib/one_time_password.php:
--------------------------------------------------------------------------------
1 | provisioningUri);
36 |
37 | $this->getMethod()->challenge($uri, $user);
38 | }
39 |
40 | /**
41 | * @param string $otp
42 | * @return bool
43 | */
44 | public function verify($otp)
45 | {
46 | $uri = str_replace('&', '&', (string) one_time_password_config::forCurrentUser()->provisioningUri);
47 |
48 | $verified = $this->getMethod()->verify($uri, $otp);
49 |
50 | if ($verified) {
51 | rex_set_session('otp_verified', true);
52 | }
53 |
54 | return $verified;
55 | }
56 |
57 | /**
58 | * @return bool
59 | */
60 | public function isVerified()
61 | {
62 | return rex_session('otp_verified', 'boolean', false);
63 | }
64 |
65 | /**
66 | * @return bool
67 | */
68 | public function isEnabled()
69 | {
70 | return one_time_password_config::forCurrentUser()->enabled;
71 | }
72 |
73 | /**
74 | * @param self::ENFORCE* $enforce
75 | *
76 | * @return void
77 | */
78 | public function enforce($enforce)
79 | {
80 | rex_config::set('2factor_auth', 'enforce', $enforce);
81 | }
82 |
83 | /**
84 | * @return self::ENFORCE*
85 | */
86 | public function isEnforced()
87 | {
88 | return rex_config::get('2factor_auth', 'enforce', self::ENFORCED_DISABLED);
89 | }
90 |
91 | /**
92 | * @return self::OPTION*
93 | */
94 | public function getAuthOption()
95 | {
96 | return rex_config::get('2factor_auth', 'option', self::OPTION_ALL);
97 | }
98 |
99 | public function setAuthOption(string $option): void
100 | {
101 | rex_config::set('2factor_auth', 'option', $option);
102 | }
103 |
104 | /**
105 | * @return method_interface
106 | */
107 | public function getMethod()
108 | {
109 | if (null === $this->method) {
110 | $methodType = one_time_password_config::forCurrentUser()->method;
111 |
112 | if ('totp' === $methodType) {
113 | $this->method = new method_totp();
114 | } elseif ('email' === $methodType) {
115 | $this->method = new method_email();
116 | } else {
117 | throw new InvalidArgumentException("Unknown method: $methodType");
118 | }
119 | }
120 |
121 | return $this->method;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php:
--------------------------------------------------------------------------------
1 | 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45
52 | $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45);
53 |
54 | // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62
55 | $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62);
56 |
57 | // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68
58 | $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68);
59 |
60 | // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7
61 | $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7);
62 |
63 | return $ret;
64 | }
65 |
66 | /**
67 | * Uses bitwise operators instead of table-lookups to turn 8-bit integers
68 | * into 6-bit integers.
69 | *
70 | * @param int $src
71 | * @return string
72 | */
73 | protected static function encode6Bits(int $src): string
74 | {
75 | $src += 0x2e;
76 |
77 | // if ($src > 0x2f) $src += 0x41 - 0x30; // 17
78 | $src += ((0x2f - $src) >> 8) & 17;
79 |
80 | // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
81 | $src += ((0x5a - $src) >> 8) & 6;
82 |
83 | // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75
84 | $src -= ((0x7a - $src) >> 8) & 75;
85 |
86 | return \pack('C', $src);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/fragments/2fa.setup-totp.php:
--------------------------------------------------------------------------------
1 |
3 |
4 | message) : ?>
5 | = $this->message ?>
6 |
7 |
8 | success) : ?>
9 |
10 |
11 |
12 |
13 |
1. = $this->addon->i18n('2fa_setup_scan') ?>
14 |
15 |
16 |
17 |
18 |
19 |
29 |
Copied to clipboard
30 |
31 |
32 |
33 |
34 |
35 |
36 |
2. = $this->addon->i18n('2fa_verify_headline') ?>
37 |
38 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
81 |
--------------------------------------------------------------------------------
/lib/method_email.php:
--------------------------------------------------------------------------------
1 | at(time());
24 |
25 | $mail->addAddress($user->getEmail());
26 | $mail->Subject = '2FA-Code: ' . rex::getServerName() . ' (' . $_SERVER['HTTP_HOST'] . ')';
27 | $mail->isHTML();
28 | $mail->Body = '' . rex::getServerName() . ' Login verification
' . $otpCode . '
is your 2 factor authentication code.';
29 | $mail->AltBody = rex::getServerName() . " Login verification \r\n ------------------ \r\n" . $otpCode . "\r\n ------------------ \r\nis your 2 factor authentication code.";
30 | if (!$mail->send()) {
31 | throw new exception(rex_i18n::msg('2factor_auth_mail_error'));
32 | }
33 | }
34 |
35 | public static function getPeriod(): int
36 | {
37 | return (int) rex_addon::get('2factor_auth')->getConfig('email_period', 300);
38 | }
39 |
40 | public static function getloginTries(): int
41 | {
42 | return 10;
43 | }
44 |
45 | public function verify(string $provisioningUrl, string $otp): bool
46 | {
47 | $TOTP = Factory::loadFromProvisioningUri($provisioningUrl);
48 |
49 | // re-create from an existant uri
50 | if ($TOTP->verify($otp)) {
51 | return true;
52 | }
53 |
54 | $lastOTPCode = $TOTP->at(time() - self::getPeriod());
55 | if ($lastOTPCode == $otp) {
56 | return Factory::loadFromProvisioningUri($provisioningUrl)->verify($TOTP->at(time()));
57 | }
58 |
59 | // TODO: Secureproblem
60 | // Unendliche Codeversuche im Moment möglich
61 |
62 | // - was mache ich bei mehreren Fehleingaben? Im Moment ist das unsicher
63 | // - wie mache ich es, wenn es Änderungen bei den Einstellungen gibt?
64 | // - solle alle E-Mail provisionalURLs neu generiert werden
65 |
66 | // gitlab - account wird gelockt nach 10 fehlversuchen
67 | // nach 10 minuten darf man dann wieder loslegen
68 |
69 | // rex_user
70 | // lasttrydate
71 | // lastlogin
72 | // backend_login_policy:
73 | // login_tries_until_blocked: 50
74 | // login_tries_until_delay: 3
75 | // relogin_delay: 5
76 |
77 | return false;
78 | }
79 |
80 | public function getProvisioningUri(rex_user $user): string
81 | {
82 | // create a uri with a random secret
83 | $otp = TOTP::create(null, self::getPeriod());
84 |
85 | // the label rendered in "Google Authenticator" or similar app
86 | $label = $user->getLogin() . '@' . rex::getServerName() . ' (' . $_SERVER['HTTP_HOST'] . ')';
87 | $label = str_replace(':', '_', $label); // colon is forbidden
88 | $otp->setLabel($label);
89 | $otp->setParameter('period', self::getPeriod());
90 | $otp->setIssuer(str_replace(':', '_', $user->getLogin()));
91 |
92 | return $otp->getProvisioningUri();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php:
--------------------------------------------------------------------------------
1 | 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
53 | $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
54 |
55 | // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
56 | $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
57 |
58 | // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
59 | $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
60 |
61 | // if ($src == 0x2c) $ret += 62 + 1;
62 | $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;
63 |
64 | // if ($src == 0x5f) ret += 63 + 1;
65 | $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64;
66 |
67 | return $ret;
68 | }
69 |
70 | /**
71 | * Uses bitwise operators instead of table-lookups to turn 8-bit integers
72 | * into 6-bit integers.
73 | *
74 | * @param int $src
75 | * @return string
76 | */
77 | protected static function encode6Bits(int $src): string
78 | {
79 | $diff = 0x41;
80 |
81 | // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
82 | $diff += ((25 - $src) >> 8) & 6;
83 |
84 | // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
85 | $diff -= ((51 - $src) >> 8) & 75;
86 |
87 | // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13
88 | $diff -= ((61 - $src) >> 8) & 13;
89 |
90 | // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3
91 | $diff += ((62 - $src) >> 8) & 49;
92 |
93 | return \pack('C', $src + $diff);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/Factory.php:
--------------------------------------------------------------------------------
1 | getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.');
25 | } catch (Throwable $throwable) {
26 | throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);
27 | }
28 | if ($clock === null) {
29 | trigger_deprecation(
30 | 'spomky-labs/otphp',
31 | '11.3.0',
32 | 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
33 | );
34 | $clock = new InternalClock();
35 | }
36 |
37 | $otp = self::createOTP($parsed_url, $clock);
38 |
39 | self::populateOTP($otp, $parsed_url);
40 |
41 | return $otp;
42 | }
43 |
44 | private static function populateParameters(OTPInterface $otp, Url $data): void
45 | {
46 | foreach ($data->getQuery() as $key => $value) {
47 | $otp->setParameter($key, $value);
48 | }
49 | }
50 |
51 | private static function populateOTP(OTPInterface $otp, Url $data): void
52 | {
53 | self::populateParameters($otp, $data);
54 | $result = explode(':', rawurldecode(mb_substr($data->getPath(), 1)));
55 |
56 | if (count($result) < 2) {
57 | $otp->setIssuerIncludedAsParameter(false);
58 |
59 | return;
60 | }
61 |
62 | if ($otp->getIssuer() !== null) {
63 | $result[0] === $otp->getIssuer() || throw new InvalidArgumentException(
64 | 'Invalid OTP: invalid issuer in parameter'
65 | );
66 | $otp->setIssuerIncludedAsParameter(true);
67 | }
68 |
69 | assert($result[0] !== '');
70 |
71 | $otp->setIssuer($result[0]);
72 | }
73 |
74 | private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface
75 | {
76 | switch ($parsed_url->getHost()) {
77 | case 'totp':
78 | $totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock);
79 | $totp->setLabel(self::getLabel($parsed_url->getPath()));
80 |
81 | return $totp;
82 | case 'hotp':
83 | $hotp = HOTP::createFromSecret($parsed_url->getSecret());
84 | $hotp->setLabel(self::getLabel($parsed_url->getPath()));
85 |
86 | return $hotp;
87 | default:
88 | throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost()));
89 | }
90 | }
91 |
92 | /**
93 | * @param non-empty-string $data
94 | * @return non-empty-string
95 | */
96 | private static function getLabel(string $data): string
97 | {
98 | $result = explode(':', rawurldecode(mb_substr($data, 1)));
99 | $label = count($result) === 2 ? $result[1] : $result[0];
100 | assert($label !== '');
101 |
102 | return $label;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/README.md:
--------------------------------------------------------------------------------
1 | # Constant-Time Encoding
2 |
3 | [](https://github.com/paragonie/constant_time_encoding/actions)
4 | [](https://github.com/paragonie/constant_time_encoding/actions)
5 | [](https://packagist.org/packages/paragonie/constant_time_encoding)
6 | [](https://packagist.org/packages/paragonie/constant_time_encoding)
7 | [](https://packagist.org/packages/paragonie/constant_time_encoding)
8 | [](https://packagist.org/packages/paragonie/constant_time_encoding)
9 |
10 | Based on the [constant-time base64 implementation made by Steve "Sc00bz" Thomas](https://github.com/Sc00bz/ConstTimeEncoding),
11 | this library aims to offer character encoding functions that do not leak
12 | information about what you are encoding/decoding via processor cache
13 | misses. Further reading on [cache-timing attacks](http://blog.ircmaxell.com/2014/11/its-all-about-time.html).
14 |
15 | Our fork offers the following enhancements:
16 |
17 | * `mbstring.func_overload` resistance
18 | * Unit tests
19 | * Composer- and Packagist-ready
20 | * Base16 encoding
21 | * Base32 encoding
22 | * Uses `pack()` and `unpack()` instead of `chr()` and `ord()`
23 |
24 | ## PHP Version Requirements
25 |
26 | Version 3 of this library should work on **PHP 8** or newer.
27 |
28 | Version 2 of this library should work on **PHP 7** or newer. See [the v2.x branch](https://github.com/paragonie/constant_time_encoding/tree/v2.x).
29 |
30 | For PHP 5 support, see [the v1.x branch](https://github.com/paragonie/constant_time_encoding/tree/v1.x).
31 |
32 | If you are adding this as a dependency to a project intended to work on PHP 5 through 8.4, please set the required version to `^1|^2|^3`.
33 |
34 | ## How to Install
35 |
36 | ```sh
37 | composer require paragonie/constant_time_encoding
38 | ```
39 |
40 | ## How to Use
41 |
42 | ```php
43 | use ParagonIE\ConstantTime\Encoding;
44 |
45 | // possibly (if applicable):
46 | // require 'vendor/autoload.php';
47 |
48 | $data = random_bytes(32);
49 | echo Encoding::base64Encode($data), "\n";
50 | echo Encoding::base32EncodeUpper($data), "\n";
51 | echo Encoding::base32Encode($data), "\n";
52 | echo Encoding::hexEncode($data), "\n";
53 | echo Encoding::hexEncodeUpper($data), "\n";
54 | ```
55 |
56 | Example output:
57 |
58 | ```
59 | 1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE=
60 | 2VMKKPSHSWVCVZJ6E7SONRY3ZXCNG3GE6ZZFU7TGJSX7KUKFNLAQ====
61 | 2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq====
62 | d558a53e4795aa2ae53e27e4e6c71bcdc4d36cc4f6725a7e664caff551456ac1
63 | D558A53E4795AA2AE53E27E4E6C71BDCC4D36CC4F6725A7E664CAFF551456AC1
64 | ```
65 |
66 | If you only need a particular variant, you can just reference the
67 | required class like so:
68 |
69 | ```php
70 | use ParagonIE\ConstantTime\Base64;
71 | use ParagonIE\ConstantTime\Base32;
72 |
73 | $data = random_bytes(32);
74 | echo Base64::encode($data), "\n";
75 | echo Base32::encode($data), "\n";
76 | ```
77 |
78 | Example output:
79 |
80 | ```
81 | 1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE=
82 | 2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq====
83 | ```
84 |
85 | ## Support Contracts
86 |
87 | If your company uses this library in their products or services, you may be
88 | interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise).
89 |
--------------------------------------------------------------------------------
/pages/verify.php:
--------------------------------------------------------------------------------
1 | getMethod();
16 |
17 | if (!isset($otp)) {
18 | try {
19 | one_time_password::getInstance()->challenge();
20 | } catch (Exception $e) {
21 | $error = true;
22 | $error_messages[] = $e->getMessage();
23 | }
24 | }
25 |
26 | switch (get_class($Method)) {
27 | case 'FriendsOfREDAXO\TwoFactorAuth\method_email':
28 | if (!isset($otp)) {
29 | $info_messages[] = rex_i18n::msg('2factor_auth_2fa_info_email_sent');
30 | }
31 | $info_messages[] = rex_i18n::msg('2factor_auth_2fa_info_email_enter_code');
32 | $blockTime = method_email::getPeriod();
33 | $loginTriesAllowed = method_email::getloginTries();
34 | break;
35 | default:
36 | case 'FriendsOfREDAXO\TwoFactorAuth\method_totp':
37 | $info_messages[] = rex_i18n::msg('2factor_auth_2fa_info_topt_enter_code');
38 | $blockTime = method_totp::getPeriod();
39 | $loginTriesAllowed = method_totp::getloginTries();
40 | break;
41 | }
42 |
43 | /** @var rex_user $user */
44 | $user = rex::getUser();
45 | $loginTries = (int) $user->getValue('one_time_password_tries');
46 | $loginLastTry = (int) $user->getValue('one_time_password_lasttry');
47 |
48 | $SQLUser = rex_sql::factory();
49 | $SQLUser->setTable(rex::getTable('user'));
50 | $SQLUser->setWhere('id = :id', ['id' => $user->getId()]);
51 |
52 | if (isset($otp) && !$csrfToken->isValid()) {
53 | $error_messages[] = rex_i18n::msg('csrf_token_invalid');
54 | } elseif ($loginTries >= $loginTriesAllowed && ($loginLastTry > time() - $blockTime)) {
55 | $countdownTime = $loginLastTry - time() + $blockTime;
56 | $error_messages[] = rex_i18n::rawMsg('one_time_password_too_many_tries', $countdownTime, $loginTriesAllowed, $blockTime);
57 |
58 | $script = '
59 |
73 | ';
74 | } elseif (isset($otp) && '' == $otp) {
75 | // $error_messages[] = rex_i18n::msg('2fa_otp_empty');
76 | } elseif (isset($otp)) {
77 |
78 |
79 | if ($OTPInstance->verify($otp)) {
80 | $SQLUser->setValue('one_time_password_tries', 0);
81 | $SQLUser->setValue('one_time_password_lasttry', time());
82 | $SQLUser->update();
83 |
84 | $message = rex_view::success('Passt');
85 | // symbolischer parameter, der nirgends ausgewertet werden sollte/darf.
86 | rex_response::sendRedirect('?ok');
87 | } else {
88 | $SQLUser->setValue('one_time_password_tries', $loginTries + 1);
89 | $SQLUser->setValue('one_time_password_lasttry', time());
90 | $SQLUser->update();
91 |
92 | $error_messages[] = $this->i18n('2fa_otp_wrong');
93 | }
94 | }
95 |
96 | $fragment = new rex_fragment();
97 | $fragment->setVar('csrfToken', $csrfToken, false);
98 | $fragment->setVar('info_messages', implode('
', $info_messages), false);
99 | $fragment->setVar('error_messages', implode('
', $error_messages), false);
100 | echo $fragment->parse('2fa.login.php');
101 | echo $script ?? '';
102 |
103 | ?>
104 |
105 |
106 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Base32Hex.php:
--------------------------------------------------------------------------------
1 | 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
48 | $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
49 |
50 | // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86
51 | $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86);
52 |
53 | return $ret;
54 | }
55 |
56 | /**
57 | * Uses bitwise operators instead of table-lookups to turn 5-bit integers
58 | * into 8-bit integers.
59 | *
60 | * @param int $src
61 | * @return int
62 | */
63 | protected static function decode5BitsUpper(int $src): int
64 | {
65 | $ret = -1;
66 |
67 | // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
68 | $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
69 |
70 | // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54
71 | $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54);
72 |
73 | return $ret;
74 | }
75 |
76 | /**
77 | * Uses bitwise operators instead of table-lookups to turn 8-bit integers
78 | * into 5-bit integers.
79 | *
80 | * @param int $src
81 | * @return string
82 | */
83 | protected static function encode5Bits(int $src): string
84 | {
85 | $src += 0x30;
86 |
87 | // if ($src > 0x39) $src += 0x61 - 0x3a; // 39
88 | $src += ((0x39 - $src) >> 8) & 39;
89 |
90 | return \pack('C', $src);
91 | }
92 |
93 | /**
94 | * Uses bitwise operators instead of table-lookups to turn 8-bit integers
95 | * into 5-bit integers.
96 | *
97 | * Uppercase variant.
98 | *
99 | * @param int $src
100 | * @return string
101 | */
102 | protected static function encode5BitsUpper(int $src): string
103 | {
104 | $src += 0x30;
105 |
106 | // if ($src > 0x39) $src += 0x41 - 0x3a; // 7
107 | $src += ((0x39 - $src) >> 8) & 7;
108 |
109 | return \pack('C', $src);
110 | }
111 | }
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/HOTP.php:
--------------------------------------------------------------------------------
1 | setCounter($counter);
28 | $htop->setDigest($digest);
29 | $htop->setDigits($digits);
30 |
31 | return $htop;
32 | }
33 |
34 | public static function createFromSecret(string $secret): self
35 | {
36 | $htop = new self($secret);
37 | $htop->setCounter(self::DEFAULT_COUNTER);
38 | $htop->setDigest(self::DEFAULT_DIGEST);
39 | $htop->setDigits(self::DEFAULT_DIGITS);
40 |
41 | return $htop;
42 | }
43 |
44 | public static function generate(): self
45 | {
46 | return self::createFromSecret(self::generateSecret());
47 | }
48 |
49 | /**
50 | * @return 0|positive-int
51 | */
52 | public function getCounter(): int
53 | {
54 | $value = $this->getParameter('counter');
55 | (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "counter" parameter.');
56 |
57 | return $value;
58 | }
59 |
60 | public function getProvisioningUri(): string
61 | {
62 | return $this->generateURI('hotp', [
63 | 'counter' => $this->getCounter(),
64 | ]);
65 | }
66 |
67 | /**
68 | * If the counter is not provided, the OTP is verified at the actual counter.
69 | *
70 | * @param null|0|positive-int $counter
71 | */
72 | public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
73 | {
74 | $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');
75 |
76 | if ($counter === null) {
77 | $counter = $this->getCounter();
78 | } elseif ($counter < $this->getCounter()) {
79 | return false;
80 | }
81 |
82 | return $this->verifyOtpWithWindow($otp, $counter, $window);
83 | }
84 |
85 | public function setCounter(int $counter): void
86 | {
87 | $this->setParameter('counter', $counter);
88 | }
89 |
90 | /**
91 | * @return array
92 | */
93 | protected function getParameterMap(): array
94 | {
95 | return [...parent::getParameterMap(), ...[
96 | 'counter' => static function (mixed $value): int {
97 | $value = (int) $value;
98 | $value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');
99 |
100 | return $value;
101 | },
102 | ]];
103 | }
104 |
105 | private function updateCounter(int $counter): void
106 | {
107 | $this->setCounter($counter);
108 | }
109 |
110 | /**
111 | * @param null|0|positive-int $window
112 | */
113 | private function getWindow(null|int $window): int
114 | {
115 | return abs($window ?? self::DEFAULT_WINDOW);
116 | }
117 |
118 | /**
119 | * @param non-empty-string $otp
120 | * @param 0|positive-int $counter
121 | * @param null|0|positive-int $window
122 | */
123 | private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool
124 | {
125 | $window = $this->getWindow($window);
126 |
127 | for ($i = $counter; $i <= $counter + $window; ++$i) {
128 | if ($this->compareOTP($this->at($i), $otp)) {
129 | $this->updateCounter($i + 1);
130 |
131 | return true;
132 | }
133 | }
134 |
135 | return false;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/OTPInterface.php:
--------------------------------------------------------------------------------
1 |
110 | */
111 | public function getParameters(): array;
112 |
113 | /**
114 | * @param non-empty-string $parameter
115 | */
116 | public function setParameter(string $parameter, mixed $value): void;
117 |
118 | /**
119 | * Get the provisioning URI.
120 | *
121 | * @return non-empty-string
122 | */
123 | public function getProvisioningUri(): string;
124 |
125 | /**
126 | * Get the provisioning URI.
127 | *
128 | * @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.
129 | * @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI
130 | */
131 | public function getQrCodeUri(string $uri, string $placeholder): string;
132 | }
133 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- |----------------------------------------|
7 | | 11.0.x | :white_check_mark: |
8 | | 10.0.x | :white_check_mark: (security fix only) |
9 | | < 10.0 | :x: |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | Please email `security@spomky-labs.com`.
14 | If deemed necessary, you can encrypt your message using one of the following GPG key
15 |
16 | ```
17 | -----BEGIN PGP PUBLIC KEY BLOCK-----
18 | xjMEXTsJVxYJKwYBBAHaRw8BAQdAZCS93eHRx97V+LQbAWuAaeKIdUZ9YIkn
19 | QH5pQ7dDU0TNMWNvbnRhY3RAc3BvbWt5LWxhYnMuY29tIDxjb250YWN0QHNw
20 | b21reS1sYWJzLmNvbT7CdwQQFgoAHwUCXTsJVwYLCQcIAwIEFQgKAgMWAgEC
21 | GQECGwMCHgEACgkQG6hbCDSDj+1/tgEAoy11uHvDV7kkG/iN2/0ylV72hU8y
22 | c/xoqGd7qFaKD6ABANcthlg63OrQVTf0dUPOT9Y2BJpOOA88JJWgILtuUPIO
23 | zjgEXTsJVxIKKwYBBAGXVQEFAQEHQKiX7nldkmICePhzwReZnBPmjpsmNt7V
24 | Y8xHdICKsr8cAwEIB8JhBBgWCAAJBQJdOwlXAhsMAAoJEBuoWwg0g4/t0KgA
25 | /31ucb/bL/MGpWFrpSjTs6uQhZWlBmcFoeMhwCYepIpZAQDd65UBqFDKXJWv
26 | Xy3zoMQQzD9Z6fUATnFrWkzjHwhvDQ==
27 | =j4dw
28 | -----END PGP PUBLIC KEY BLOCK-----
29 | ```
30 |
31 |
32 | ```
33 | -----BEGIN PGP PUBLIC KEY BLOCK-----
34 | xsFNBGILZFoBEADo9pzAMRVxL5typ22Ywifdyi3CMHgg7zptfb8otrQci8IX
35 | m7B8/NTA0I9EkenzSW/Mf4k2iPNCwXc+qVEHPvPNvr3WazcdiDQJjXqMtkxG
36 | l2dvdQHdBxN46v+mvWDVGf9anYQxIAmZrj7CDLOfD/cG/8STL4hSbFjRBOKs
37 | xAP8wgRA/amcrf9WcCDxURGIq8mDPcECR8fca+iukTmMe2NDEc56pJi0KVoF
38 | pFhOMMfjgP/XvtGjjSNZNGRgHSLTQs8UiK+5BjPh+iWFIPV5+ZPLpbSOcoma
39 | GyeX5i1DmAh7cWx/FphvFzOun6to3ERuy82+zW54iA9zS8+kIfV4Wjr2qE7l
40 | Ctc9l8RIv/6dMXoW2Y42CTuywlAMnlP7XaaUgE++CXTIuO7+6Gp0E5NlmqB5
41 | lb+CZLV/LS27gUcajs23ve5B3UId2bGUflvTtY/J0VPzrJMoEErVnkCsnD7W
42 | Oiwe8GiSNMJmTGu/A45xf5nuYNcuU7blA5XXwPoHZuALj1zv6eCWVxWz02l9
43 | Fc/T+gNkOEErlXOcldyXxQ5Qb99TU5NgdqzbibyR9QAqdfwtgg19oFbiSP7t
44 | 8b5P2qAIW2GaOCkX007cBCzTXNrcQNruTwUD59LZQLhdGz5WJo/gefC/3ZvR
45 | vKoJKCRlk7s43aUjeZzE+Engpr5e1wl63WjAzQARAQABzTNzZWN1cml0eUBz
46 | cG9ta3ktbGFicy5jb20gPHNlY3VyaXR5QHNwb21reS1sYWJzLmNvbT7CwY0E
47 | EAEIACAFAmILZFoGCwkHCAMCBBUICgIEFgIBAAIZAQIbAwIeAQAhCRBy14gx
48 | FHv4aBYhBKgF8zJv89FYVv0RFHLXiDEUe/hoA+YP/ijaePtilKURzNVrPWfc
49 | gDw/ZNCR+dVAgwGo9VcbOvkyZmyqD6yBjuDWvG96KQs0LRrqWKonAvnewNtp
50 | wQruuvrlcCuNE6TTfvx0wh2+lwKD7MH5dKutHUCowVNAsZ5uZxHVF9RGLBh+
51 | JRofklupcGqUx+Jtx4uq2gAGOqV4/QdvneMjkLwqVu8FGIM59LfdNfp/iA3p
52 | wX2DvfxBO58Gu6hilmf7R+b9nX0U7xYJM6QJb7H89cV3/AoTh2kf1wtFY+Py
53 | Di6VZTMUBYOoz2iSnvCE8KlBWDu98/A2EJ7kDGQdmnuIgsURsyap3yKioaUr
54 | LGTaG0OiC/gkXkKisH6eff6Gw06qelBarf5N/GgoeAN/amE8twy3a+Hx1pyw
55 | ZzkjPsL7uWg3Koy5mPuCtWfPtIBcJaTLS5d8ESlJ8/CfaVaDludzYQZo70Xn
56 | m4KzjPnptm3djpZNwoFEUxrHVREOEe69/MnEL2PNcEMQkapg16PnH4phajnC
57 | 7bYOPDteMJlHjNmQzz9d25ZwzVBHDDT50mHDijR2D/OgKx3NQr88fiFAWhKG
58 | lEu1ZuOkKIKV5VIFbocTWSoV7bkzIfrll49xWou+4VOxgRuqjquFC4RV8fea
59 | lLbHOcJlOR00aFDmoOWQ3/QNvajaWJFzDdocGbgbnEBMDFRoUkuhqOBcnzA+
60 | apW/zsFNBGILZFoBEADSwiM49wObRpxOyas91M6WvJ4Gt3iXqj+L8dmcw0FW
61 | UdDpwOxy8tuZx+OfXEBBH3eJHOobC66vN+E9WYobVkJ5zfbGxfQruTuvUZNl
62 | X9Lo0UwoP+AP21AKUUvsf48iZGWzmlkxgPnhAQS4ECkkWCKPf7nFTk+V+jIN
63 | nf6ZDZLXaRUnG0nLvzs0raG1eTVrGvPSCC8u3R2zIh9SvoeEgTnT/Re0mhCu
64 | ah3fwG+4vXc6VIjR1ZtpM9+Y8sl+PFZ/Oiisc+46oU5qXVVLtHfLdxYZ4vl2
65 | IflHDKKmrfbfGY1hJl/foBLglT3Cd8GTu3FjiAJX9PpkiWbsflc0OUBQf9aC
66 | 73W5FLS4P4clm4nNzVGkNucWHvk+urM6nEUf02bhsfF0TPeos3QcJorfKNUS
67 | TvuGYccENuK5cVOzEcU+VhN08GT0pr0CpqJnsw+zV8vD4k3aPmMFmSVog+bY
68 | NhfB7AgwbOjd6MhQJcP7YjYTHaa6YsnKMSg4RhkDjvMa3421hfaWsVvlIb0f
69 | AZJ8BnXgfE0uI8CKA9dc6I2Posl33zC8HI2sS1MEJ90Am68P+uJt61LdJeD5
70 | VXSrCkzBhUBds0hbGR6+DF20UD496m7Lw3VBoWOl2bMeLdERDarFMDYsPH47
71 | rie9wlrnPNR57HUqK4bpkFwqTStRkRFUhFv7LLWZ1QARAQABwsF2BBgBCAAJ
72 | BQJiC2RaAhsMACEJEHLXiDEUe/hoFiEEqAXzMm/z0VhW/REUcteIMRR7+GhI
73 | lQ/9GbSwIdGue6Gw0msYAEoER9HhpYB//9/GG7/c4ZW60nLSSYuhNWIo0Akl
74 | 10CzeApezf/O9/1EExqZ9ygj4wtUphcQOdRJVhXPt+gskw7/NHoXUJ+Z1rbb
75 | EWbKle9YufZ4PAKYhlxdqTlWyQvPVxrRvbuhYeQG4S412VzKjH0/x1Fh2CfV
76 | hFuyOaRjg89T6rihXL1rCSJ/PDQeQtvtXeJ30yFj+aapCj+VqUl+2D+N0bzS
77 | LL18kEPQnJw4BOHOXrw349dAKmHN/QkRH8DINlXLyaOlABglnSViDQL3Q1t3
78 | sBuIeClsl3brQNJRp/RKOdTBMNAX+BhAjqodbwwT+UkJl9xJKw0Cla4wtbs2
79 | T0yoK/Z1iFfvPdufkK4q6ocAHJUp3+XckFIZxsHQvhQPbm9XoOt1RTO29MOw
80 | EYo8UjFQCnXJVsj1/6XMgIUe5tPYvS/ZZZNJFF4j+OE8xRKLKqg/DFcpEipC
81 | LCmzzr/hhWx0XP4CIK2tYsAMk3ieCZuk1Wa+NGLL4WfALWsNHq3wg5Wzv+yJ
82 | dp14fv711BVYlriI+VKggGFgBdz0dWkgrBk4+thLatJFcjFYr8BLkbtPraa3
83 | sFI/cGxvOXSIy4GEALdfnozyU3RJtMNtVi3IzGeIFAOb457y/IrMqpWLp1FX
84 | BUqlX5YJHneD9Q8Sfz/HKDQDCqg=
85 | =o+4z
86 | -----END PGP PUBLIC KEY BLOCK-----
87 | ```
88 |
--------------------------------------------------------------------------------
/lib/one_time_password_config.php:
--------------------------------------------------------------------------------
1 | user = $user;
30 | }
31 |
32 | /**
33 | * @return self
34 | */
35 | public static function forCurrentUser()
36 | {
37 | return self::forUser(rex::getImpersonator() ?? rex::requireUser());
38 | }
39 |
40 | /**
41 | * @return self
42 | */
43 | public static function forUser(rex_user $user)
44 | {
45 | return self::fromJson($user->getValue('one_time_password_config'), $user);
46 | }
47 |
48 | public static function loadFromDb(method_interface $method, rex_user $user): self
49 | {
50 | // get non-cached values
51 | $userSql = rex_sql::factory();
52 | $userSql->setTable(rex::getTablePrefix() . 'user');
53 | $userSql->setWhere(['id' => $user->getId()]);
54 | $userSql->select();
55 |
56 | $json = (string) $userSql->getValue('one_time_password_config');
57 | $config = self::fromJson($json, $user);
58 | $config->init($method);
59 | return $config;
60 | }
61 |
62 | /**
63 | * @param string|null $json
64 | * @return self
65 | */
66 | private static function fromJson($json, rex_user $user)
67 | {
68 | if (is_string($json)) {
69 | $configArr = json_decode($json, true);
70 |
71 | if (is_array($configArr)) {
72 | // compat with older versions, which did not yet define a method
73 | if (!array_key_exists('method', $configArr)) {
74 | $configArr['method'] = 'totp';
75 | }
76 |
77 | $config = new self($user);
78 | $config->provisioningUri = $configArr['provisioningUri'];
79 | $config->enabled = $configArr['enabled'];
80 | $config->method = $configArr['method'];
81 | return $config;
82 | }
83 | }
84 |
85 | $default = new self($user);
86 | $default->init(new method_totp());
87 | return $default;
88 | }
89 |
90 | /**
91 | * @return void
92 | */
93 | private function init(method_interface $method)
94 | {
95 | $this->method = $method instanceof method_email ? 'email' : 'totp';
96 | if (null === $this->provisioningUri) {
97 | $this->provisioningUri = $method->getProvisioningUri($this->user);
98 | }
99 |
100 | $this->save();
101 | }
102 |
103 | /**
104 | * @return void
105 | */
106 | public function enable()
107 | {
108 | $this->enabled = true;
109 |
110 | if (null === $this->provisioningUri) {
111 | throw new exception('Missing provisioning url');
112 | }
113 | if (null === $this->method) {
114 | throw new exception('Missing method');
115 | }
116 |
117 | $this->save();
118 | }
119 |
120 | /**
121 | * @return void
122 | */
123 | public function disable()
124 | {
125 | $this->enabled = false;
126 | $this->provisioningUri = null;
127 |
128 | $this->save();
129 | }
130 |
131 | /**
132 | * @return void
133 | */
134 | public function updateMethod(method_interface $method)
135 | {
136 | $this->method = $method instanceof method_email ? 'email' : 'totp';
137 | $this->provisioningUri = $method->getProvisioningUri($this->user);
138 | $this->save();
139 | }
140 |
141 | /**
142 | * @return void
143 | */
144 | private function save()
145 | {
146 | $userSql = rex_sql::factory();
147 | $userSql->setTable(rex::getTablePrefix() . 'user');
148 | $userSql->setWhere(['id' => $this->user->getId()]);
149 | $userSql->setValue('one_time_password_config', json_encode(
150 | [
151 | 'provisioningUri' => $this->provisioningUri,
152 | 'method' => $this->method,
153 | 'enabled' => $this->enabled,
154 | ],
155 | ));
156 | $userSql->addGlobalUpdateFields();
157 | $userSql->update();
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 2-Faktor-Authentifizierung
2 |
3 | 2-Faktor-Authentifizierung mittels one-time-password (OTP).
4 | Mit diesem Addon wird der Login in das REDAXO CMS durch einen zweiten Authentifizierungsweg abgesichert.
5 |
6 | 
7 |
8 | ## Einstellungen
9 |
10 | Der Administrator hat die Möglichkeit unter `Einstellungen` innerhalb des AddOns die 2-Faktor-Authentifizierung für die Benutzer zu erzwingen.
11 | Alternativ kann die 2-Faktor-Authentifizierung als Optional gehandhabt werden. Die Authentifizierungsoptionen können eingeschränkt werden, z.B. Ausschließlich E-Mail-Authentifizierung zu erlauben
12 |
13 | ## Einrichtung Endbenutzer
14 |
15 | Als Authentifikator-Apps stehen alle Apps zur Verfügung, die den OTP-Standard einhalten, zum Beispiel:
16 |
17 | Umgebung | App | Hilfe
18 | ------------------------- |---------------------------| -----
19 | MacOS/Windows/Android/iOS | 1Password | Kurzanleitung:
20 | MacOS native | Native (ab Sequoia) | Kurzanleitung weiter unten – "2-Faktor-Authentifizierung mit MacOS Bordmitteln"
21 | iOS | FreeOTP | App: , Kurzanleitung:
22 | Android | FreeOTP | App:
23 | Android | Microsoft Authentificator | App:
24 | iOS | Microsoft Authentificator | App:
25 | iOS | Google Authentificator | App:
26 | Android | Google Authentificator | App:
27 | Android | 2FAS Authenticator | App:
28 | iOS | 2FAS Authenticator | App:
29 |
30 | Zunächst muss eine der Apps installiert sein, um die Einrichtung abzuschließen. Anschließend:
31 |
32 | 1. Im REDAXO-Backend > `2-Faktor-Login` öffnen.
33 | 2. Die 2-Faktor-Einrichtung aktivieren. Es wird ein QR-Code dargestellt.
34 | 3. Den QR-Code in der Authentifikator-App einlesen.
35 |
36 | > Hinweis: Manche OTP-Apps benötigen den manuellen Modus. Hierbei gilt: Name = `Name der Website`; Benutzer = `REDAXO-Benutzername`; Secret = `Secret-Schlüssel`
37 |
38 | Damit ist die Einrichtung abgeschlossen.
39 |
40 | ## Verwendung
41 |
42 | Nach der erfolgreichen Einrichtung wird jeder neue Login in das REDAXO-Backend durch ein zusätzliches, einmalig generiertes Passwort geschützt.
43 |
44 | 1. Den REDAXO-Backend-Login aufrufen.
45 | 2. Mit den üblichen Zugangsdaten einloggen. Es wird nach dem einmaligen Zugangscode gefragt.
46 | 3. Die Authentifikator-App öffnen und den Zugangscode generieren lassen.
47 | 4. Diesen Code eingeben und fortfahren.
48 |
49 | Anschließend ist man, wie gewohnt, im REDAXO-Backend eingeloggt.
50 |
51 | ## 2-Faktor-Authentifizierung mit MacOS Bordmitteln
52 |
53 | Seit macOS Sequoia lässt sich die 2-Faktor-Authentifizierung auch ohne Dritt-App einrichten.
54 |
55 | 1. In Redaxo startet man die TOTP-Einrichtung mittels Button "TOTP Einrichtung starten (erfordert App; empfohlen)". Das zeigt einem dann einen QR-Code sowie darunter eine Textzeile an, die mit "otpauth://" beginnt.
56 | 3. Diese Zeile kopiert man in die Zwischenablage und wechselt zur Passwords App in macOS
57 | 4. Dort sucht man das bestehende Login zu dieser Website (oder erstellt ein Neues, wenn noch nie gespeichert) und klickt auf "Edit" (Dt. wahrscheinlich "Bearbeiten") oben rechts.
58 | 5. Dann klickt man "Setup Code..."
59 | 6. und fügt die Zwischenablage in das Feld "Setup Key".
60 | 7. Dann klickt man "Use Setup Key" und bekommt eine 6-stellige Nummer, welche man auf der Website in das Feld "2. OTP-Code eingeben um die Einrichtung abzuschliessen" einfügt und durch Bestätigen aktiviert.
61 |
62 | ## Hinweise
63 |
64 | Bei E-Mail OTP kann man das Zeitinterval für die Gültigkeit des OTP-Codes einstellen. Sollte es bereits Benutzer geben, die ein OTP eingerichtet haben, so gilt das neue Zeitinterval für diese nicht.
65 |
66 | ## 💌 Give back some love
67 |
68 | [Consider supporting the project](https://github.com/sponsors/staabm), so we can make this tool even better even faster for everyone.
69 |
70 | ## Credits
71 |
72 | **Markus Staab**
73 | https://github.com/staabm
74 |
75 | **Jan Kristinus**
76 | https://github.com/dergel
77 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/OTP.php:
--------------------------------------------------------------------------------
1 | setSecret($secret);
29 | }
30 |
31 | public function getQrCodeUri(string $uri, string $placeholder): string
32 | {
33 | $provisioning_uri = urlencode($this->getProvisioningUri());
34 |
35 | return str_replace($placeholder, $provisioning_uri, $uri);
36 | }
37 |
38 | /**
39 | * @param 0|positive-int $input
40 | */
41 | public function at(int $input): string
42 | {
43 | return $this->generateOTP($input);
44 | }
45 |
46 | /**
47 | * @return non-empty-string
48 | */
49 | final protected static function generateSecret(): string
50 | {
51 | return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
52 | }
53 |
54 | /**
55 | * The OTP at the specified input.
56 | *
57 | * @param 0|positive-int $input
58 | *
59 | * @return non-empty-string
60 | */
61 | protected function generateOTP(int $input): string
62 | {
63 | $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
64 | $unpacked = unpack('C*', $hash);
65 | $unpacked !== false || throw new InvalidArgumentException('Invalid data.');
66 | $hmac = array_values($unpacked);
67 |
68 | $offset = ($hmac[count($hmac) - 1] & 0xF);
69 | $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
70 | $otp = $code % (10 ** $this->getDigits());
71 |
72 | return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);
73 | }
74 |
75 | /**
76 | * @param array $options
77 | */
78 | protected function filterOptions(array &$options): void
79 | {
80 | foreach ([
81 | 'algorithm' => 'sha1',
82 | 'period' => 30,
83 | 'digits' => 6,
84 | ] as $key => $default) {
85 | if (isset($options[$key]) && $default === $options[$key]) {
86 | unset($options[$key]);
87 | }
88 | }
89 |
90 | ksort($options);
91 | }
92 |
93 | /**
94 | * @param non-empty-string $type
95 | * @param array $options
96 | *
97 | * @return non-empty-string
98 | */
99 | protected function generateURI(string $type, array $options): string
100 | {
101 | $label = $this->getLabel();
102 | is_string($label) || throw new InvalidArgumentException('The label is not set.');
103 | $this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');
104 | $options = [...$options, ...$this->getParameters()];
105 | $this->filterOptions($options);
106 | $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
107 |
108 | return sprintf(
109 | 'otpauth://%s/%s?%s',
110 | $type,
111 | rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),
112 | $params
113 | );
114 | }
115 |
116 | /**
117 | * @param non-empty-string $safe
118 | * @param non-empty-string $user
119 | */
120 | protected function compareOTP(string $safe, string $user): bool
121 | {
122 | return hash_equals($safe, $user);
123 | }
124 |
125 | /**
126 | * @return non-empty-string
127 | */
128 | private function getDecodedSecret(): string
129 | {
130 | try {
131 | $decoded = Base32::decodeUpper($this->getSecret());
132 | } catch (Exception) {
133 | throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
134 | }
135 | assert($decoded !== '');
136 |
137 | return $decoded;
138 | }
139 |
140 | private function intToByteString(int $int): string
141 | {
142 | $result = [];
143 | while ($int !== 0) {
144 | $result[] = chr($int & 0xFF);
145 | $int >>= 8;
146 | }
147 |
148 | return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/lang/de_de.lang:
--------------------------------------------------------------------------------
1 | 2factor_auth = 2-Faktor-Login
2 | 2factor_auth_setup = Einstellungen
3 | 2factor_auth_readme = ReadMe
4 | 2factor_auth_OTP-Verifizierung = OTP-Verifizierung
5 | 2factor_auth_page_setup = 2-Faktor-Authentifizierung einrichten
6 | 2factor_auth_2fa_enforced = 2-Faktor-Authentifizierung
7 |
8 | 2factor_auth_2fa_active = 2-Faktor-Authentifizierung ist aktiviert.
9 | 2factor_auth_2fa_inactive = 2-Faktor-Authentifizierung ist deaktiviert.
10 | 2factor_auth_2fa_disable = 2-Faktor-Authentifizierung für `{0}`deaktivieren
11 | 2factor_auth_2fa_disable_instruction = 2-Faktor-Authentifizierung für den aktuell angemeldeten Benutzer deaktivieren.
12 | 2factor_auth_2fa_setup = Einrichtung
13 | 2factor_auth_2fa_verify_instruction = Geben Sie einen OTP-Code ein, um sich einzuloggen.
14 | 2factor_auth_2fa_page_instruction = 2-Faktor-Authentifizierung für den aktuell angemeldeten Benutzer einrichten.
15 | 2factor_auth_2fa_setup_start_totp = TOTP Einrichtung starten (erfordert App; empfohlen)
16 | 2factor_auth_2fa_otp_wrong = Falsches one-time-password, bitte erneut versuchen
17 | 2factor_auth_2fa_setup_start_email = E-Mail basierte Einrichtung starten
18 | 2factor_auth_2fa_setup_start_email_required = Zunächst muss im Benutzerprofil eine gültige E-Mail Adresse hinterlegt werden.
19 | 2factor_auth_2fa_setup_start_phpmailer_required = Für die E-Mail Einrichtung ist das PHPMailer-AddOn erforderlich.
20 | 2factor_auth_2fa_setup_start_email_open_profile = zum Benutzerprofil
21 | 2factor_auth_2fa_setup_verify = Einstellungen verifizieren
22 | 2factor_auth_2fa_setup_scan = QR-Code scannen oder Code kopieren
23 | 2factor_auth_2fa_setup_successfull = 2-Faktor-Authentifizierung erfolgreich eingerichtet
24 | 2factor_auth_2fa_wrong_opt = Falsches one-time-password, bitte erneut versuchen
25 | 2factor_auth_2fa_verify_headline = OTP-Code eingeben, um die Einrichtung abzuschließen
26 | 2factor_auth_2fa_verify_action = Bestätigen
27 | 2factor_auth_2fa_one_time_password = Einmal-Passwort
28 | 2factor_auth_copy = Kopieren
29 | 2factor_auth_2fa_status_email_info = Aktuell ist eine E-Mail-2-Faktor-Authentifizierung für `{0} [{1}]` aktiviert.
30 | 2factor_auth_2fa_status_otp_info = Aktuell ist eine OTP-2-Faktor-Authentifizierung für {0} aktiviert.
31 |
32 | 2factor_auth_2fa_page_totp_instruction = Bei einer TOTP-Authentifizierung wird eine gesonderte Applikation benötigt, z.B. auf einem immer verfügbaren Mobilgerät. Diese muss initial eingerichtet werden. Nach einem Login in REDAXO wird ein Code abgefragt, welcher dann in dieser Applikation eingesehen und eingetragen werden kann, um ins REDAXO Backend zu kommen.
33 | 2factor_auth_2fa_page_email_instruction = Bei einer E-Mail-Authentifizierung wird nach dem Login eine E-Mail an den entsprechenden User geschickt. Mit dem in der E-Mail mitgeschickten Code kommt man ins Backendsystem von REDAXO. Der User muss eine E-Mail im eigenen Profil eingetragen haben.
34 |
35 | 2factor_auth_login = Authentifizierungs App starten und Code eingeben
36 | 2fa_setup_start_email_send = E-Mail wurde versendet
37 |
38 | 2factor_auth_2fa_info_email_sent = Eine E-Mail mit dem Code wurde versendet
39 | 2factor_auth_2fa_info_email_enter_code = Bitte geben sie den Code aus der versendeten E-Mail ein.
40 | 2factor_auth_2fa_info_topt_enter_code = Bitte schauen sie in der Authenticator App nach dem passenden Code.
41 |
42 | 2factor_auth_private_setup = Persönliches Setup
43 | 2factor_auth_config = Config
44 |
45 | 2factor_auth_options = Optionen
46 | 2factor_auth_settings = Einstellungen
47 | 2factor_auth_enforce = Einschränkungen
48 |
49 | 2factor_auth_enforce_all = Erzwungen für alle REDAXO-CMS Benutzer
50 | 2factor_auth_enforce_admins_only = Erzwungen nur für REDAXO-CMS Administratoren
51 | 2factor_auth_enforce_disabled = Keine Einschränkungen (Optional für jeden)
52 |
53 | 2factor_auth_option_all = Alle Möglichkeiten erlauben
54 | 2factor_auth_option_totp_only = Nur Time-Based One-Time Passwort erlauben
55 | 2factor_auth_option_email_only = Nur One-Time Passwort über E-Mail erlauben
56 |
57 | 2factor_auth_updated = Die Konfiguration wurde gespeichert
58 |
59 | 2factor_auth_select_email_only = Nur 2-Faktor-Authentifizierung über E-Mail ist aktiviert
60 | 2factor_auth_select_email_only = Nur 2-Faktor-Authentifizierung über TOTP ist aktiviert
61 |
62 | 2factor_auth_email_period = Zeitintervall bei E-Mail Authentifizierung
63 | minutes = Minuten
64 | 2factor_auth_users = Benutzer
65 |
66 | 2factor_auth_totp_period = Zeitintervall bei Authentikatoren (nicht veränderbar)
67 | 2factor_auth_totp_period_info = {0} Sekunden
68 |
69 | 2factor_auth_logintries = Anzahl der erlaubten Fehlversuche (nicht veränderbar)
70 | 2factor_auth_logintries_info = {0} Versuche
71 |
72 | one_time_password_too_many_tries = Die maximale Anzahl von {1} Versuchen ist überschritten. Der Zugriff ist noch für {0}
Sekunden geblockt. Jede erneute falsche Eingabe blockiert die Eingabe wieder für {2} Sekunden
73 |
74 | 2factor_auth_mail_error = Bitte versuche es noch einmal oder kontaktiere den Administrator
75 | 2factor_auth_config_save = speichern
76 |
--------------------------------------------------------------------------------
/assets/clipboard-copy-element.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global = global || self, global.ClipboardCopyElement = factory());
5 | }(this, function () { 'use strict';
6 |
7 | function createNode(text) {
8 | const node = document.createElement('pre');
9 | node.style.width = '1px';
10 | node.style.height = '1px';
11 | node.style.position = 'fixed';
12 | node.style.top = '5px';
13 | node.textContent = text;
14 | return node;
15 | }
16 |
17 | function copyNode(node) {
18 | if ('clipboard' in navigator) {
19 | // eslint-disable-next-line flowtype/no-flow-fix-me-comments
20 | // $FlowFixMe Clipboard is not defined in Flow yet.
21 | return navigator.clipboard.writeText(node.textContent);
22 | }
23 |
24 | const selection = getSelection();
25 |
26 | if (selection == null) {
27 | return Promise.reject(new Error());
28 | }
29 |
30 | selection.removeAllRanges();
31 | const range = document.createRange();
32 | range.selectNodeContents(node);
33 | selection.addRange(range);
34 | document.execCommand('copy');
35 | selection.removeAllRanges();
36 | return Promise.resolve();
37 | }
38 | function copyText(text) {
39 | if ('clipboard' in navigator) {
40 | // eslint-disable-next-line flowtype/no-flow-fix-me-comments
41 | // $FlowFixMe Clipboard is not defined in Flow yet.
42 | return navigator.clipboard.writeText(text);
43 | }
44 |
45 | const body = document.body;
46 |
47 | if (!body) {
48 | return Promise.reject(new Error());
49 | }
50 |
51 | const node = createNode(text);
52 | body.appendChild(node);
53 | copyNode(node);
54 | body.removeChild(node);
55 | return Promise.resolve();
56 | }
57 |
58 | function copy(button) {
59 | const id = button.getAttribute('for');
60 | const text = button.getAttribute('value');
61 |
62 | function trigger() {
63 | button.dispatchEvent(new CustomEvent('clipboard-copy', {
64 | bubbles: true
65 | }));
66 | }
67 |
68 | if (text) {
69 | copyText(text).then(trigger);
70 | } else if (id) {
71 | const root = 'getRootNode' in Element.prototype ? button.getRootNode() : button.ownerDocument;
72 | if (!(root instanceof Document || 'ShadowRoot' in window && root instanceof ShadowRoot)) return;
73 | const node = root.getElementById(id);
74 | if (node) copyTarget(node).then(trigger);
75 | }
76 | }
77 |
78 | function copyTarget(content) {
79 | if (content instanceof HTMLInputElement || content instanceof HTMLTextAreaElement) {
80 | return copyText(content.value);
81 | } else if (content instanceof HTMLAnchorElement && content.hasAttribute('href')) {
82 | return copyText(content.href);
83 | } else {
84 | return copyNode(content);
85 | }
86 | }
87 |
88 | function clicked(event) {
89 | const button = event.currentTarget;
90 |
91 | if (button instanceof HTMLElement) {
92 | copy(button);
93 | }
94 | }
95 |
96 | function keydown(event) {
97 | if (event.key === ' ' || event.key === 'Enter') {
98 | const button = event.currentTarget;
99 |
100 | if (button instanceof HTMLElement) {
101 | event.preventDefault();
102 | copy(button);
103 | }
104 | }
105 | }
106 |
107 | function focused(event) {
108 | event.currentTarget.addEventListener('keydown', keydown);
109 | }
110 |
111 | function blurred(event) {
112 | event.currentTarget.removeEventListener('keydown', keydown);
113 | }
114 |
115 | class ClipboardCopyElement extends HTMLElement {
116 | constructor() {
117 | super();
118 | this.addEventListener('click', clicked);
119 | this.addEventListener('focus', focused);
120 | this.addEventListener('blur', blurred);
121 | }
122 |
123 | connectedCallback() {
124 | if (!this.hasAttribute('tabindex')) {
125 | this.setAttribute('tabindex', '0');
126 | }
127 |
128 | if (!this.hasAttribute('role')) {
129 | this.setAttribute('role', 'button');
130 | }
131 | }
132 |
133 | get value() {
134 | return this.getAttribute('value') || '';
135 | }
136 |
137 | set value(text) {
138 | this.setAttribute('value', text);
139 | }
140 |
141 | }
142 |
143 | if (!window.customElements.get('clipboard-copy')) {
144 | window.ClipboardCopyElement = ClipboardCopyElement;
145 | window.customElements.define('clipboard-copy', ClipboardCopyElement);
146 | }
147 |
148 | return ClipboardCopyElement;
149 |
150 | }));
151 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Hex.php:
--------------------------------------------------------------------------------
1 | $chunk */
53 | $chunk = \unpack('C', $binString[$i]);
54 | $c = $chunk[1] & 0xf;
55 | $b = $chunk[1] >> 4;
56 |
57 | $hex .= \pack(
58 | 'CC',
59 | (87 + $b + ((($b - 10) >> 8) & ~38)),
60 | (87 + $c + ((($c - 10) >> 8) & ~38))
61 | );
62 | }
63 | return $hex;
64 | }
65 |
66 | /**
67 | * Convert a binary string into a hexadecimal string without cache-timing
68 | * leaks, returning uppercase letters (as per RFC 4648)
69 | *
70 | * @param string $binString (raw binary)
71 | * @return string
72 | * @throws TypeError
73 | */
74 | public static function encodeUpper(
75 | #[\SensitiveParameter]
76 | string $binString
77 | ): string {
78 | $hex = '';
79 | $len = Binary::safeStrlen($binString);
80 |
81 | for ($i = 0; $i < $len; ++$i) {
82 | /** @var array $chunk */
83 | $chunk = \unpack('C', $binString[$i]);
84 | $c = $chunk[1] & 0xf;
85 | $b = $chunk[1] >> 4;
86 |
87 | $hex .= \pack(
88 | 'CC',
89 | (55 + $b + ((($b - 10) >> 8) & ~6)),
90 | (55 + $c + ((($c - 10) >> 8) & ~6))
91 | );
92 | }
93 | return $hex;
94 | }
95 |
96 | /**
97 | * Convert a hexadecimal string into a binary string without cache-timing
98 | * leaks
99 | *
100 | * @param string $encodedString
101 | * @param bool $strictPadding
102 | * @return string (raw binary)
103 | * @throws RangeException
104 | */
105 | public static function decode(
106 | #[\SensitiveParameter]
107 | string $encodedString,
108 | bool $strictPadding = false
109 | ): string {
110 | $hex_pos = 0;
111 | $bin = '';
112 | $c_acc = 0;
113 | $hex_len = Binary::safeStrlen($encodedString);
114 | $state = 0;
115 | if (($hex_len & 1) !== 0) {
116 | if ($strictPadding) {
117 | throw new RangeException(
118 | 'Expected an even number of hexadecimal characters'
119 | );
120 | } else {
121 | $encodedString = '0' . $encodedString;
122 | ++$hex_len;
123 | }
124 | }
125 |
126 | /** @var array $chunk */
127 | $chunk = \unpack('C*', $encodedString);
128 | while ($hex_pos < $hex_len) {
129 | ++$hex_pos;
130 | $c = $chunk[$hex_pos];
131 | $c_num = $c ^ 48;
132 | $c_num0 = ($c_num - 10) >> 8;
133 | $c_alpha = ($c & ~32) - 55;
134 | $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
135 |
136 | if (($c_num0 | $c_alpha0) === 0) {
137 | throw new RangeException(
138 | 'Expected hexadecimal character'
139 | );
140 | }
141 | $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
142 | if ($state === 0) {
143 | $c_acc = $c_val * 16;
144 | } else {
145 | $bin .= \pack('C', $c_acc | $c_val);
146 | }
147 | $state ^= 1;
148 | }
149 | return $bin;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/RFC4648.php:
--------------------------------------------------------------------------------
1 | "Zm9v"
43 | *
44 | * @param string $str
45 | * @return string
46 | *
47 | * @throws TypeError
48 | */
49 | public static function base64Encode(
50 | #[\SensitiveParameter]
51 | string $str
52 | ): string {
53 | return Base64::encode($str);
54 | }
55 |
56 | /**
57 | * RFC 4648 Base64 decoding
58 | *
59 | * "Zm9v" -> "foo"
60 | *
61 | * @param string $str
62 | * @return string
63 | *
64 | * @throws TypeError
65 | */
66 | public static function base64Decode(
67 | #[\SensitiveParameter]
68 | string $str
69 | ): string {
70 | return Base64::decode($str, true);
71 | }
72 |
73 | /**
74 | * RFC 4648 Base64 (URL Safe) encoding
75 | *
76 | * "foo" -> "Zm9v"
77 | *
78 | * @param string $str
79 | * @return string
80 | *
81 | * @throws TypeError
82 | */
83 | public static function base64UrlSafeEncode(
84 | #[\SensitiveParameter]
85 | string $str
86 | ): string {
87 | return Base64UrlSafe::encode($str);
88 | }
89 |
90 | /**
91 | * RFC 4648 Base64 (URL Safe) decoding
92 | *
93 | * "Zm9v" -> "foo"
94 | *
95 | * @param string $str
96 | * @return string
97 | *
98 | * @throws TypeError
99 | */
100 | public static function base64UrlSafeDecode(
101 | #[\SensitiveParameter]
102 | string $str
103 | ): string {
104 | return Base64UrlSafe::decode($str, true);
105 | }
106 |
107 | /**
108 | * RFC 4648 Base32 encoding
109 | *
110 | * "foo" -> "MZXW6==="
111 | *
112 | * @param string $str
113 | * @return string
114 | *
115 | * @throws TypeError
116 | */
117 | public static function base32Encode(
118 | #[\SensitiveParameter]
119 | string $str
120 | ): string {
121 | return Base32::encodeUpper($str);
122 | }
123 |
124 | /**
125 | * RFC 4648 Base32 encoding
126 | *
127 | * "MZXW6===" -> "foo"
128 | *
129 | * @param string $str
130 | * @return string
131 | *
132 | * @throws TypeError
133 | */
134 | public static function base32Decode(
135 | #[\SensitiveParameter]
136 | string $str
137 | ): string {
138 | return Base32::decodeUpper($str, true);
139 | }
140 |
141 | /**
142 | * RFC 4648 Base32-Hex encoding
143 | *
144 | * "foo" -> "CPNMU==="
145 | *
146 | * @param string $str
147 | * @return string
148 | *
149 | * @throws TypeError
150 | */
151 | public static function base32HexEncode(
152 | #[\SensitiveParameter]
153 | string $str
154 | ): string {
155 | return Base32::encodeUpper($str);
156 | }
157 |
158 | /**
159 | * RFC 4648 Base32-Hex decoding
160 | *
161 | * "CPNMU===" -> "foo"
162 | *
163 | * @param string $str
164 | * @return string
165 | *
166 | * @throws TypeError
167 | */
168 | public static function base32HexDecode(
169 | #[\SensitiveParameter]
170 | string $str
171 | ): string {
172 | return Base32::decodeUpper($str, true);
173 | }
174 |
175 | /**
176 | * RFC 4648 Base16 decoding
177 | *
178 | * "foo" -> "666F6F"
179 | *
180 | * @param string $str
181 | * @return string
182 | *
183 | * @throws TypeError
184 | */
185 | public static function base16Encode(
186 | #[\SensitiveParameter]
187 | string $str
188 | ): string {
189 | return Hex::encodeUpper($str);
190 | }
191 |
192 | /**
193 | * RFC 4648 Base16 decoding
194 | *
195 | * "666F6F" -> "foo"
196 | *
197 | * @param string $str
198 | * @return string
199 | */
200 | public static function base16Decode(
201 | #[\SensitiveParameter]
202 | string $str
203 | ): string {
204 | return Hex::decode($str, true);
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/pages/settings.php:
--------------------------------------------------------------------------------
1 | setConfig('enforce', rex_request('2factor_auth_enforce', 'string'));
10 | $this->setConfig('option', rex_request('2factor_auth_option', 'string'));
11 | $this->setConfig('email_period', rex_request('2factor_auth_email_period', 'int', 300));
12 | echo rex_view::success($this->i18n('2factor_auth_updated'));
13 | }
14 |
15 | $selectEnforce = new rex_select();
16 | $selectEnforce->setId('2factor_auth_enforce');
17 | $selectEnforce->setName('2factor_auth_enforce');
18 | $selectEnforce->setAttribute('class', 'form-control selectpicker');
19 | $selectEnforce->setSelected($this->getConfig('enforce'));
20 |
21 | $selectEnforce->addOption($this->i18n('2factor_auth_enforce_' . one_time_password::ENFORCED_ALL), one_time_password::ENFORCED_ALL);
22 | $selectEnforce->addOption($this->i18n('2factor_auth_enforce_' . one_time_password::ENFORCED_ADMINS), one_time_password::ENFORCED_ADMINS);
23 | $selectEnforce->addOption($this->i18n('2factor_auth_enforce_' . one_time_password::ENFORCED_DISABLED), one_time_password::ENFORCED_DISABLED);
24 |
25 | $selectOption = new rex_select();
26 | $selectOption->setId('2factor_auth_option');
27 | $selectOption->setName('2factor_auth_option');
28 | $selectOption->setAttribute('class', 'form-control selectpicker');
29 | $selectOption->setSelected($this->getConfig('option'));
30 |
31 | $selectOption->addOption($this->i18n('2factor_auth_option_' . one_time_password::OPTION_ALL), one_time_password::OPTION_ALL);
32 | $selectOption->addOption($this->i18n('2factor_auth_option_' . one_time_password::OPTION_TOTP), one_time_password::OPTION_TOTP);
33 | $selectOption->addOption($this->i18n('2factor_auth_option_' . one_time_password::OPTION_EMAIL), one_time_password::OPTION_EMAIL);
34 |
35 | $selectEmailPeriod = new rex_select();
36 | $selectEmailPeriod->setId('2factor_auth_email_period');
37 | $selectEmailPeriod->setName('2factor_auth_email_period');
38 | $selectEmailPeriod->setAttribute('class', 'form-control selectpicker');
39 | $selectEmailPeriod->setSelected($this->getConfig('email_period'));
40 |
41 | $selectEmailPeriod->addOption('5 ' . $this->i18n('minutes'), 300);
42 | $selectEmailPeriod->addOption('10 ' . $this->i18n('minutes'), 600);
43 | $selectEmailPeriod->addOption('15 ' . $this->i18n('minutes'), 900);
44 | $selectEmailPeriod->addOption('30 ' . $this->i18n('minutes'), 1800);
45 |
46 | $selectTOTPPeriod = new rex_select();
47 | $selectTOTPPeriod->setAttribute('class', 'form-control selectpicker');
48 | $selectTOTPPeriod->setDisabled(true);
49 | $selectTOTPPeriod->addOption($this->i18n('2factor_auth_totp_period_info', method_totp::getPeriod()), 30);
50 |
51 | $selectLoginTries = new rex_select();
52 | $selectLoginTries->setAttribute('class', 'form-control selectpicker');
53 | $selectLoginTries->setDisabled(true);
54 | $selectLoginTries->addOption($this->i18n('2factor_auth_logintries_info', method_totp::getloginTries()), 30);
55 |
56 | $content = '
57 |
116 |
117 | ';
118 |
119 | $fragment = new rex_fragment();
120 | $fragment->setVar('class', 'edit');
121 | $fragment->setVar('title', $this->i18n('2factor_auth_settings'));
122 | $fragment->setVar('body', $content, false);
123 | echo $fragment->parse('core/page/section.php');
124 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/ParameterTrait.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | private array $parameters = [];
20 |
21 | /**
22 | * @var non-empty-string|null
23 | */
24 | private null|string $issuer = null;
25 |
26 | /**
27 | * @var non-empty-string|null
28 | */
29 | private null|string $label = null;
30 |
31 | private bool $issuer_included_as_parameter = true;
32 |
33 | /**
34 | * @return array
35 | */
36 | public function getParameters(): array
37 | {
38 | $parameters = $this->parameters;
39 |
40 | if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
41 | $parameters['issuer'] = $this->getIssuer();
42 | }
43 |
44 | return $parameters;
45 | }
46 |
47 | public function getSecret(): string
48 | {
49 | $value = $this->getParameter('secret');
50 | (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "secret" parameter.');
51 |
52 | return $value;
53 | }
54 |
55 | public function getLabel(): null|string
56 | {
57 | return $this->label;
58 | }
59 |
60 | public function setLabel(string $label): void
61 | {
62 | $this->setParameter('label', $label);
63 | }
64 |
65 | public function getIssuer(): null|string
66 | {
67 | return $this->issuer;
68 | }
69 |
70 | public function setIssuer(string $issuer): void
71 | {
72 | $this->setParameter('issuer', $issuer);
73 | }
74 |
75 | public function isIssuerIncludedAsParameter(): bool
76 | {
77 | return $this->issuer_included_as_parameter;
78 | }
79 |
80 | public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
81 | {
82 | $this->issuer_included_as_parameter = $issuer_included_as_parameter;
83 | }
84 |
85 | public function getDigits(): int
86 | {
87 | $value = $this->getParameter('digits');
88 | (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "digits" parameter.');
89 |
90 | return $value;
91 | }
92 |
93 | public function getDigest(): string
94 | {
95 | $value = $this->getParameter('algorithm');
96 | (is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "algorithm" parameter.');
97 |
98 | return $value;
99 | }
100 |
101 | public function hasParameter(string $parameter): bool
102 | {
103 | return array_key_exists($parameter, $this->parameters);
104 | }
105 |
106 | public function getParameter(string $parameter): mixed
107 | {
108 | if ($this->hasParameter($parameter)) {
109 | return $this->getParameters()[$parameter];
110 | }
111 |
112 | throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
113 | }
114 |
115 | public function setParameter(string $parameter, mixed $value): void
116 | {
117 | $map = $this->getParameterMap();
118 |
119 | if (array_key_exists($parameter, $map) === true) {
120 | $callback = $map[$parameter];
121 | $value = $callback($value);
122 | }
123 |
124 | if (property_exists($this, $parameter)) {
125 | $this->{$parameter} = $value;
126 | } else {
127 | $this->parameters[$parameter] = $value;
128 | }
129 | }
130 |
131 | public function setSecret(string $secret): void
132 | {
133 | $this->setParameter('secret', $secret);
134 | }
135 |
136 | public function setDigits(int $digits): void
137 | {
138 | $this->setParameter('digits', $digits);
139 | }
140 |
141 | public function setDigest(string $digest): void
142 | {
143 | $this->setParameter('algorithm', $digest);
144 | }
145 |
146 | /**
147 | * @return array
148 | */
149 | protected function getParameterMap(): array
150 | {
151 | return [
152 | 'label' => function (string $value): string {
153 | assert($value !== '');
154 | $this->hasColon($value) === false || throw new InvalidArgumentException(
155 | 'Label must not contain a colon.'
156 | );
157 |
158 | return $value;
159 | },
160 | 'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')),
161 | 'algorithm' => static function (string $value): string {
162 | $value = mb_strtolower($value);
163 | in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
164 | 'The "%s" digest is not supported.',
165 | $value
166 | ));
167 |
168 | return $value;
169 | },
170 | 'digits' => static function ($value): int {
171 | $value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');
172 |
173 | return (int) $value;
174 | },
175 | 'issuer' => function (string $value): string {
176 | assert($value !== '');
177 | $this->hasColon($value) === false || throw new InvalidArgumentException(
178 | 'Issuer must not contain a colon.'
179 | );
180 |
181 | return $value;
182 | },
183 | ];
184 | }
185 |
186 | /**
187 | * @param non-empty-string $value
188 | */
189 | private function hasColon(string $value): bool
190 | {
191 | $colons = [':', '%3A', '%3a'];
192 | foreach ($colons as $colon) {
193 | if (str_contains($value, $colon)) {
194 | return true;
195 | }
196 | }
197 |
198 | return false;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/vendor/spomky-labs/otphp/src/TOTP.php:
--------------------------------------------------------------------------------
1 | clock = $clock;
32 | }
33 |
34 | public static function create(
35 | null|string $secret = null,
36 | int $period = self::DEFAULT_PERIOD,
37 | string $digest = self::DEFAULT_DIGEST,
38 | int $digits = self::DEFAULT_DIGITS,
39 | int $epoch = self::DEFAULT_EPOCH,
40 | ?ClockInterface $clock = null
41 | ): self {
42 | $totp = $secret !== null
43 | ? self::createFromSecret($secret, $clock)
44 | : self::generate($clock)
45 | ;
46 | $totp->setPeriod($period);
47 | $totp->setDigest($digest);
48 | $totp->setDigits($digits);
49 | $totp->setEpoch($epoch);
50 |
51 | return $totp;
52 | }
53 |
54 | public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self
55 | {
56 | $totp = new self($secret, $clock);
57 | $totp->setPeriod(self::DEFAULT_PERIOD);
58 | $totp->setDigest(self::DEFAULT_DIGEST);
59 | $totp->setDigits(self::DEFAULT_DIGITS);
60 | $totp->setEpoch(self::DEFAULT_EPOCH);
61 |
62 | return $totp;
63 | }
64 |
65 | public static function generate(?ClockInterface $clock = null): self
66 | {
67 | return self::createFromSecret(self::generateSecret(), $clock);
68 | }
69 |
70 | public function getPeriod(): int
71 | {
72 | $value = $this->getParameter('period');
73 | (is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
74 |
75 | return $value;
76 | }
77 |
78 | public function getEpoch(): int
79 | {
80 | $value = $this->getParameter('epoch');
81 | (is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
82 |
83 | return $value;
84 | }
85 |
86 | public function expiresIn(): int
87 | {
88 | $period = $this->getPeriod();
89 |
90 | return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
91 | }
92 |
93 | /**
94 | * The OTP at the specified input.
95 | *
96 | * @param 0|positive-int $input
97 | */
98 | public function at(int $input): string
99 | {
100 | return $this->generateOTP($this->timecode($input));
101 | }
102 |
103 | public function now(): string
104 | {
105 | $timestamp = $this->clock->now()
106 | ->getTimestamp();
107 | assert($timestamp >= 0, 'The timestamp must return a positive integer.');
108 |
109 | return $this->at($timestamp);
110 | }
111 |
112 | /**
113 | * If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
114 | * allow time drift. The passed value is in seconds.
115 | *
116 | * @param 0|positive-int $timestamp
117 | * @param null|0|positive-int $leeway
118 | */
119 | public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
120 | {
121 | $timestamp ??= $this->clock->now()
122 | ->getTimestamp();
123 | $timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
124 |
125 | if ($leeway === null) {
126 | return $this->compareOTP($this->at($timestamp), $otp);
127 | }
128 |
129 | $leeway = abs($leeway);
130 | $leeway < $this->getPeriod() || throw new InvalidArgumentException(
131 | 'The leeway must be lower than the TOTP period'
132 | );
133 | $timestampMinusLeeway = $timestamp - $leeway;
134 | $timestampMinusLeeway >= 0 || throw new InvalidArgumentException(
135 | 'The timestamp must be greater than or equal to the leeway.'
136 | );
137 |
138 | return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
139 | || $this->compareOTP($this->at($timestamp), $otp)
140 | || $this->compareOTP($this->at($timestamp + $leeway), $otp);
141 | }
142 |
143 | public function getProvisioningUri(): string
144 | {
145 | $params = [];
146 | if ($this->getPeriod() !== 30) {
147 | $params['period'] = $this->getPeriod();
148 | }
149 |
150 | if ($this->getEpoch() !== 0) {
151 | $params['epoch'] = $this->getEpoch();
152 | }
153 |
154 | return $this->generateURI('totp', $params);
155 | }
156 |
157 | public function setPeriod(int $period): void
158 | {
159 | $this->setParameter('period', $period);
160 | }
161 |
162 | public function setEpoch(int $epoch): void
163 | {
164 | $this->setParameter('epoch', $epoch);
165 | }
166 |
167 | /**
168 | * @return array
169 | */
170 | protected function getParameterMap(): array
171 | {
172 | return [
173 | ...parent::getParameterMap(),
174 | 'period' => static function ($value): int {
175 | (int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');
176 |
177 | return (int) $value;
178 | },
179 | 'epoch' => static function ($value): int {
180 | (int) $value >= 0 || throw new InvalidArgumentException(
181 | 'Epoch must be greater than or equal to 0.'
182 | );
183 |
184 | return (int) $value;
185 | },
186 | ];
187 | }
188 |
189 | /**
190 | * @param array $options
191 | */
192 | protected function filterOptions(array &$options): void
193 | {
194 | parent::filterOptions($options);
195 |
196 | if (isset($options['epoch']) && $options['epoch'] === 0) {
197 | unset($options['epoch']);
198 | }
199 |
200 | ksort($options);
201 | }
202 |
203 | /**
204 | * @param 0|positive-int $timestamp
205 | *
206 | * @return 0|positive-int
207 | */
208 | private function timecode(int $timestamp): int
209 | {
210 | $timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
211 | assert($timecode >= 0);
212 |
213 | return $timecode;
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/pages/setup.php:
--------------------------------------------------------------------------------
1 | i18n('2fa_setup');
13 | $buttons = '';
14 | $content = '';
15 | $uri = '';
16 | $success = false;
17 |
18 | $csrfToken = rex_csrf_token::factory('2factor_auth_setup');
19 | $func = rex_request('func', 'string');
20 |
21 | $otp = one_time_password::getInstance();
22 | $otp_options = $otp->getAuthOption();
23 |
24 | if (one_time_password::OPTION_EMAIL == $otp_options) {
25 | // email_only -> kein totp
26 | echo rex_view::info($this->i18n('2factor_auth_select_' . one_time_password::OPTION_EMAIL));
27 | if ('setup-totp' == $func) {
28 | $func = '';
29 | }
30 | }
31 |
32 | if (one_time_password::OPTION_TOTP == $otp_options) {
33 | // only email email_only
34 | echo rex_view::info($this->i18n('2factor_auth_select_' . one_time_password::OPTION_EMAIL));
35 | if ('setup-email' == $func) {
36 | $func = '';
37 | }
38 | }
39 |
40 | if ('setup-email' === $func || 'setup-totp' === $func) {
41 | switch ($func) {
42 | case 'setup-email':
43 | $otpMethod = new method_email();
44 | break;
45 | default:
46 | $otpMethod = new method_totp();
47 | break;
48 | }
49 |
50 | $config = one_time_password_config::loadFromDb($otpMethod, rex::requireUser());
51 | $config->updateMethod($otpMethod);
52 | $user_id = rex::requireUser()->getId();
53 | rex_user::clearInstance($user_id);
54 | rex::setProperty('user', rex_user::get($user_id));
55 | }
56 |
57 | $otpMethod = $otp->getMethod();
58 |
59 | if ('' !== $func && !$csrfToken->isValid()) {
60 | $message = rex_view::error($this->i18n('csrf_token_invalid'));
61 | $func = '';
62 | }
63 |
64 | if ('disable' === $func) {
65 | $config = one_time_password_config::loadFromDb($otpMethod, rex::requireUser());
66 | $config->disable();
67 | $func = '';
68 | }
69 |
70 | if (one_time_password::ENFORCED_ALL === $otp->isEnforced()) {
71 | $message .= rex_view::info($this->i18n('2fa_enforced') . ': ' . $this->i18n('2factor_auth_enforce_' . one_time_password::ENFORCED_ALL));
72 | }
73 | if (one_time_password::ENFORCED_ADMINS === $otp->isEnforced()) {
74 | $message .= rex_view::info($this->i18n('2fa_enforced') . ': ' . $this->i18n('2factor_auth_enforce_' . one_time_password::ENFORCED_ADMINS));
75 | }
76 |
77 | $config = one_time_password_config::loadFromDb($otpMethod, rex::requireUser());
78 | if ($otp->isEnabled() && $config->enabled) {
79 | $title = $this->i18n('status');
80 | switch ($config->method) {
81 | case 'email':
82 | $content = '' . $this->i18n('2fa_status_email_info', rex::getUser()->getLogin(), rex::getUser()->getEmail()) . '
';
83 | break;
84 | default:
85 | $content = '' . $this->i18n('2fa_status_otp_info', rex::getUser()->getLogin()) . '
';
86 | break;
87 | }
88 | $this->i18n('2fa_status_otp_instruction');
89 | $content .= '' . $this->i18n('2fa_disable', rex::getUser()->getLogin()) . '
';
90 | } else {
91 | if ('' === $func) {
92 | if (one_time_password::OPTION_ALL == $otp_options || one_time_password::OPTION_TOTP == $otp_options) {
93 | $content .= '' . $this->i18n('2factor_auth_2fa_page_totp_instruction') . '
';
94 | $content .= '' . $this->i18n('2fa_setup_start_totp') . '
';
95 | }
96 | if (one_time_password::OPTION_ALL == $otp_options || one_time_password::OPTION_EMAIL == $otp_options) {
97 | $content .= '' . $this->i18n('2factor_auth_2fa_page_email_instruction') . '
';
98 | $content .= '' . $this->i18n('2fa_setup_start_email') . '
';
99 | }
100 | } elseif ('setup-totp' === $func) {
101 | // nothing todo
102 | } elseif ('setup-email' === $func) {
103 | if (!rex_addon::get('phpmailer')->isAvailable()) {
104 | $content = rex_view::error($this->i18n('2fa_setup_start_phpmailer_required'));
105 | $func = '';
106 | }
107 |
108 | $email = trim(rex::requireUser()->getEmail());
109 | if ('' !== $func && ('' === $email || !str_contains($email, '@'))) {
110 | $content = rex_view::error($this->i18n('2fa_setup_start_email_required'));
111 | $buttons = '' . $this->i18n('2fa_setup_start_email_open_profile') . '';
112 |
113 | $func = '';
114 | }
115 |
116 | if ('' !== $func) {
117 | try {
118 | one_time_password::getInstance()->challenge();
119 | $message = rex_view::info($this->i18n('2fa_setup_start_email_send'));
120 | } catch (Exception $e) {
121 | $message = rex_view::error($e->getMessage());
122 | $func = '';
123 | }
124 | }
125 | } elseif ('verify-totp' === $func || 'verify-email' === $func) {
126 | $otp = rex_post('rex_login_otp', 'string', '');
127 |
128 | if ('' !== $otp) {
129 | if (one_time_password::getInstance()->verify($otp)) {
130 | $message = '' . $this->i18n('2fa_setup_successfull') . '
';
131 | $config = one_time_password_config::loadFromDb($otpMethod, rex::requireUser());
132 | $config->enable();
133 | $content = '';
134 | $success = true;
135 | }
136 | }
137 |
138 | if (!$success) {
139 | $message = '' . $this->i18n('2fa_wrong_opt') . '
';
140 | }
141 | } else {
142 | throw new rex_exception('unknown state');
143 | }
144 | }
145 |
146 | if ('setup-email' === $func || 'verify-email' === $func || 'setup-totp' === $func || 'verify-totp' === $func) {
147 | $config = one_time_password_config::loadFromDb($otpMethod, rex::requireUser());
148 | $uri = $config->provisioningUri;
149 |
150 | $fragment->setVar('addon', $this, false);
151 | $fragment->setVar('csrfToken', $csrfToken, false);
152 | $fragment->setVar('message', $message, false);
153 | $fragment->setVar('buttons', $buttons, false);
154 | $fragment->setVar('uri', $uri, false);
155 | $fragment->setVar('success', $success, false);
156 |
157 | if ('setup-totp' === $func || 'verify-totp' === $func) {
158 | echo $fragment->parse('2fa.setup-totp.php');
159 | } else {
160 | echo $fragment->parse('2fa.setup-email.php');
161 | }
162 | } else {
163 | $fragment = new rex_fragment();
164 | $fragment->setVar('before', $message, false);
165 | $fragment->setVar('heading', $title, false);
166 | $fragment->setVar('body', $content, false);
167 | $fragment->setVar('buttons', $buttons, false);
168 | echo $fragment->parse('core/page/section.php');
169 | }
170 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Encoding.php:
--------------------------------------------------------------------------------
1 | =8.1",
142 | "psr/clock": "^1.0",
143 | "symfony/deprecation-contracts": "^3.2"
144 | },
145 | "require-dev": {
146 | "ekino/phpstan-banned-code": "^1.0",
147 | "infection/infection": "^0.26|^0.27|^0.28|^0.29",
148 | "php-parallel-lint/php-parallel-lint": "^1.3",
149 | "phpstan/phpstan": "^1.0",
150 | "phpstan/phpstan-deprecation-rules": "^1.0",
151 | "phpstan/phpstan-phpunit": "^1.0",
152 | "phpstan/phpstan-strict-rules": "^1.0",
153 | "phpunit/phpunit": "^9.5.26|^10.0|^11.0",
154 | "qossmic/deptrac-shim": "^1.0",
155 | "rector/rector": "^1.0",
156 | "symfony/phpunit-bridge": "^6.1|^7.0",
157 | "symplify/easy-coding-standard": "^12.0"
158 | },
159 | "type": "library",
160 | "autoload": {
161 | "psr-4": {
162 | "OTPHP\\": "src/"
163 | }
164 | },
165 | "notification-url": "https://packagist.org/downloads/",
166 | "license": [
167 | "MIT"
168 | ],
169 | "authors": [
170 | {
171 | "name": "Florent Morselli",
172 | "homepage": "https://github.com/Spomky"
173 | },
174 | {
175 | "name": "All contributors",
176 | "homepage": "https://github.com/Spomky-Labs/otphp/contributors"
177 | }
178 | ],
179 | "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
180 | "homepage": "https://github.com/Spomky-Labs/otphp",
181 | "keywords": [
182 | "FreeOTP",
183 | "RFC 4226",
184 | "RFC 6238",
185 | "google authenticator",
186 | "hotp",
187 | "otp",
188 | "totp"
189 | ],
190 | "support": {
191 | "issues": "https://github.com/Spomky-Labs/otphp/issues",
192 | "source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0"
193 | },
194 | "funding": [
195 | {
196 | "url": "https://github.com/Spomky",
197 | "type": "github"
198 | },
199 | {
200 | "url": "https://www.patreon.com/FlorentMorselli",
201 | "type": "patreon"
202 | }
203 | ],
204 | "time": "2024-06-12T11:22:32+00:00"
205 | },
206 | {
207 | "name": "symfony/deprecation-contracts",
208 | "version": "v3.5.0",
209 | "source": {
210 | "type": "git",
211 | "url": "https://github.com/symfony/deprecation-contracts.git",
212 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
213 | },
214 | "dist": {
215 | "type": "zip",
216 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
217 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
218 | "shasum": ""
219 | },
220 | "require": {
221 | "php": ">=8.1"
222 | },
223 | "type": "library",
224 | "extra": {
225 | "branch-alias": {
226 | "dev-main": "3.5-dev"
227 | },
228 | "thanks": {
229 | "name": "symfony/contracts",
230 | "url": "https://github.com/symfony/contracts"
231 | }
232 | },
233 | "autoload": {
234 | "files": [
235 | "function.php"
236 | ]
237 | },
238 | "notification-url": "https://packagist.org/downloads/",
239 | "license": [
240 | "MIT"
241 | ],
242 | "authors": [
243 | {
244 | "name": "Nicolas Grekas",
245 | "email": "p@tchwork.com"
246 | },
247 | {
248 | "name": "Symfony Community",
249 | "homepage": "https://symfony.com/contributors"
250 | }
251 | ],
252 | "description": "A generic function and convention to trigger deprecation notices",
253 | "homepage": "https://symfony.com",
254 | "support": {
255 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
256 | },
257 | "funding": [
258 | {
259 | "url": "https://symfony.com/sponsor",
260 | "type": "custom"
261 | },
262 | {
263 | "url": "https://github.com/fabpot",
264 | "type": "github"
265 | },
266 | {
267 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
268 | "type": "tidelift"
269 | }
270 | ],
271 | "time": "2024-04-18T09:32:20+00:00"
272 | }
273 | ],
274 | "packages-dev": [],
275 | "aliases": [],
276 | "minimum-stability": "stable",
277 | "stability-flags": [],
278 | "prefer-stable": false,
279 | "prefer-lowest": false,
280 | "platform": {
281 | "php": ">=8.1"
282 | },
283 | "platform-dev": [],
284 | "plugin-api-version": "2.6.0"
285 | }
286 |
--------------------------------------------------------------------------------
/vendor/paragonie/constant_time_encoding/src/Base64.php:
--------------------------------------------------------------------------------
1 | $chunk */
91 | $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3));
92 | $b0 = $chunk[1];
93 | $b1 = $chunk[2];
94 | $b2 = $chunk[3];
95 |
96 | $dest .=
97 | static::encode6Bits( $b0 >> 2 ) .
98 | static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
99 | static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
100 | static::encode6Bits( $b2 & 63);
101 | }
102 | // The last chunk, which may have padding:
103 | if ($i < $srcLen) {
104 | /** @var array $chunk */
105 | $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
106 | $b0 = $chunk[1];
107 | if ($i + 1 < $srcLen) {
108 | $b1 = $chunk[2];
109 | $dest .=
110 | static::encode6Bits($b0 >> 2) .
111 | static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
112 | static::encode6Bits(($b1 << 2) & 63);
113 | if ($pad) {
114 | $dest .= '=';
115 | }
116 | } else {
117 | $dest .=
118 | static::encode6Bits( $b0 >> 2) .
119 | static::encode6Bits(($b0 << 4) & 63);
120 | if ($pad) {
121 | $dest .= '==';
122 | }
123 | }
124 | }
125 | return $dest;
126 | }
127 |
128 | /**
129 | * decode from base64 into binary
130 | *
131 | * Base64 character set "./[A-Z][a-z][0-9]"
132 | *
133 | * @param string $encodedString
134 | * @param bool $strictPadding
135 | * @return string
136 | *
137 | * @throws RangeException
138 | * @throws TypeError
139 | */
140 | public static function decode(
141 | #[\SensitiveParameter]
142 | string $encodedString,
143 | bool $strictPadding = false
144 | ): string {
145 | // Remove padding
146 | $srcLen = Binary::safeStrlen($encodedString);
147 | if ($srcLen === 0) {
148 | return '';
149 | }
150 |
151 | if ($strictPadding) {
152 | if (($srcLen & 3) === 0) {
153 | if ($encodedString[$srcLen - 1] === '=') {
154 | $srcLen--;
155 | if ($encodedString[$srcLen - 1] === '=') {
156 | $srcLen--;
157 | }
158 | }
159 | }
160 | if (($srcLen & 3) === 1) {
161 | throw new RangeException(
162 | 'Incorrect padding'
163 | );
164 | }
165 | if ($encodedString[$srcLen - 1] === '=') {
166 | throw new RangeException(
167 | 'Incorrect padding'
168 | );
169 | }
170 | } else {
171 | $encodedString = \rtrim($encodedString, '=');
172 | $srcLen = Binary::safeStrlen($encodedString);
173 | }
174 |
175 | $err = 0;
176 | $dest = '';
177 | // Main loop (no padding):
178 | for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
179 | /** @var array $chunk */
180 | $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4));
181 | $c0 = static::decode6Bits($chunk[1]);
182 | $c1 = static::decode6Bits($chunk[2]);
183 | $c2 = static::decode6Bits($chunk[3]);
184 | $c3 = static::decode6Bits($chunk[4]);
185 |
186 | $dest .= \pack(
187 | 'CCC',
188 | ((($c0 << 2) | ($c1 >> 4)) & 0xff),
189 | ((($c1 << 4) | ($c2 >> 2)) & 0xff),
190 | ((($c2 << 6) | $c3 ) & 0xff)
191 | );
192 | $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
193 | }
194 | // The last chunk, which may have padding:
195 | if ($i < $srcLen) {
196 | /** @var array $chunk */
197 | $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i));
198 | $c0 = static::decode6Bits($chunk[1]);
199 |
200 | if ($i + 2 < $srcLen) {
201 | $c1 = static::decode6Bits($chunk[2]);
202 | $c2 = static::decode6Bits($chunk[3]);
203 | $dest .= \pack(
204 | 'CC',
205 | ((($c0 << 2) | ($c1 >> 4)) & 0xff),
206 | ((($c1 << 4) | ($c2 >> 2)) & 0xff)
207 | );
208 | $err |= ($c0 | $c1 | $c2) >> 8;
209 | if ($strictPadding) {
210 | $err |= ($c2 << 6) & 0xff;
211 | }
212 | } elseif ($i + 1 < $srcLen) {
213 | $c1 = static::decode6Bits($chunk[2]);
214 | $dest .= \pack(
215 | 'C',
216 | ((($c0 << 2) | ($c1 >> 4)) & 0xff)
217 | );
218 | $err |= ($c0 | $c1) >> 8;
219 | if ($strictPadding) {
220 | $err |= ($c1 << 4) & 0xff;
221 | }
222 | } elseif ($strictPadding) {
223 | $err |= 1;
224 | }
225 | }
226 | $check = ($err === 0);
227 | if (!$check) {
228 | throw new RangeException(
229 | 'Base64::decode() only expects characters in the correct base64 alphabet'
230 | );
231 | }
232 | return $dest;
233 | }
234 |
235 | /**
236 | * @param string $encodedString
237 | * @return string
238 | */
239 | public static function decodeNoPadding(
240 | #[\SensitiveParameter]
241 | string $encodedString
242 | ): string {
243 | $srcLen = Binary::safeStrlen($encodedString);
244 | if ($srcLen === 0) {
245 | return '';
246 | }
247 | if (($srcLen & 3) === 0) {
248 | // If $strLen is not zero, and it is divisible by 4, then it's at least 4.
249 | if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {
250 | throw new InvalidArgumentException(
251 | "decodeNoPadding() doesn't tolerate padding"
252 | );
253 | }
254 | }
255 | return static::decode(
256 | $encodedString,
257 | true
258 | );
259 | }
260 |
261 | /**
262 | * Uses bitwise operators instead of table-lookups to turn 6-bit integers
263 | * into 8-bit integers.
264 | *
265 | * Base64 character set:
266 | * [A-Z] [a-z] [0-9] + /
267 | * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
268 | *
269 | * @param int $src
270 | * @return int
271 | */
272 | protected static function decode6Bits(int $src): int
273 | {
274 | $ret = -1;
275 |
276 | // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
277 | $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
278 |
279 | // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
280 | $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
281 |
282 | // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
283 | $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
284 |
285 | // if ($src == 0x2b) $ret += 62 + 1;
286 | $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;
287 |
288 | // if ($src == 0x2f) ret += 63 + 1;
289 | $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;
290 |
291 | return $ret;
292 | }
293 |
294 | /**
295 | * Uses bitwise operators instead of table-lookups to turn 8-bit integers
296 | * into 6-bit integers.
297 | *
298 | * @param int $src
299 | * @return string
300 | */
301 | protected static function encode6Bits(int $src): string
302 | {
303 | $diff = 0x41;
304 |
305 | // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
306 | $diff += ((25 - $src) >> 8) & 6;
307 |
308 | // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
309 | $diff -= ((51 - $src) >> 8) & 75;
310 |
311 | // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15
312 | $diff -= ((61 - $src) >> 8) & 15;
313 |
314 | // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3
315 | $diff += ((62 - $src) >> 8) & 3;
316 |
317 | return \pack('C', $src + $diff);
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/vendor/composer/installed.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | {
4 | "name": "paragonie/constant_time_encoding",
5 | "version": "v3.0.0",
6 | "version_normalized": "3.0.0.0",
7 | "source": {
8 | "type": "git",
9 | "url": "https://github.com/paragonie/constant_time_encoding.git",
10 | "reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
11 | },
12 | "dist": {
13 | "type": "zip",
14 | "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
15 | "reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
16 | "shasum": ""
17 | },
18 | "require": {
19 | "php": "^8"
20 | },
21 | "require-dev": {
22 | "phpunit/phpunit": "^9",
23 | "vimeo/psalm": "^4|^5"
24 | },
25 | "time": "2024-05-08T12:36:18+00:00",
26 | "type": "library",
27 | "installation-source": "dist",
28 | "autoload": {
29 | "psr-4": {
30 | "ParagonIE\\ConstantTime\\": "src/"
31 | }
32 | },
33 | "notification-url": "https://packagist.org/downloads/",
34 | "license": [
35 | "MIT"
36 | ],
37 | "authors": [
38 | {
39 | "name": "Paragon Initiative Enterprises",
40 | "email": "security@paragonie.com",
41 | "homepage": "https://paragonie.com",
42 | "role": "Maintainer"
43 | },
44 | {
45 | "name": "Steve 'Sc00bz' Thomas",
46 | "email": "steve@tobtu.com",
47 | "homepage": "https://www.tobtu.com",
48 | "role": "Original Developer"
49 | }
50 | ],
51 | "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
52 | "keywords": [
53 | "base16",
54 | "base32",
55 | "base32_decode",
56 | "base32_encode",
57 | "base64",
58 | "base64_decode",
59 | "base64_encode",
60 | "bin2hex",
61 | "encoding",
62 | "hex",
63 | "hex2bin",
64 | "rfc4648"
65 | ],
66 | "support": {
67 | "email": "info@paragonie.com",
68 | "issues": "https://github.com/paragonie/constant_time_encoding/issues",
69 | "source": "https://github.com/paragonie/constant_time_encoding"
70 | },
71 | "install-path": "../paragonie/constant_time_encoding"
72 | },
73 | {
74 | "name": "psr/clock",
75 | "version": "1.0.0",
76 | "version_normalized": "1.0.0.0",
77 | "source": {
78 | "type": "git",
79 | "url": "https://github.com/php-fig/clock.git",
80 | "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
81 | },
82 | "dist": {
83 | "type": "zip",
84 | "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
85 | "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
86 | "shasum": ""
87 | },
88 | "require": {
89 | "php": "^7.0 || ^8.0"
90 | },
91 | "time": "2022-11-25T14:36:26+00:00",
92 | "type": "library",
93 | "installation-source": "dist",
94 | "autoload": {
95 | "psr-4": {
96 | "Psr\\Clock\\": "src/"
97 | }
98 | },
99 | "notification-url": "https://packagist.org/downloads/",
100 | "license": [
101 | "MIT"
102 | ],
103 | "authors": [
104 | {
105 | "name": "PHP-FIG",
106 | "homepage": "https://www.php-fig.org/"
107 | }
108 | ],
109 | "description": "Common interface for reading the clock.",
110 | "homepage": "https://github.com/php-fig/clock",
111 | "keywords": [
112 | "clock",
113 | "now",
114 | "psr",
115 | "psr-20",
116 | "time"
117 | ],
118 | "support": {
119 | "issues": "https://github.com/php-fig/clock/issues",
120 | "source": "https://github.com/php-fig/clock/tree/1.0.0"
121 | },
122 | "install-path": "../psr/clock"
123 | },
124 | {
125 | "name": "spomky-labs/otphp",
126 | "version": "11.3.0",
127 | "version_normalized": "11.3.0.0",
128 | "source": {
129 | "type": "git",
130 | "url": "https://github.com/Spomky-Labs/otphp.git",
131 | "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33"
132 | },
133 | "dist": {
134 | "type": "zip",
135 | "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
136 | "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
137 | "shasum": ""
138 | },
139 | "require": {
140 | "ext-mbstring": "*",
141 | "paragonie/constant_time_encoding": "^2.0 || ^3.0",
142 | "php": ">=8.1",
143 | "psr/clock": "^1.0",
144 | "symfony/deprecation-contracts": "^3.2"
145 | },
146 | "require-dev": {
147 | "ekino/phpstan-banned-code": "^1.0",
148 | "infection/infection": "^0.26|^0.27|^0.28|^0.29",
149 | "php-parallel-lint/php-parallel-lint": "^1.3",
150 | "phpstan/phpstan": "^1.0",
151 | "phpstan/phpstan-deprecation-rules": "^1.0",
152 | "phpstan/phpstan-phpunit": "^1.0",
153 | "phpstan/phpstan-strict-rules": "^1.0",
154 | "phpunit/phpunit": "^9.5.26|^10.0|^11.0",
155 | "qossmic/deptrac-shim": "^1.0",
156 | "rector/rector": "^1.0",
157 | "symfony/phpunit-bridge": "^6.1|^7.0",
158 | "symplify/easy-coding-standard": "^12.0"
159 | },
160 | "time": "2024-06-12T11:22:32+00:00",
161 | "type": "library",
162 | "installation-source": "dist",
163 | "autoload": {
164 | "psr-4": {
165 | "OTPHP\\": "src/"
166 | }
167 | },
168 | "notification-url": "https://packagist.org/downloads/",
169 | "license": [
170 | "MIT"
171 | ],
172 | "authors": [
173 | {
174 | "name": "Florent Morselli",
175 | "homepage": "https://github.com/Spomky"
176 | },
177 | {
178 | "name": "All contributors",
179 | "homepage": "https://github.com/Spomky-Labs/otphp/contributors"
180 | }
181 | ],
182 | "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
183 | "homepage": "https://github.com/Spomky-Labs/otphp",
184 | "keywords": [
185 | "FreeOTP",
186 | "RFC 4226",
187 | "RFC 6238",
188 | "google authenticator",
189 | "hotp",
190 | "otp",
191 | "totp"
192 | ],
193 | "support": {
194 | "issues": "https://github.com/Spomky-Labs/otphp/issues",
195 | "source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0"
196 | },
197 | "funding": [
198 | {
199 | "url": "https://github.com/Spomky",
200 | "type": "github"
201 | },
202 | {
203 | "url": "https://www.patreon.com/FlorentMorselli",
204 | "type": "patreon"
205 | }
206 | ],
207 | "install-path": "../spomky-labs/otphp"
208 | },
209 | {
210 | "name": "symfony/deprecation-contracts",
211 | "version": "v3.5.0",
212 | "version_normalized": "3.5.0.0",
213 | "source": {
214 | "type": "git",
215 | "url": "https://github.com/symfony/deprecation-contracts.git",
216 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
217 | },
218 | "dist": {
219 | "type": "zip",
220 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
221 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
222 | "shasum": ""
223 | },
224 | "require": {
225 | "php": ">=8.1"
226 | },
227 | "time": "2024-04-18T09:32:20+00:00",
228 | "type": "library",
229 | "extra": {
230 | "branch-alias": {
231 | "dev-main": "3.5-dev"
232 | },
233 | "thanks": {
234 | "name": "symfony/contracts",
235 | "url": "https://github.com/symfony/contracts"
236 | }
237 | },
238 | "installation-source": "dist",
239 | "autoload": {
240 | "files": [
241 | "function.php"
242 | ]
243 | },
244 | "notification-url": "https://packagist.org/downloads/",
245 | "license": [
246 | "MIT"
247 | ],
248 | "authors": [
249 | {
250 | "name": "Nicolas Grekas",
251 | "email": "p@tchwork.com"
252 | },
253 | {
254 | "name": "Symfony Community",
255 | "homepage": "https://symfony.com/contributors"
256 | }
257 | ],
258 | "description": "A generic function and convention to trigger deprecation notices",
259 | "homepage": "https://symfony.com",
260 | "support": {
261 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
262 | },
263 | "funding": [
264 | {
265 | "url": "https://symfony.com/sponsor",
266 | "type": "custom"
267 | },
268 | {
269 | "url": "https://github.com/fabpot",
270 | "type": "github"
271 | },
272 | {
273 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
274 | "type": "tidelift"
275 | }
276 | ],
277 | "install-path": "../symfony/deprecation-contracts"
278 | }
279 | ],
280 | "dev": true,
281 | "dev-package-names": []
282 | }
283 |
--------------------------------------------------------------------------------