├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── publish-to-redaxo-org.yml ├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── clipboard-copy-element.js ├── qrious.min.js └── qrious.min.js.map ├── boot.php ├── composer.json ├── composer.lock ├── fragments ├── 2fa.login.php ├── 2fa.setup-email.php └── 2fa.setup-totp.php ├── install.php ├── lang └── de_de.lang ├── lib ├── console │ └── 2factor_auth │ │ ├── enforce.php │ │ ├── status.php │ │ └── user.php ├── exception.php ├── method_email.php ├── method_interface.php ├── method_totp.php ├── one_time_password.php ├── one_time_password_config.php └── setup.php ├── package.yml ├── pages ├── index.php ├── settings.php ├── setup.php ├── users.php └── verify.php ├── uninstall.php ├── update.php └── vendor ├── autoload.php ├── composer ├── ClassLoader.php ├── InstalledVersions.php ├── LICENSE ├── autoload_classmap.php ├── autoload_files.php ├── autoload_namespaces.php ├── autoload_psr4.php ├── autoload_real.php ├── autoload_static.php ├── installed.json ├── installed.php └── platform_check.php ├── paragonie └── constant_time_encoding │ ├── LICENSE.txt │ ├── README.md │ ├── composer.json │ └── src │ ├── Base32.php │ ├── Base32Hex.php │ ├── Base64.php │ ├── Base64DotSlash.php │ ├── Base64DotSlashOrdered.php │ ├── Base64UrlSafe.php │ ├── Binary.php │ ├── EncoderInterface.php │ ├── Encoding.php │ ├── Hex.php │ └── RFC4648.php ├── psr └── clock │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ └── ClockInterface.php ├── spomky-labs └── otphp │ ├── LICENSE │ ├── README.md │ ├── SECURITY.md │ ├── composer.json │ └── src │ ├── Factory.php │ ├── FactoryInterface.php │ ├── HOTP.php │ ├── HOTPInterface.php │ ├── InternalClock.php │ ├── OTP.php │ ├── OTPInterface.php │ ├── ParameterTrait.php │ ├── TOTP.php │ ├── TOTPInterface.php │ └── Url.php └── symfony └── deprecation-contracts ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── function.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [staabm] 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-redaxo-org.yml: -------------------------------------------------------------------------------- 1 | # Instructions: https://github.com/FriendsOfREDAXO/installer-action/ 2 | 3 | name: Publish to REDAXO.org 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | redaxo_publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: FriendsOfREDAXO/installer-action@v1 15 | with: 16 | myredaxo-username: ${{ secrets.MYREDAXO_USERNAME }} 17 | myredaxo-api-key: ${{ secrets.MYREDAXO_API_KEY }} 18 | description: ${{ github.event.release.body }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .php_cs.cache 3 | vendor/.DS_Store 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ![2-Faktor-Authentifizierung Weboberfläche](https://github.com/FriendsOfREDAXO/2factor_auth/blob/assets/screen.png?raw=true) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=8.1", 4 | "spomky-labs/otphp": "^11.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "f88e09db1372a5f4287d6302714eab2b", 8 | "packages": [ 9 | { 10 | "name": "paragonie/constant_time_encoding", 11 | "version": "v3.0.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/paragonie/constant_time_encoding.git", 15 | "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", 20 | "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^8" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^9", 28 | "vimeo/psalm": "^4|^5" 29 | }, 30 | "type": "library", 31 | "autoload": { 32 | "psr-4": { 33 | "ParagonIE\\ConstantTime\\": "src/" 34 | } 35 | }, 36 | "notification-url": "https://packagist.org/downloads/", 37 | "license": [ 38 | "MIT" 39 | ], 40 | "authors": [ 41 | { 42 | "name": "Paragon Initiative Enterprises", 43 | "email": "security@paragonie.com", 44 | "homepage": "https://paragonie.com", 45 | "role": "Maintainer" 46 | }, 47 | { 48 | "name": "Steve 'Sc00bz' Thomas", 49 | "email": "steve@tobtu.com", 50 | "homepage": "https://www.tobtu.com", 51 | "role": "Original Developer" 52 | } 53 | ], 54 | "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", 55 | "keywords": [ 56 | "base16", 57 | "base32", 58 | "base32_decode", 59 | "base32_encode", 60 | "base64", 61 | "base64_decode", 62 | "base64_encode", 63 | "bin2hex", 64 | "encoding", 65 | "hex", 66 | "hex2bin", 67 | "rfc4648" 68 | ], 69 | "support": { 70 | "email": "info@paragonie.com", 71 | "issues": "https://github.com/paragonie/constant_time_encoding/issues", 72 | "source": "https://github.com/paragonie/constant_time_encoding" 73 | }, 74 | "time": "2024-05-08T12:36:18+00:00" 75 | }, 76 | { 77 | "name": "psr/clock", 78 | "version": "1.0.0", 79 | "source": { 80 | "type": "git", 81 | "url": "https://github.com/php-fig/clock.git", 82 | "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" 83 | }, 84 | "dist": { 85 | "type": "zip", 86 | "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", 87 | "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", 88 | "shasum": "" 89 | }, 90 | "require": { 91 | "php": "^7.0 || ^8.0" 92 | }, 93 | "type": "library", 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 | "time": "2022-11-25T14:36:26+00:00" 123 | }, 124 | { 125 | "name": "spomky-labs/otphp", 126 | "version": "11.3.0", 127 | "source": { 128 | "type": "git", 129 | "url": "https://github.com/Spomky-Labs/otphp.git", 130 | "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33" 131 | }, 132 | "dist": { 133 | "type": "zip", 134 | "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", 135 | "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", 136 | "shasum": "" 137 | }, 138 | "require": { 139 | "ext-mbstring": "*", 140 | "paragonie/constant_time_encoding": "^2.0 || ^3.0", 141 | "php": ">=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 | -------------------------------------------------------------------------------- /fragments/2fa.login.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | csrfToken->getHiddenField(); ?> 4 |
5 |
6 |
7 | parse('core/login_branding.php'); 10 | ?> 11 | 12 | info_messages) : ?> 13 | 18 | error_messages) : ?> 19 | 24 | 25 | 26 |
27 | 28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 | 54 |
55 |
56 |
57 |
58 | 59 | 64 | 65 | parse('core/login_background.php'); 68 | ?> 69 | -------------------------------------------------------------------------------- /fragments/2fa.setup-email.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | message) : ?> 5 | message ?> 6 | 7 | 8 | success) : ?> 9 |
10 |
11 |

2. addon->i18n('2fa_verify_headline') ?>

12 |
13 |
14 |
15 | csrfToken->getHiddenField(); ?> 16 | 17 | 18 |
19 |
20 | Passwort versand an E-Mail aus dem Userprofil: getEmail()); ?> 21 |
22 |
23 |
24 | 25 |
26 | 29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 | -------------------------------------------------------------------------------- /fragments/2fa.setup-totp.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | message) : ?> 5 | message ?> 6 | 7 | 8 | success) : ?> 9 |
10 |
11 |
12 |
13 |

1. addon->i18n('2fa_setup_scan') ?>

14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 | addon->i18n('copy') ?> 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |

2. addon->i18n('2fa_verify_headline') ?>

37 |
38 |
39 |
40 | csrfToken->getHiddenField(); ?> 41 | 42 |
43 |
44 | 45 |
46 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 65 | 66 | 81 | -------------------------------------------------------------------------------- /install.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/exception.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 | -------------------------------------------------------------------------------- /lib/method_interface.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/setup.php: -------------------------------------------------------------------------------- 1 | =8.1' 41 | redaxo: ^5.14.0 42 | 43 | load: early 44 | -------------------------------------------------------------------------------- /pages/index.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 |
58 | 59 | 60 | 61 |
62 |
63 |
64 | 65 |
66 |
67 | ' . $selectEnforce->get() . ' 68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 | ' . $selectOption->get() . ' 77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | ' . $selectEmailPeriod->get() . ' 86 |
87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 | ' . $selectTOTPPeriod->get() . ' 95 |
96 |
97 | 98 |
99 |
100 | 101 |
102 |
103 | ' . $selectLoginTries->get() . ' 104 |
105 |
106 | 107 |
108 | 109 |
110 |
111 | 112 |
113 |
114 | 115 |
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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 .= ''; 27 | $content .= ''; 28 | $content .= ''; 29 | $content .= ''; 30 | $content .= ''; 31 | $content .= ''; 32 | $content .= ''; 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 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | '; 51 | } 52 | 53 | $content .= implode('', $trs); 54 | 55 | $content .= ''; 56 | $content .= '
idloginstatusmethodtrieslast_tryactions
' . rex_escape($user->getId()) . '' . rex_escape($user->getLogin()) . '' . rex_escape($config->enabled ? 'on' : 'off') . '' . rex_escape($config->method) . '' . rex_escape($user->getValue('one_time_password_tries')) . '' . rex_escape($user->getValue('one_time_password_lasttry')) . 'deactivate
'; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | removeColumn('one_time_password_config') 5 | ->removeColumn('one_time_password_tries') 6 | ->removeColumn('one_time_password_lasttry') 7 | ->ensure(); 8 | -------------------------------------------------------------------------------- /update.php: -------------------------------------------------------------------------------- 1 | includeFile(__DIR__.'/install.php'); 4 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_files.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/symfony/deprecation-contracts/function.php', 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/composer/platform_check.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 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | # Constant-Time Encoding 2 | 3 | [![Build Status](https://github.com/paragonie/constant_time_encoding/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions) 4 | [![Static Analysis](https://github.com/paragonie/constant_time_encoding/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions) 5 | [![Latest Stable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/stable)](https://packagist.org/packages/paragonie/constant_time_encoding) 6 | [![Latest Unstable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/unstable)](https://packagist.org/packages/paragonie/constant_time_encoding) 7 | [![License](https://poser.pugx.org/paragonie/constant_time_encoding/license)](https://packagist.org/packages/paragonie/constant_time_encoding) 8 | [![Downloads](https://img.shields.io/packagist/dt/paragonie/constant_time_encoding.svg)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/paragonie/constant_time_encoding/src/Binary.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 | -------------------------------------------------------------------------------- /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/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/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/psr/clock/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psr/clock", 3 | "description": "Common interface for reading the clock.", 4 | "keywords": ["psr", "psr-20", "time", "clock", "now"], 5 | "homepage": "https://github.com/php-fig/clock", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "PHP-FIG", 10 | "homepage": "https://www.php-fig.org/" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.0 || ^8.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Psr\\Clock\\": "src/" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vendor/psr/clock/src/ClockInterface.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 | -------------------------------------------------------------------------------- /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/spomky-labs/otphp/src/FactoryInterface.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/HOTPInterface.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /vendor/spomky-labs/otphp/src/TOTPInterface.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 | -------------------------------------------------------------------------------- /vendor/symfony/deprecation-contracts/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | The changelog is maintained for all Symfony contracts at the following URL: 5 | https://github.com/symfony/contracts/blob/main/CHANGELOG.md 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /vendor/symfony/deprecation-contracts/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/deprecation-contracts", 3 | "type": "library", 4 | "description": "A generic function and convention to trigger deprecation notices", 5 | "homepage": "https://symfony.com", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Nicolas Grekas", 10 | "email": "p@tchwork.com" 11 | }, 12 | { 13 | "name": "Symfony Community", 14 | "homepage": "https://symfony.com/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=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/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 | --------------------------------------------------------------------------------