diff --git a/plugins/Makefile b/plugins/Makefile index 34975ac1f..210254294 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -1,4 +1,4 @@ -SUBDIRS = ai archive chat desktopnotifications files filesbackendDefault filesbackendSeafile intranet kendox maps mdm meet passwd smime templatesnippets +SUBDIRS = ai archive chat desktopnotifications files filesbackendDefault filesbackendSeafile google2fa intranet kendox maps mdm meet passwd smime templatesnippets .PHONY: subdirs $(SUBDIRS) diff --git a/plugins/google2fa/Makefile b/plugins/google2fa/Makefile new file mode 100644 index 000000000..dc6e51a5f --- /dev/null +++ b/plugins/google2fa/Makefile @@ -0,0 +1,31 @@ +include ../shared.mk + +MSGFMT ?= msgfmt + +JSFILES = js/Google2FAPlugin.js \ + js/data/Configuration.js \ + js/data/ResponseHandler.js \ + js/settings/Category.js \ + js/settings/GeneralSettingsWidget.js + +COPYFILES = manifest.xml config.php \ + $(wildcard resources/css/*.css) \ + $(shell find resources/icons/ -type f \( -name '*.png' -o -name '*.gif' -o -name '*.jpg' \) ) \ + $(shell find php/ -type f \! -name '.*' -not -path '*/tests/*' -not -path '*/examples/*' -not -path '*/vendor/bin/*' -not -path '*/.github/*' -not -iname '*.py' -not -iname 'naturalselection') + +CSSDEPLOY = $(DESTDIR)/resources/css/ + +COPYFILESDEST = $(addprefix $(DESTDIR)/, $(COPYFILES)) + +all: $(COPYFILESDEST) $(JSDEPLOY)/google2fa.js $(CSSDEPLOY)/google2fa.css + +$(JSDEPLOY)/google2fa.js: $(JSFILES) + mkdir -p $(DESTDIR)/js + cat $(JSFILES) > $(@:.js=-debug.js) + $(JSCOMPILER) $(@:.js=-debug.js) --output $@ \ + --source-map "url='$(shell basename $@.map)'" \ + $(JSOPTIONS) + +$(CSSDEPLOY)/google2fa.css: $(CSSFILES) + mkdir -p $(CSSDEPLOY) + cat $(CSSFILES) > $(CSSDEPLOY)/google2fa.css diff --git a/plugins/google2fa/README.md b/plugins/google2fa/README.md new file mode 100644 index 000000000..475b47d15 --- /dev/null +++ b/plugins/google2fa/README.md @@ -0,0 +1,26 @@ +# grommunio Google 2FA Plugin + +This plugin will integrate two-factor authorization with an external OTP provider (Google Authenticator App) in grommunio-web. + +# Configuration + +The plugin configuration can be found in the **'config.php'** file. + +```const PLUGIN_GOOGLE2FA_ENABLE = true;``` + +* Enable/Disable plugin +* Default for new users, this doesn't mean the activation of two-factor authentication! + +```const PLUGIN_GOOGLE2FA_ALWAYS_ENABLED = false;``` + +* Enable plugin when plugin is loading, the user can't disable the plugin. + +```const PLUGIN_GOOGLE2FA_ACTIVATE = false;``` + +* Activate / deactivate 2FA +* Default for new users. + +```const PLUGIN_GOOGLE2FA_ALWAYS_ACTIVATED = false;``` + +* Activate 2FA when plugin is loading. + diff --git a/plugins/google2fa/config.php b/plugins/google2fa/config.php new file mode 100644 index 000000000..bf0f833fc --- /dev/null +++ b/plugins/google2fa/config.php @@ -0,0 +1,27 @@ +" + + _('After activation you need next to your password an one-time code to log in.') + "
" + "
" + + _('To generate an one-time code, you have to configure a second device, usually a smartphone.') + "
 " + }, { + xtype: "button", + text: _('Configuration'), + handler: this.openConfigurationDialog, + scope: this, + width: 250 + }, { + xtype: "displayfield", + hideLabel: true, + value: "
" + _('Activate or deactivate the two-factor authentication.') + "
 " + }, { + xtype: "displayfield", + fieldLabel: _('Current status'), + value: this.getStatus(), + htmlEncode: true, + ref: "status", + width: 250 + }, { + xtype: "displayfield", + hideLabel: true, + value: "" + }, { + xtype: "button", + text: _('Activate / Deactivate'), + handler: this.activate, + scope: this, + width: 250 + }, { + xtype: "displayfield", + hideLabel: true, + value: "
" + _('You can completely reset the configuration.') + "
" + + _('This deletes the secret key and deactivates the two-factor authentication.') + "
" + + _('If fundamental changes in the configuration were made, for example the encryption type, this step might be useful.') + "
 " + }, { + xtype: "button", + text: _('Reset'), + handler: this.openResetConfigurationDialog, + scope: this, + width: 250 + }] + }); + Zarafa.plugins.google2fa.settings.GeneralSettingsWidget.superclass.constructor.call(this, config); + }, + getStatus: function () { + return (Zarafa.plugins.google2fa.data.Configuration.isActivated() ? _('Activated') : _('Deactivated')); + }, + openResetConfigurationDialog: function () { + Zarafa.common.dialogs.MessageBox.show({ + title: _('Reset'), + msg: _('Do you really want to reset the configuration?'), + icon: Zarafa.common.dialogs.MessageBox.QUESTION, + buttons: Zarafa.common.dialogs.MessageBox.YESNO, + fn: this.resetConfiguration, + scope: this + }); + }, + resetConfiguration: function (a) { + if (a === "yes") { + container.getRequest().singleRequest("google2famodule", "resetconfiguration", {}, new Zarafa.plugins.google2fa.data.ResponseHandler({ + successCallback: this.openResetConfigurationFinishDialog.createDelegate(this) + })); + } + }, + openResetConfigurationFinishDialog: function (a) { + Zarafa.plugins.google2fa.data.Configuration.gotIsActivated(a); + this.status.setValue(this.getStatus()); + Zarafa.common.dialogs.MessageBox.show({ + title: _('Reset'), + msg: _('The configuration has been reset.'), + icon: Zarafa.common.dialogs.MessageBox.INFO, + buttons: Zarafa.common.dialogs.MessageBox.OK, + scope: this + }); + }, + openConfigurationDialog: function () { + container.getRequest().singleRequest("google2famodule", "getsecret", {}, new Zarafa.plugins.google2fa.data.ResponseHandler({ + successCallback: this.openConfigurationDialogX.createDelegate(this) + })); + }, + openConfigurationDialogX: function (a) { + let secret = atob(a.secret); + let qRCodeGoogleUrl = atob(a.qRCodeGoogleUrl); + Zarafa.common.dialogs.MessageBox.addCustomButtons({ + title: _('Configuration'), + msg: _('Please install an authentication App on second device:') + "
" + + _('Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)') + "
" + + _('Open and configure the authentication app by scanning the QR code below.') + "

" + + "

" + + _('Alternatively, you can manually create an account with the following information.') + "

" + + _('Application') + ": " + a.application + "
" + + _('Account') + ": " + a.username + "
" + _('Key') + ": " + secret + "
" + + _('Afterwards test the function with a generated code to ensure that the configurations are correct.'), + fn: this.openVerifyCodeDialog, + customButton: [{ + text: _('Test generated code'), + name: "verify" + }], + scope: this, + width: 500 + }); + }, + openVerifyCodeDialog: function (a) { + if (a === "verify") + Zarafa.common.dialogs.MessageBox.prompt(_('Test generated code'), _('Please enter code'), this.verifyCode, this); + }, + verifyCode: function (a, b) { + if (a === "ok") { + container.getRequest().singleRequest("google2famodule", "verifycode", {code: b}, new Zarafa.plugins.google2fa.data.ResponseHandler({ + successCallback: this.openResponseDialog.createDelegate(this) + })); + } + }, + openResponseDialog: function (a) { + if (a.isCodeOK) { + Zarafa.common.dialogs.MessageBox.show({ + title: _('Test generated code'), + msg: _('Valid code, you can use the two-factor authentication.'), + icon: Zarafa.common.dialogs.MessageBox.INFO, + buttons: Zarafa.common.dialogs.MessageBox.OK, + scope: this, + width: 350 + }); + } else { + Zarafa.common.dialogs.MessageBox.show({ + title: _('Test generated code'), + msg: _('Invalid code, please check code.') + "
" + + _('You can use a code only one-time.') + "
" + + _('Please make sure that time from of server and second device are correct.'), + icon: Zarafa.common.dialogs.MessageBox.ERROR, + buttons: Zarafa.common.dialogs.MessageBox.OK, + scope: this, + width: 350 + }); + } + }, + activate: function () { + container.getRequest().singleRequest("google2famodule", "activate", {}, new Zarafa.plugins.google2fa.data.ResponseHandler({ + successCallback: this.setStatus.createDelegate(this) + })); + }, + setStatus: function (a) { + Zarafa.plugins.google2fa.data.Configuration.gotIsActivated(a); + this.status.setValue(this.getStatus()); + container.getNotifier().notify("info.saved", _('Two-factor authentication') + ": " + this.getStatus(), + _('Current status') + ": " + this.getStatus()); + } +}); +Ext.reg("Zarafa.plugins.google2fa.generalsettingswidget", Zarafa.plugins.google2fa.settings.GeneralSettingsWidget); diff --git a/plugins/google2fa/manifest.xml b/plugins/google2fa/manifest.xml new file mode 100644 index 000000000..c85848025 --- /dev/null +++ b/plugins/google2fa/manifest.xml @@ -0,0 +1,39 @@ + + + + + 1.0 + google2fa + Two-factor authentication + grommunio GmbH + http://www.grommunio.com + Integrate 2FA via Google Authenticator into grommunio web + + + config.php + + + + + + php/class.google2famodule.php + php/plugin.google2fa.php + + + js/google2fa.js?debug=false + js/google2fa-debug.js?debug=true + js/data/Configuration.js + js/data/ResponseHandler.js + js/Google2FAPlugin.js + js/settings/Category.js + js/settings/GeneralSettingsWidget.js + + + resources/css/google2fa.css?debug=false + resources/css/google2fa.css?debug=true + resources/css/google2fa.css?debug=true&source=true + + + + + diff --git a/plugins/google2fa/php/class.google2fadata.settings.php b/plugins/google2fa/php/class.google2fadata.settings.php new file mode 100644 index 000000000..4a638112c --- /dev/null +++ b/plugins/google2fa/php/class.google2fadata.settings.php @@ -0,0 +1,55 @@ +get("zarafa/v1/plugins/google2fa/secret_key")); + } + + /** + * Set secret key + * + * @param string $secret key + * @throws Exception + */ + public static function setSecret(string $secret) + { + $GLOBALS["settings"]->set("zarafa/v1/plugins/google2fa/secret_key", base64_encode($secret)); + $GLOBALS["settings"]->saveSettings(); + } + + /** + * Two-factor authentication activated + * + * @return boolean + */ + public static function isActivated(): bool + { + return $GLOBALS["settings"]->get("zarafa/v1/plugins/google2fa/activate"); + } + + /** + * Activate or deactivate two-factor authentication + * + * @param boolean $activate activation true/false + */ + public static function setActivate(bool $activate) + { + $GLOBALS["settings"]->set("zarafa/v1/plugins/google2fa/activate", $activate); + $GLOBALS["settings"]->saveSettings(); + } +} diff --git a/plugins/google2fa/php/class.google2famodule.php b/plugins/google2fa/php/class.google2famodule.php new file mode 100644 index 000000000..77685040d --- /dev/null +++ b/plugins/google2fa/php/class.google2famodule.php @@ -0,0 +1,193 @@ +addActionData("resetconfiguration", $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + return true; + } + + /** + * Toggle activate/deactivate two-factor authentication + * + * @access private + * @return boolean + * @throws Exception + */ + private function activate(): bool + { + $isActivated = Google2FAData::isActivated(); + Google2FAData::setActivate(!$isActivated); + $response = array(); + $response['isActivated'] = !$isActivated; + $this->addActionData("activate", $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + return true; + } + + /** + * Send if two-factor authentication is activated + * + * @access private + * @return boolean + * @throws Exception + */ + private function isActivated(): bool + { + $isActivated = Google2FAData::isActivated(); + $response = array(); + $response['isActivated'] = $isActivated; + $this->addActionData("isactivated", $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + return true; + } + + /** + * Verify code + * + * @access private + * @param array $actionData + * @return boolean + * @throws Exception + */ + private function verifyCode(array $actionData): bool + { + $code = $actionData['code']; + $encryptionStore = EncryptionStore::getInstance(); + $user = $encryptionStore->get('username'); + $this->otp = TOTP::createFromSecret(Google2FAData::getSecret()); + $this->otp->setLabel($user); + $isCodeOK = $this->otp->verify($code); + if ($isCodeOK) { + Google2FAData::setActivate(true); + } + $response['isCodeOK'] = $isCodeOK; + $this->addActionData("verifycode", $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + return true; + } + + /** + * Send secret key + * + * @access private + * @return boolean + * @throws Exception + */ + private function getSecret(): bool + { + $secret = Google2FAData::getSecret(); + $encryptionStore = EncryptionStore::getInstance(); + $user = $encryptionStore->get('username'); + $this->otp = TOTP::createFromSecret($secret); + $this->otp->setLabel($user); + $response = array(); + if ($secret === "") { + $secret = $this->createSecret(); + } + $response['qRCodeGoogleUrl'] = base64_encode($this->otp->getQrCodeUri( + 'https://api.qrserver.com/v1/create-qr-code/?color=5330FF&bgcolor=70FF7E&data=[DATA]&qzone=2&margin=0&size=300x300&ecc=M', + '[DATA]') + ); + $response['secret'] = base64_encode($secret); + $response['username'] = $user; + $this->addActionData("getsecret", $response); + $GLOBALS["bus"]->addData($this->getResponseData()); + return true; + } + + /** + * Create and save new secret key + * + * @access private + * @return string + * @throws Exception + */ + private function createSecret(): string + { + $secret = trim(Base32::encodeUpper(random_bytes(128)), '='); + Google2FAData::setSecret($secret); + return $secret; + } + + /** + * Executes all the actions in the $data variable. + * + * @access public + * @return boolean true on success or false on failure. + */ + public function execute(): bool + { + $result = false; + foreach ($this->data as $actionType => $actionData) { + if (isset($actionType)) { + try { + switch ($actionType) { + case "resetconfiguration": + $result = $this->resetConfiguration(); + break; + case "getsecret": + $result = $this->getSecret(); + break; + case "activate": + $result = $this->activate(); + break; + case "isactivated": + $result = $this->isActivated(); + break; + case "verifycode": + $result = $this->verifyCode($actionData); + break; + default: + $this->handleUnknownActionType($actionType); + } + } catch (Exception $e) { + $mess = $e->getFile() . ":" . $e->getLine() . "
" . $e->getMessage(); + error_log("[google2fa]: " . $mess); + $this->sendFeedback(false, array( + 'type' => ERROR_GENERAL, + 'info' => array('original_message' => $mess, 'display_message' => $mess) + )); + } + } + } + return $result; + } +} diff --git a/plugins/google2fa/php/composer.json b/plugins/google2fa/php/composer.json new file mode 100644 index 000000000..8be2c791d --- /dev/null +++ b/plugins/google2fa/php/composer.json @@ -0,0 +1,7 @@ +{ + "minimum-stability" : "stable", + "prefer-stable" : true, + "require": { + "spomky-labs/otphp": "^11.5" + } +} \ No newline at end of file diff --git a/plugins/google2fa/php/composer.lock b/plugins/google2fa/php/composer.lock new file mode 100644 index 000000000..98eff31d5 --- /dev/null +++ b/plugins/google2fa/php/composer.lock @@ -0,0 +1,277 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "fc7c31c9c9d956f58cd91112938e95e8", + "packages": [ + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "spomky-labs/otphp", + "version": "11.5.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "877683d6352b80cdc7020fd43a725629c2524435" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/877683d6352b80cdc7020fd43a725629c2524435", + "reference": "877683d6352b80cdc7020fd43a725629c2524435", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OTPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "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", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/otphp/issues", + "source": "https://github.com/Spomky-Labs/otphp/tree/11.5.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2026-06-06T23:41:24+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/plugins/google2fa/php/login.php b/plugins/google2fa/php/login.php new file mode 100644 index 000000000..ab1178ee1 --- /dev/null +++ b/plugins/google2fa/php/login.php @@ -0,0 +1,273 @@ +get('username'); + +$cookieValue = $user; // @todo: generate random value +$cookieName = str_replace(['@', '.'], '-', $user) . "-grommunio-web-google2fa-remember"; + +if(isset($_COOKIE[$cookieName])) { + header('Location: logon.php', true, 303); +} + +/* + * Get the favicon either from theme or use the default. + * + * @param string theme the users theme + * @return string favicon + */ +function getFavicon($theme) +{ + + if ($theme) { + $favicon = Theming::getFavicon($theme); + } + + if (!isset($favicon) || $favicon === false) { + $favicon = 'client/resources/images/favicon.ico?kv2.2.0'; + } + + return $favicon; +} + +$favicon = getFavicon(Theming::getActiveTheme()); +?> + + + + + + + + + + <?php echo $webappTitle; ?> + + + + + + + + + + '; + else + echo ' '; + + $theme = Theming::getActiveTheme(); // Add the styling of the theme + $css = Theming::getCss($theme); + if (count($css)) { + foreach ($css as $file) { + echo ' '; + } + } + ?> + + + + + +
+
+
+
+ +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+
+ + + + diff --git a/plugins/google2fa/php/logon.php b/plugins/google2fa/php/logon.php new file mode 100644 index 000000000..b20c5201b --- /dev/null +++ b/plugins/google2fa/php/logon.php @@ -0,0 +1,51 @@ +get('google2FASecret'); +$user = $encryptionStore->get('username'); +$verification = false; + +$cookieValue = $user; // @todo: generate random value +$cookieName = str_replace(['@', '.'], '-', $user) . "-grommunio-web-google2fa-remember"; + +if(!isset($_COOKIE[$cookieName])) { + $otp = TOTP::createFromSecret($secret); + $otp->setLabel($user); + + if ($otp->verify($code)) { + $verification = true; + if ($remember) { + setcookie($cookieName, $cookieValue, ['expires' => time() + (86400 * 30), 'path' => "/", 'secure' => true, 'httponly' => true]); + } + } +} else { + if ($_COOKIE[$cookieName] == $user) { + $verification = true; + } +} + +if ($verification) { + $_SESSION['google2FACode'] = $code; // to disable code + $_SESSION['google2FALoggedOn'] = TRUE; // 2FA successful + header('Location: ../../../index.php', true, 303); +} else { + $_SESSION['google2FALoggedOn'] = FALSE; // login not successful + header('Location: login.php', true, 303); +} diff --git a/plugins/google2fa/php/plugin.google2fa.php b/plugins/google2fa/php/plugin.google2fa.php new file mode 100644 index 000000000..25c290df1 --- /dev/null +++ b/plugins/google2fa/php/plugin.google2fa.php @@ -0,0 +1,104 @@ +registerHook('server.core.settings.init.before'); + $this->registerHook('server.index.load.main.before'); + } + + /** + * Function is executed when a hook is triggered by the PluginManager + * + * @param string $eventID the id of the triggered hook + * @param mixed $data object(s) related to the hook + */ + public function execute($eventID, &$data) + { + switch ($eventID) { + case 'server.core.settings.init.before' : + $this->injectPluginSettings($data); + break; + + case 'server.index.load.main.before' : // don't use the logon trigger because we need the settings + try { + if (PLUGIN_GOOGLE2FA_ALWAYS_ENABLED) { + $GLOBALS["settings"]->set('zarafa/v1/plugins/google2fa/enable', true); + $GLOBALS["settings"]->saveSettings(); + } + + if (PLUGIN_GOOGLE2FA_ALWAYS_ACTIVATED) + Google2FAData::setActivate(true); + + // Check, if user has enabled plugin and has activated 2FA + if (!$GLOBALS["settings"]->get('zarafa/v1/plugins/google2fa/enable') + || !Google2FAData::isActivated()) + break; + + // Check, if token authorisation is already done (example: attachment-upload) + if (array_key_exists('google2FALoggedOn', $_SESSION) && $_SESSION['google2FALoggedOn']) { + + // Login successful - save or remove code + if (isset($_SESSION['google2FACode'])) { + unset($_SESSION['google2FACode']); + } + break; + } + + // Save data in session for token authentication with login.php and logon.php + $encryptionStore = EncryptionStore::getInstance(); + $encryptionStore->add('google2FASecret', Google2FAData::getSecret()); + $_SESSION['google2FAEcho']['txtCodePlaceholder'] = dgettext('plugin_google2fa', 'Code'); + $_SESSION['google2FAEcho']['rememberMe'] = dgettext('plugin_google2fa', 'Remember me for 30 days'); + $_SESSION['google2FAEcho']['msgInvalidCode'] = dgettext('plugin_google2fa', 'Invalid code. Please check code.'); + $_SESSION['google2FAEcho']['butOk'] = dgettext('plugin_google2fa', 'Ok'); + $_SESSION['google2FAEcho']['butCancel'] = dgettext('plugin_google2fa', 'Cancel'); + + // Call token login page + header('Location: plugins/google2fa/php/login.php', true, 303); // delete GLOBALS, go to token page + exit; // don't execute header-function in index.php + + } catch (Exception $e) { + $mess = $e->getFile() . ":" . $e->getLine() . "
" . $e->getMessage(); + error_log("[google2fa]: " . $mess); + die($mess); + } + } + } + + /** + * Inject default plugin settings + * + * @param mixed $data Reference to the data of the triggered hook + */ + function injectPluginSettings(&$data) + { + $data['settingsObj']->addSysAdminDefaults(array( + 'zarafa' => array( + 'v1' => array( + 'plugins' => array( + 'google2fa' => array( + 'enable' => PLUGIN_GOOGLE2FA_ENABLE, + 'user_disable_allowed' => !PLUGIN_GOOGLE2FA_ALWAYS_ENABLED, + 'secret_key' => '', + 'activate' => PLUGIN_GOOGLE2FA_ACTIVATE + ) + ) + ) + ) + )); + } +} diff --git a/plugins/google2fa/php/vendor/autoload.php b/plugins/google2fa/php/vendor/autoload.php new file mode 100644 index 000000000..6072f0338 --- /dev/null +++ b/plugins/google2fa/php/vendor/autoload.php @@ -0,0 +1,22 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/plugins/google2fa/php/vendor/composer/InstalledVersions.php b/plugins/google2fa/php/vendor/composer/InstalledVersions.php new file mode 100644 index 000000000..2052022fd --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/InstalledVersions.php @@ -0,0 +1,396 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/plugins/google2fa/php/vendor/composer/LICENSE b/plugins/google2fa/php/vendor/composer/LICENSE new file mode 100644 index 000000000..f27399a04 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/plugins/google2fa/php/vendor/composer/autoload_classmap.php b/plugins/google2fa/php/vendor/composer/autoload_classmap.php new file mode 100644 index 000000000..0fb0a2c19 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/plugins/google2fa/php/vendor/composer/autoload_files.php b/plugins/google2fa/php/vendor/composer/autoload_files.php new file mode 100644 index 000000000..be8e1c3f2 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/autoload_files.php @@ -0,0 +1,10 @@ + $vendorDir . '/symfony/deprecation-contracts/function.php', +); diff --git a/plugins/google2fa/php/vendor/composer/autoload_namespaces.php b/plugins/google2fa/php/vendor/composer/autoload_namespaces.php new file mode 100644 index 000000000..15a2ff3ad --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/psr/clock/src'), + 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'), + 'OTPHP\\' => array($vendorDir . '/spomky-labs/otphp/src'), +); diff --git a/plugins/google2fa/php/vendor/composer/autoload_real.php b/plugins/google2fa/php/vendor/composer/autoload_real.php new file mode 100644 index 000000000..b4a3f584a --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/autoload_real.php @@ -0,0 +1,50 @@ +register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInit7e01b166e8368bc63de473c7ff18adec::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/plugins/google2fa/php/vendor/composer/autoload_static.php b/plugins/google2fa/php/vendor/composer/autoload_static.php new file mode 100644 index 000000000..fcc8fc092 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/autoload_static.php @@ -0,0 +1,53 @@ + __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'P' => + array ( + 'Psr\\Clock\\' => 10, + 'ParagonIE\\ConstantTime\\' => 23, + ), + 'O' => + array ( + 'OTPHP\\' => 6, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Psr\\Clock\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/clock/src', + ), + 'ParagonIE\\ConstantTime\\' => + array ( + 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src', + ), + 'OTPHP\\' => + array ( + 0 => __DIR__ . '/..' . '/spomky-labs/otphp/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit7e01b166e8368bc63de473c7ff18adec::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit7e01b166e8368bc63de473c7ff18adec::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit7e01b166e8368bc63de473c7ff18adec::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/plugins/google2fa/php/vendor/composer/installed.json b/plugins/google2fa/php/vendor/composer/installed.json new file mode 100644 index 000000000..d3b3a95e9 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/installed.json @@ -0,0 +1,276 @@ +{ + "packages": [ + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "version_normalized": "3.1.3.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "time": "2025-09-24T15:06:41+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "install-path": "../paragonie/constant_time_encoding" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "time": "2022-11-25T14:36:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "install-path": "../psr/clock" + }, + { + "name": "spomky-labs/otphp", + "version": "11.5.0", + "version_normalized": "11.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "877683d6352b80cdc7020fd43a725629c2524435" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/877683d6352b80cdc7020fd43a725629c2524435", + "reference": "877683d6352b80cdc7020fd43a725629c2524435", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0|^8.0" + }, + "time": "2026-06-06T23:41:24+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "OTPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "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", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/otphp/issues", + "source": "https://github.com/Spomky-Labs/otphp/tree/11.5.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "install-path": "../spomky-labs/otphp" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "version_normalized": "3.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2026-04-13T15:52:40+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/plugins/google2fa/php/vendor/composer/installed.php b/plugins/google2fa/php/vendor/composer/installed.php new file mode 100644 index 000000000..4a1b97f22 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/installed.php @@ -0,0 +1,59 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '180ba31734931a75da7f80512b93360f1d8a224d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '180ba31734931a75da7f80512b93360f1d8a224d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'paragonie/constant_time_encoding' => array( + 'pretty_version' => 'v3.1.3', + 'version' => '3.1.3.0', + 'reference' => 'd5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77', + 'type' => 'library', + 'install_path' => __DIR__ . '/../paragonie/constant_time_encoding', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/clock' => array( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/clock', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'spomky-labs/otphp' => array( + 'pretty_version' => '11.5.0', + 'version' => '11.5.0.0', + 'reference' => '877683d6352b80cdc7020fd43a725629c2524435', + 'type' => 'library', + 'install_path' => __DIR__ . '/../spomky-labs/otphp', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v3.7.0', + 'version' => '3.7.0.0', + 'reference' => '50f59d1f3ca46d41ac911f97a78626b6756af35b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/plugins/google2fa/php/vendor/composer/platform_check.php b/plugins/google2fa/php/vendor/composer/platform_check.php new file mode 100644 index 000000000..2beb14918 --- /dev/null +++ b/plugins/google2fa/php/vendor/composer/platform_check.php @@ -0,0 +1,25 @@ += 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + 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; + } + } + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) + ); +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/LICENSE.txt b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/LICENSE.txt new file mode 100644 index 000000000..91acaca67 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/LICENSE.txt @@ -0,0 +1,48 @@ +The MIT License (MIT) + +Copyright (c) 2016 - 2022 Paragon Initiative Enterprises + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ +This library was based on the work of Steve "Sc00bz" Thomas. +------------------------------------------------------------------------------ + +The MIT License (MIT) + +Copyright (c) 2014 Steve Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/README.md b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/README.md new file mode 100644 index 000000000..211f0dc9f --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/README.md @@ -0,0 +1,88 @@ +# Constant-Time Encoding + +[![Build Status](https://github.com/paragonie/constant_time_encoding/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions) +[![Static Analysis](https://github.com/paragonie/constant_time_encoding/actions/workflows/psalm.yml/badge.svg)](https://github.com/paragonie/constant_time_encoding/actions) +[![Latest Stable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/stable)](https://packagist.org/packages/paragonie/constant_time_encoding) +[![Latest Unstable Version](https://poser.pugx.org/paragonie/constant_time_encoding/v/unstable)](https://packagist.org/packages/paragonie/constant_time_encoding) +[![License](https://poser.pugx.org/paragonie/constant_time_encoding/license)](https://packagist.org/packages/paragonie/constant_time_encoding) +[![Downloads](https://img.shields.io/packagist/dt/paragonie/constant_time_encoding.svg)](https://packagist.org/packages/paragonie/constant_time_encoding) + +Based on the [constant-time base64 implementation made by Steve "Sc00bz" Thomas](https://github.com/Sc00bz/ConstTimeEncoding), +this library aims to offer character encoding functions that do not leak +information about what you are encoding/decoding via processor cache +misses. Further reading on [cache-timing attacks](http://blog.ircmaxell.com/2014/11/its-all-about-time.html). + +Our fork offers the following enhancements: + +* `mbstring.func_overload` resistance +* Unit tests +* Composer- and Packagist-ready +* Base16 encoding +* Base32 encoding +* Uses `pack()` and `unpack()` instead of `chr()` and `ord()` + +## PHP Version Requirements + +Version 3 of this library should work on **PHP 8** or newer. + +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). + +For PHP 5 support, see [the v1.x branch](https://github.com/paragonie/constant_time_encoding/tree/v1.x). + +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`. + +## How to Install + +```sh +composer require paragonie/constant_time_encoding +``` + +## How to Use + +```php +use ParagonIE\ConstantTime\Encoding; + +// possibly (if applicable): +// require 'vendor/autoload.php'; + +$data = random_bytes(32); +echo Encoding::base64Encode($data), "\n"; +echo Encoding::base32EncodeUpper($data), "\n"; +echo Encoding::base32Encode($data), "\n"; +echo Encoding::hexEncode($data), "\n"; +echo Encoding::hexEncodeUpper($data), "\n"; +``` + +Example output: + +``` +1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE= +2VMKKPSHSWVCVZJ6E7SONRY3ZXCNG3GE6ZZFU7TGJSX7KUKFNLAQ==== +2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq==== +d558a53e4795aa2ae53e27e4e6c71bcdc4d36cc4f6725a7e664caff551456ac1 +D558A53E4795AA2AE53E27E4E6C71BDCC4D36CC4F6725A7E664CAFF551456AC1 +``` + +If you only need a particular variant, you can just reference the +required class like so: + +```php +use ParagonIE\ConstantTime\Base64; +use ParagonIE\ConstantTime\Base32; + +$data = random_bytes(32); +echo Base64::encode($data), "\n"; +echo Base32::encode($data), "\n"; +``` + +Example output: + +``` +1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE= +2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq==== +``` + +## Support Contracts + +If your company uses this library in their products or services, you may be +interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise). diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/composer.json b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/composer.json new file mode 100644 index 000000000..11fad5e63 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/composer.json @@ -0,0 +1,67 @@ +{ + "name": "paragonie/constant_time_encoding", + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base64", + "encoding", + "rfc4648", + "base32", + "base16", + "hex", + "bin2hex", + "hex2bin", + "base64_encode", + "base64_decode", + "base32_encode", + "base32_decode" + ], + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "support": { + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "email": "info@paragonie.com", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "ParagonIE\\ConstantTime\\Tests\\": "tests/" + } + }, + "scripts": { + "mutation-test": "infection" + }, + "config": { + "process-timeout": 0, + "allow-plugins": { + "infection/extension-installer": true + } + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base32.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base32.php new file mode 100644 index 000000000..379552af5 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base32.php @@ -0,0 +1,558 @@ + 96 && $src < 123) $ret += $src - 97 + 1; // -64 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96); + + // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 + $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 5-bit integers + * into 8-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return int + * @api + */ + protected static function decode5BitsUpper(int $src): int + { + $ret = -1; + + // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 + $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * @param int $src + * @return string + * @api + */ + protected static function encode5Bits(int $src): string + { + $diff = 0x61; + + // if ($src > 25) $ret -= 72; + $diff -= ((25 - $src) >> 8) & 73; + + return pack('C', $src + $diff); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return string + * @api + */ + protected static function encode5BitsUpper(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $ret -= 40; + $diff -= ((25 - $src) >> 8) & 41; + + return pack('C', $src + $diff); + } + + /** + * @param string $encodedString + * @param bool $upper + * @return string + * @api + */ + public static function decodeNoPadding( + #[SensitiveParameter] + string $encodedString, + bool $upper = false + ): string { + $srcLen = strlen($encodedString); + if ($srcLen === 0) { + return ''; + } + if (($srcLen & 7) === 0) { + for ($j = 0; $j < 7 && $j < $srcLen; ++$j) { + if ($encodedString[$srcLen - $j - 1] === '=') { + throw new InvalidArgumentException( + "decodeNoPadding() doesn't tolerate padding" + ); + } + } + } + return static::doDecode( + $encodedString, + $upper, + true + ); + } + + /** + * Base32 decoding + * + * @param string $src + * @param bool $upper + * @param bool $strictPadding + * @return string + * + * @throws TypeError + */ + protected static function doDecode( + #[SensitiveParameter] + string $src, + bool $upper = false, + bool $strictPadding = false + ): string { + // We do this to reduce code duplication: + $method = $upper + ? 'decode5BitsUpper' + : 'decode5Bits'; + + // Remove padding + $srcLen = strlen($src); + if ($srcLen === 0) { + return ''; + } + if ($strictPadding) { + if (($srcLen & 7) === 0) { + for ($j = 0; $j < 7; ++$j) { + if ($src[$srcLen - 1] === '=') { + $srcLen--; + } else { + break; + } + } + } + if (($srcLen & 7) === 1) { + throw new RangeException( + 'Incorrect padding' + ); + } + } else { + $src = rtrim($src, '='); + $srcLen = strlen($src); + } + + $err = 0; + $dest = ''; + // Main loop (no padding): + for ($i = 0; $i + 8 <= $srcLen; $i += 8) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($src, $i, 8)); + /** @var int $c0 */ + $c0 = static::$method($chunk[1]); + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + /** @var int $c6 */ + $c6 = static::$method($chunk[7]); + /** @var int $c7 */ + $c7 = static::$method($chunk[8]); + + $dest .= pack( + 'CCCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff, + (($c6 << 5) | ($c7 ) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8; + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($src, $i, $srcLen - $i)); + /** @var int $c0 */ + $c0 = static::$method($chunk[1]); + + if ($i + 6 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + /** @var int $c6 */ + $c6 = static::$method($chunk[7]); + + $dest .= pack( + 'CCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8; + if ($strictPadding) { + $err |= ($c6 << 5) & 0xff; + } + } elseif ($i + 5 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + + $dest .= pack( + 'CCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8; + } elseif ($i + 4 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + + $dest .= pack( + 'CCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8; + if ($strictPadding) { + $err |= ($c4 << 7) & 0xff; + } + } elseif ($i + 3 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + + $dest .= pack( + 'CC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3) >> 8; + if ($strictPadding) { + $err |= ($c3 << 4) & 0xff; + } + } elseif ($i + 2 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + + $dest .= pack( + 'CC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2) >> 8; + if ($strictPadding) { + $err |= ($c2 << 6) & 0xff; + } + } elseif ($i + 1 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + + $dest .= pack( + 'C', + (($c0 << 3) | ($c1 >> 2) ) & 0xff + ); + $err |= ($c0 | $c1) >> 8; + if ($strictPadding) { + $err |= ($c1 << 6) & 0xff; + } + } else { + $dest .= pack( + 'C', + (($c0 << 3) ) & 0xff + ); + $err |= ($c0) >> 8; + } + } + $check = ($err === 0); + if (!$check) { + throw new RangeException( + 'Base32::doDecode() only expects characters in the correct base32 alphabet' + ); + } + return $dest; + } + + /** + * Base32 Encoding + * + * @param string $src + * @param bool $upper + * @param bool $pad + * @return string + * @throws TypeError + */ + protected static function doEncode( + #[SensitiveParameter] + string $src, + bool $upper = false, + bool $pad = true + ): string { + // We do this to reduce code duplication: + $method = $upper + ? 'encode5BitsUpper' + : 'encode5Bits'; + + $dest = ''; + $srcLen = strlen($src); + + // Main loop (no padding): + for ($i = 0; $i + 5 <= $srcLen; $i += 5) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($src, $i, 5)); + $b0 = $chunk[1]; + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $b3 = $chunk[4]; + $b4 = $chunk[5]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . + static::$method((($b3 >> 2) ) & 31) . + static::$method((($b3 << 3) | ($b4 >> 5)) & 31) . + static::$method( $b4 & 31); + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($src, $i, $srcLen - $i)); + $b0 = $chunk[1]; + if ($i + 3 < $srcLen) { + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $b3 = $chunk[4]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . + static::$method((($b3 >> 2) ) & 31) . + static::$method((($b3 << 3) ) & 31); + if ($pad) { + $dest .= '='; + } + } elseif ($i + 2 < $srcLen) { + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) ) & 31); + if ($pad) { + $dest .= '==='; + } + } elseif ($i + 1 < $srcLen) { + $b1 = $chunk[2]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) ) & 31); + if ($pad) { + $dest .= '===='; + } + } else { + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method( ($b0 << 2) & 31); + if ($pad) { + $dest .= '======'; + } + } + } + return $dest; + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base32Hex.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base32Hex.php new file mode 100644 index 000000000..4323a573a --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base32Hex.php @@ -0,0 +1,118 @@ + 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); + + // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86 + $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 5-bit integers + * into 8-bit integers. + * + * @param int $src + * @return int + */ + #[Override] + protected static function decode5BitsUpper(int $src): int + { + $ret = -1; + + // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); + + // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54 + $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * @param int $src + * @return string + */ + #[Override] + protected static function encode5Bits(int $src): string + { + $src += 0x30; + + // if ($src > 0x39) $src += 0x61 - 0x3a; // 39 + $src += ((0x39 - $src) >> 8) & 39; + + return pack('C', $src); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return string + */ + #[Override] + protected static function encode5BitsUpper(int $src): string + { + $src += 0x30; + + // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 + $src += ((0x39 - $src) >> 8) & 7; + + return pack('C', $src); + } +} \ No newline at end of file diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64.php new file mode 100644 index 000000000..9679748dc --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64.php @@ -0,0 +1,381 @@ + SODIUM_BASE64_VARIANT_ORIGINAL, + Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE, + default => 0, + }; + if ($variant > 0) { + try { + return sodium_bin2base64($binString, $variant); + } catch (SodiumException $ex) { + throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); + } + } + } + return static::doEncode($binString, true); + } + + /** + * Encode into Base64, no = padding + * + * Base64 character set "[A-Z][a-z][0-9]+/" + * + * @param string $src + * @return string + * + * @throws TypeError + * @api + */ + public static function encodeUnpadded( + #[SensitiveParameter] + string $src + ): string { + if (extension_loaded('sodium')) { + $variant = match(static::class) { + Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, + Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, + default => 0, + }; + if ($variant > 0) { + try { + return sodium_bin2base64($src, $variant); + } catch (SodiumException $ex) { + throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); + } + } + } + return static::doEncode($src, false); + } + + /** + * @param string $src + * @param bool $pad Include = padding? + * @return string + * + * @throws TypeError + */ + protected static function doEncode( + #[SensitiveParameter] + string $src, + bool $pad = true + ): string { + $dest = ''; + $srcLen = strlen($src); + // Main loop (no padding): + for ($i = 0; $i + 3 <= $srcLen; $i += 3) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($src, $i, 3)); + $b0 = $chunk[1]; + $b1 = $chunk[2]; + $b2 = $chunk[3]; + + $dest .= + static::encode6Bits( $b0 >> 2 ) . + static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . + static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . + static::encode6Bits( $b2 & 63); + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($src, $i, $srcLen - $i)); + $b0 = $chunk[1]; + if ($i + 1 < $srcLen) { + $b1 = $chunk[2]; + $dest .= + static::encode6Bits($b0 >> 2) . + static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . + static::encode6Bits(($b1 << 2) & 63); + if ($pad) { + $dest .= '='; + } + } else { + $dest .= + static::encode6Bits( $b0 >> 2) . + static::encode6Bits(($b0 << 4) & 63); + if ($pad) { + $dest .= '=='; + } + } + } + return $dest; + } + + /** + * decode from base64 into binary + * + * Base64 character set "./[A-Z][a-z][0-9]" + * + * @param string $encodedString + * @param bool $strictPadding + * @return string + * + * @throws RangeException + * @throws TypeError + */ + #[Override] + public static function decode( + #[SensitiveParameter] + string $encodedString, + bool $strictPadding = false + ): string { + // Remove padding + $srcLen = strlen($encodedString); + if ($srcLen === 0) { + return ''; + } + + if ($strictPadding) { + if (($srcLen & 3) === 0) { + if ($encodedString[$srcLen - 1] === '=') { + $srcLen--; + if ($encodedString[$srcLen - 1] === '=') { + $srcLen--; + } + } + } + if (($srcLen & 3) === 1) { + throw new RangeException( + 'Incorrect padding' + ); + } + if ($encodedString[$srcLen - 1] === '=') { + throw new RangeException( + 'Incorrect padding' + ); + } + if (extension_loaded('sodium')) { + $variant = match(static::class) { + Base64::class => SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, + Base64UrlSafe::class => SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, + default => 0, + }; + if ($variant > 0) { + try { + return sodium_base642bin(substr($encodedString, 0, $srcLen), $variant); + } catch (SodiumException $ex) { + throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); + } + } + } + } else { + // Just remove all padding. + $encodedString = rtrim($encodedString, '='); + $srcLen = strlen($encodedString); + } + + $err = 0; + $dest = ''; + // Main loop (no padding): + for ($i = 0; $i + 4 <= $srcLen; $i += 4) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($encodedString, $i, 4)); + $c0 = static::decode6Bits($chunk[1]); + $c1 = static::decode6Bits($chunk[2]); + $c2 = static::decode6Bits($chunk[3]); + $c3 = static::decode6Bits($chunk[4]); + + $dest .= pack( + 'CCC', + ((($c0 << 2) | ($c1 >> 4)) & 0xff), + ((($c1 << 4) | ($c2 >> 2)) & 0xff), + ((($c2 << 6) | $c3 ) & 0xff) + ); + $err |= ($c0 | $c1 | $c2 | $c3) >> 8; + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = unpack('C*', substr($encodedString, $i, $srcLen - $i)); + $c0 = static::decode6Bits($chunk[1]); + + if ($i + 2 < $srcLen) { + $c1 = static::decode6Bits($chunk[2]); + $c2 = static::decode6Bits($chunk[3]); + $dest .= pack( + 'CC', + ((($c0 << 2) | ($c1 >> 4)) & 0xff), + ((($c1 << 4) | ($c2 >> 2)) & 0xff) + ); + $err |= ($c0 | $c1 | $c2) >> 8; + if ($strictPadding) { + $err |= ($c2 << 6) & 0xff; + } + } elseif ($i + 1 < $srcLen) { + $c1 = static::decode6Bits($chunk[2]); + $dest .= pack( + 'C', + ((($c0 << 2) | ($c1 >> 4)) & 0xff) + ); + $err |= ($c0 | $c1) >> 8; + if ($strictPadding) { + $err |= ($c1 << 4) & 0xff; + } + } elseif ($strictPadding) { + $err |= 1; + } + } + $check = ($err === 0); + if (!$check) { + throw new RangeException( + 'Base64::decode() only expects characters in the correct base64 alphabet' + ); + } + return $dest; + } + + /** + * @param string $encodedString + * @return string + * @api + */ + public static function decodeNoPadding( + #[SensitiveParameter] + string $encodedString + ): string { + $srcLen = strlen($encodedString); + if ($srcLen === 0) { + return ''; + } + if (($srcLen & 3) === 0) { + // If $strLen is not zero, and it is divisible by 4, then it's at least 4. + if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') { + throw new InvalidArgumentException( + "decodeNoPadding() doesn't tolerate padding" + ); + } + } + return static::decode( + $encodedString, + true + ); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 6-bit integers + * into 8-bit integers. + * + * Base64 character set: + * [A-Z] [a-z] [0-9] + / + * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f + * + * @param int $src + * @return int + */ + protected static function decode6Bits(int $src): int + { + $ret = -1; + + // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); + + // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); + + // if ($src == 0x2b) $ret += 62 + 1; + $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; + + // if ($src == 0x2f) ret += 63 + 1; + $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 + $diff += ((25 - $src) >> 8) & 6; + + // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 + $diff -= ((51 - $src) >> 8) & 75; + + // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 + $diff -= ((61 - $src) >> 8) & 15; + + // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 + $diff += ((62 - $src) >> 8) & 3; + + return pack('C', $src + $diff); + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php new file mode 100644 index 000000000..847751767 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php @@ -0,0 +1,92 @@ + 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45 + $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45); + + // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62); + + // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68); + + // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + #[Override] + protected static function encode6Bits(int $src): string + { + $src += 0x2e; + + // if ($src > 0x2f) $src += 0x41 - 0x30; // 17 + $src += ((0x2f - $src) >> 8) & 17; + + // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 + $src += ((0x5a - $src) >> 8) & 6; + + // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75 + $src -= ((0x7a - $src) >> 8) & 75; + + return \pack('C', $src); + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php new file mode 100644 index 000000000..2c42db37f --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php @@ -0,0 +1,86 @@ + 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45 + $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45); + + // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52); + + // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + #[Override] + protected static function encode6Bits(int $src): string + { + $src += 0x2e; + + // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 + $src += ((0x39 - $src) >> 8) & 7; + + // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 + $src += ((0x5a - $src) >> 8) & 6; + + return \pack('C', $src); + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php new file mode 100644 index 000000000..845aaf626 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php @@ -0,0 +1,99 @@ + 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); + + // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); + + // if ($src == 0x2c) $ret += 62 + 1; + $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63; + + // if ($src == 0x5f) ret += 63 + 1; + $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64; + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + #[Override] + protected static function encode6Bits(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 + $diff += ((25 - $src) >> 8) & 6; + + // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 + $diff -= ((51 - $src) >> 8) & 75; + + // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13 + $diff -= ((61 - $src) >> 8) & 13; + + // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3 + $diff += ((62 - $src) >> 8) & 49; + + return \pack('C', $src + $diff); + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Binary.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Binary.php new file mode 100644 index 000000000..369584407 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/Binary.php @@ -0,0 +1,87 @@ +getMessage(), $ex->getCode(), $ex); + } + } + $hex = ''; + $len = strlen($binString); + for ($i = 0; $i < $len; ++$i) { + /** @var array $chunk */ + $chunk = unpack('C', $binString[$i]); + $c = $chunk[1] & 0xf; + $b = $chunk[1] >> 4; + + $hex .= pack( + 'CC', + (87 + $b + ((($b - 10) >> 8) & ~38)), + (87 + $c + ((($c - 10) >> 8) & ~38)) + ); + } + return $hex; + } + + /** + * Convert a binary string into a hexadecimal string without cache-timing + * leaks, returning uppercase letters (as per RFC 4648) + * + * @param string $binString (raw binary) + * @return string + * @throws TypeError + */ + public static function encodeUpper( + #[SensitiveParameter] + string $binString + ): string { + $hex = ''; + $len = strlen($binString); + + for ($i = 0; $i < $len; ++$i) { + /** @var array $chunk */ + $chunk = unpack('C', $binString[$i]); + $c = $chunk[1] & 0xf; + $b = $chunk[1] >> 4; + + $hex .= pack( + 'CC', + (55 + $b + ((($b - 10) >> 8) & ~6)), + (55 + $c + ((($c - 10) >> 8) & ~6)) + ); + } + return $hex; + } + + /** + * Convert a hexadecimal string into a binary string without cache-timing + * leaks + * + * @param string $encodedString + * @param bool $strictPadding + * @return string (raw binary) + * @throws RangeException + */ + #[Override] + public static function decode( + #[SensitiveParameter] + string $encodedString, + bool $strictPadding = false + ): string { + if (extension_loaded('sodium') && $strictPadding) { + try { + return sodium_hex2bin($encodedString); + } catch (SodiumException $ex) { + throw new RangeException($ex->getMessage(), $ex->getCode(), $ex); + } + } + $hex_pos = 0; + $bin = ''; + $c_acc = 0; + $hex_len = strlen($encodedString); + $state = 0; + if (($hex_len & 1) !== 0) { + if ($strictPadding) { + throw new RangeException( + 'Expected an even number of hexadecimal characters' + ); + } else { + $encodedString = '0' . $encodedString; + ++$hex_len; + } + } + + /** @var array $chunk */ + $chunk = unpack('C*', $encodedString); + while ($hex_pos < $hex_len) { + ++$hex_pos; + $c = $chunk[$hex_pos]; + $c_num = $c ^ 48; + $c_num0 = ($c_num - 10) >> 8; + $c_alpha = ($c & ~32) - 55; + $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; + + if (($c_num0 | $c_alpha0) === 0) { + throw new RangeException( + 'Expected hexadecimal character' + ); + } + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= pack('C', $c_acc | $c_val); + } + $state ^= 1; + } + return $bin; + } +} diff --git a/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/RFC4648.php b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/RFC4648.php new file mode 100644 index 000000000..fb66f73d7 --- /dev/null +++ b/plugins/google2fa/php/vendor/paragonie/constant_time_encoding/src/RFC4648.php @@ -0,0 +1,208 @@ + "Zm9v" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64Encode( + #[SensitiveParameter] + string $str + ): string { + return Base64::encode($str); + } + + /** + * RFC 4648 Base64 decoding + * + * "Zm9v" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64Decode( + #[SensitiveParameter] + string $str + ): string { + return Base64::decode($str, true); + } + + /** + * RFC 4648 Base64 (URL Safe) encoding + * + * "foo" -> "Zm9v" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64UrlSafeEncode( + #[SensitiveParameter] + string $str + ): string { + return Base64UrlSafe::encode($str); + } + + /** + * RFC 4648 Base64 (URL Safe) decoding + * + * "Zm9v" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64UrlSafeDecode( + #[SensitiveParameter] + string $str + ): string { + return Base64UrlSafe::decode($str, true); + } + + /** + * RFC 4648 Base32 encoding + * + * "foo" -> "MZXW6===" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32Encode( + #[SensitiveParameter] + string $str + ): string { + return Base32::encodeUpper($str); + } + + /** + * RFC 4648 Base32 encoding + * + * "MZXW6===" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32Decode( + #[SensitiveParameter] + string $str + ): string { + return Base32::decodeUpper($str, true); + } + + /** + * RFC 4648 Base32-Hex encoding + * + * "foo" -> "CPNMU===" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32HexEncode( + #[SensitiveParameter] + string $str + ): string { + return Base32::encodeUpper($str); + } + + /** + * RFC 4648 Base32-Hex decoding + * + * "CPNMU===" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32HexDecode( + #[SensitiveParameter] + string $str + ): string { + return Base32::decodeUpper($str, true); + } + + /** + * RFC 4648 Base16 decoding + * + * "foo" -> "666F6F" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base16Encode( + #[SensitiveParameter] + string $str + ): string { + return Hex::encodeUpper($str); + } + + /** + * RFC 4648 Base16 decoding + * + * "666F6F" -> "foo" + * + * @param string $str + * @return string + */ + public static function base16Decode( + #[SensitiveParameter] + string $str + ): string { + return Hex::decode($str, true); + } +} diff --git a/plugins/google2fa/php/vendor/psr/clock/CHANGELOG.md b/plugins/google2fa/php/vendor/psr/clock/CHANGELOG.md new file mode 100644 index 000000000..3cd6b9b75 --- /dev/null +++ b/plugins/google2fa/php/vendor/psr/clock/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.0.0 + +First stable release after PSR-20 acceptance + +## 0.1.0 + +First release diff --git a/plugins/google2fa/php/vendor/psr/clock/LICENSE b/plugins/google2fa/php/vendor/psr/clock/LICENSE new file mode 100644 index 000000000..be6834212 --- /dev/null +++ b/plugins/google2fa/php/vendor/psr/clock/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/google2fa/php/vendor/psr/clock/README.md b/plugins/google2fa/php/vendor/psr/clock/README.md new file mode 100644 index 000000000..6ca877eeb --- /dev/null +++ b/plugins/google2fa/php/vendor/psr/clock/README.md @@ -0,0 +1,61 @@ +# PSR Clock + +This repository holds the interface for [PSR-20][psr-url]. + +Note that this is not a clock of its own. It is merely an interface that +describes a clock. See the specification for more details. + +## Installation + +```bash +composer require psr/clock +``` + +## Usage + +If you need a clock, you can use the interface like this: + +```php +clock = $clock; + } + + public function doSomething() + { + /** @var DateTimeImmutable $currentDateAndTime */ + $currentDateAndTime = $this->clock->now(); + // do something useful with that information + } +} +``` + +You can then pick one of the [implementations][implementation-url] of the interface to get a clock. + +If you want to implement the interface, you can require this package and +implement `Psr\Clock\ClockInterface` in your code. + +Don't forget to add `psr/clock-implementation` to your `composer.json`s `provides`-section like this: + +```json +{ + "provides": { + "psr/clock-implementation": "1.0" + } +} +``` + +And please read the [specification text][specification-url] for details on the interface. + +[psr-url]: https://www.php-fig.org/psr/psr-20 +[package-url]: https://packagist.org/packages/psr/clock +[implementation-url]: https://packagist.org/providers/psr/clock-implementation +[specification-url]: https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md diff --git a/plugins/google2fa/php/vendor/psr/clock/composer.json b/plugins/google2fa/php/vendor/psr/clock/composer.json new file mode 100644 index 000000000..77992eda7 --- /dev/null +++ b/plugins/google2fa/php/vendor/psr/clock/composer.json @@ -0,0 +1,21 @@ +{ + "name": "psr/clock", + "description": "Common interface for reading the clock.", + "keywords": ["psr", "psr-20", "time", "clock", "now"], + "homepage": "https://github.com/php-fig/clock", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": "^7.0 || ^8.0" + }, + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + } +} diff --git a/plugins/google2fa/php/vendor/psr/clock/src/ClockInterface.php b/plugins/google2fa/php/vendor/psr/clock/src/ClockInterface.php new file mode 100644 index 000000000..7b6d8d8aa --- /dev/null +++ b/plugins/google2fa/php/vendor/psr/clock/src/ClockInterface.php @@ -0,0 +1,13 @@ +..`. +The version number may have additional information, such as "-rc1,-rc2,-rc3" to mark release candidate builds for earlier access. +Such releases will be considered as "pre-releases". + +## Minor Release Support Matrix + +| Version | Supported | +|----------|--------------------| +| 11.4.x | :white_check_mark: | +| < 11.4.x | :x: | diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/SECURITY.md b/plugins/google2fa/php/vendor/spomky-labs/otphp/SECURITY.md new file mode 100644 index 000000000..be7bcd845 --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/SECURITY.md @@ -0,0 +1,74 @@ +# Security Release Process + +OTPHP is devoted to providing the best experience for all developers who want to implement OTP authentication in their applications. +Spomky-Labs has adopted this security disclosure and response policy to ensure we responsibly handle critical issues. + +## Supported Versions + +The OTPHP project maintains release branches for the three most recent minor releases. +Applicable fixes, including security fixes, may be backported to those three release branches, depending on severity and feasibility. Please refer to [RELEASES.md](RELEASES.md) for details. + +## Reporting a Vulnerability - Private Disclosure Process + +Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to OTPHP privately, to minimize attacks against current users of OTPHP before they are fixed. +Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. +This information could be kept entirely internal to the project. + +If you know of a publicly disclosed security vulnerability for OTPHP, please **IMMEDIATELY** contact security@spomky-labs.com to inform the OTPHP Security Team. + +**IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** + +To report a vulnerability or a security-related issue, please email the private address security@spomky-labs.com with the details of the vulnerability. +The email will be fielded by the OTPHP Security Team, which is made up of the OTPHP maintainers who have committer and release permissions. +Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/Spomky-Labs/otphp/issues/new/choose) instead. + +Emails can be encrypted if you wish to share the vulnerability details securely. +The OTPHP Security Team's PGP key is available on the [PGP keyservers](https://keys.openpgp.org/search?q=security%40spomky-labs.com). + +### Proposed Email Content + +Provide a descriptive subject line and in the body of the email include the following information: + +- Basic identity information, such as your name and your affiliation or company. +- Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and compressed packet captures are all helpful to us). +- Description of the effects of the vulnerability on OTPHP and the related hardware and software configurations, so that the OTPHP Security Team can reproduce it. +- How the vulnerability affects OTPHP usage and an estimation of the attack surface, if there is one. +- List other projects or dependencies that were used in conjunction with OTPHP to produce the vulnerability. + +## When to report a vulnerability + +- When you think OTPHP has a potential security vulnerability. +- When you suspect a potential vulnerability, but you are unsure that it impacts OTPHP. +- When you know of or suspect a potential vulnerability on another project that is used by OTPHP. + +## Patch, Release, and Disclosure + +The OTPHP Security Team will respond to vulnerability reports as follows: + +1. The Security Team will investigate the vulnerability and determine its effects and criticality. +2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection. +3. The Security Team will initiate a conversation with the reporter as soon as possible. +4. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out. +5. The Security Team will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix. +6. A public disclosure date is negotiated by the OTPHP Security Team and the bug submitter. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it's already publicly known) to a few weeks. For a critical vulnerability with a straightforward mitigation, we expect report date to public disclosure date to be on the order of 14 business days. The OTPHP Security Team holds the final say when setting a public disclosure date. +7. Once the fix is confirmed, the Security Team will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. Upon release of the patched version of OTPHP, we will follow the **Public Disclosure Process**. + +### Public Disclosure Process + +The Security Team publishes a public [advisory](https://github.com/Spomky-Labs/otphp/security/advisories) to the OTPHP community via GitHub. In most cases, additional communication via Twitter, blog and other channels will assist in educating OTPHP users and rolling out the patched release to affected users. + +The Security Team will also publish any mitigating steps users can take until the fix can be applied to their OTPHP instances. OTPHP distributors will handle creating and publishing their own security advisories. + +## Mailing lists + +- Use security@spomky-labs.com to report security concerns to the OTPHP Security Team, who uses the list to privately discuss security issues and fixes prior to disclosure. + +## Early Disclosure to OTPHP Distributors List + +This private list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues. + +## Confidentiality, integrity and availability + +We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. +Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. +The OTPHP Security Team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner. diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/composer.json b/plugins/google2fa/php/vendor/spomky-labs/otphp/composer.json new file mode 100644 index 000000000..848a574b4 --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/composer.json @@ -0,0 +1,47 @@ +{ + "name": "spomky-labs/otphp", + "type": "library", + "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", + "license": "MIT", + "keywords": ["otp", "hotp", "totp", "RFC 4226", "RFC 6238", "Google Authenticator", "FreeOTP"], + "homepage": "https://github.com/Spomky-Labs/otphp", + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "require": { + "php": ">=8.1", + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0|^8.0" + }, + "autoload": { + "psr-4": { "OTPHP\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "OTPHP\\Test\\": "tests/" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "infection/extension-installer": true, + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + } +} diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/src/Exception/InvalidLabelException.php b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/Exception/InvalidLabelException.php new file mode 100644 index 000000000..25e188b17 --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/Exception/InvalidLabelException.php @@ -0,0 +1,25 @@ +getScheme() === 'otpauth' || throw new InvalidProvisioningUriException('Invalid scheme.'); + } catch (Throwable $throwable) { + throw new InvalidProvisioningUriException( + 'Not a valid OTP provisioning URI', + $throwable->getCode(), + $throwable + ); + } + if ($clock === null) { + trigger_deprecation( + 'spomky-labs/otphp', + '11.3.0', + 'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".' + ); + $clock = new InternalClock(); + } + + try { + $otp = self::createOTP($parsed_url, $clock); + + self::populateOTP($otp, $parsed_url); + } catch (InvalidProvisioningUriException $exception) { + throw $exception; + } catch (Throwable $throwable) { + throw new InvalidProvisioningUriException( + 'Not a valid OTP provisioning URI', + $throwable->getCode(), + $throwable + ); + } + + return $otp; + } + + private static function populateParameters(OTPInterface $otp, Url $data): void + { + foreach ($data->getQuery() as $key => $value) { + $otp->setParameter($key, $value); + } + } + + private static function populateOTP(OTPInterface $otp, Url $data): void + { + self::populateParameters($otp, $data); + $result = explode(':', rawurldecode(substr($data->getPath(), 1))); + + if (count($result) < 2) { + $otp->setIssuerIncludedAsParameter(false); + + return; + } + + $issuerFromLabel = $result[0]; + $issuerFromParameter = $otp->getIssuer(); + + if ($issuerFromParameter !== null) { + // Issuer parameter takes precedence over issuer in label + // According to Google Authenticator spec: "they should be equal" but not required to be + $otp->setIssuerIncludedAsParameter(true); + } else { + // No issuer parameter, use the issuer from label + $issuerFromLabel !== '' || throw new InvalidProvisioningUriException( + 'Issuer from label must not be empty.' + ); + $otp->setIssuer($issuerFromLabel); + } + } + + private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface + { + switch ($parsed_url->getHost()) { + case 'totp': + $totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock); + $totp->setLabel(self::getLabel($parsed_url->getPath())); + + return $totp; + case 'hotp': + $hotp = HOTP::createFromSecret($parsed_url->getSecret()); + $hotp->setLabel(self::getLabel($parsed_url->getPath())); + + return $hotp; + default: + throw new InvalidProvisioningUriException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost())); + } + } + + /** + * @param non-empty-string $data + * @return non-empty-string + */ + private static function getLabel(string $data): string + { + $result = explode(':', rawurldecode(substr($data, 1))); + $label = count($result) === 2 ? $result[1] : $result[0]; + $label !== '' || throw new InvalidProvisioningUriException('Label must not be empty.'); + + return $label; + } +} diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/src/FactoryInterface.php b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/FactoryInterface.php new file mode 100644 index 000000000..e34056ace --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/FactoryInterface.php @@ -0,0 +1,18 @@ +setCounter($counter); + $htop->setDigest($digest); + $htop->setDigits($digits); + + return $htop; + } + + public static function createFromSecret(string $secret): self + { + $htop = new self($secret); + $htop->setCounter(self::DEFAULT_COUNTER); + $htop->setDigest(self::DEFAULT_DIGEST); + $htop->setDigits(self::DEFAULT_DIGITS); + + return $htop; + } + + /** + * @param positive-int|null $secretSize + */ + public static function generate(?int $secretSize = null): self + { + return self::createFromSecret(self::generateSecret($secretSize)); + } + + /** + * @return 0|positive-int + */ + public function getCounter(): int + { + $value = $this->getParameter('counter'); + (is_int($value) && $value >= 0) || throw new InvalidParameterException( + 'Invalid "counter" parameter.', + 'counter', + $value + ); + + return $value; + } + + public function getProvisioningUri(): string + { + return $this->generateURI('hotp', [ + 'counter' => $this->getCounter(), + ]); + } + + /** + * If the counter is not provided, the OTP is verified at the actual counter. + * + * @param null|0|positive-int $counter + */ + public function verify(string $otp, null|int $counter = null, null|int $window = null): bool + { + $counter >= 0 || throw new InvalidParameterException( + 'The counter must be at least 0.', + 'counter', + $counter + ); + + if ($counter === null) { + $counter = $this->getCounter(); + } elseif ($counter < $this->getCounter()) { + return false; + } + + return $this->verifyOtpWithWindow($otp, $counter, $window); + } + + public function setCounter(int $counter): void + { + $this->setParameter('counter', $counter); + } + + public function withCounter(int $counter): self + { + $otp = clone $this; + $otp->setParameter('counter', $counter); + + return $otp; + } + + /** + * @return array + */ + protected function getParameterMap(): array + { + return [...parent::getParameterMap(), ...[ + 'counter' => static function (mixed $value): int { + $value = (int) $value; + $value >= 0 || throw new InvalidParameterException( + 'Counter must be at least 0.', + 'counter', + $value + ); + + return $value; + }, + ]]; + } + + private function updateCounter(int $counter): void + { + $this->setCounter($counter); + } + + /** + * @param null|0|positive-int $window + */ + private function getWindow(null|int $window): int + { + return abs($window ?? self::DEFAULT_WINDOW); + } + + /** + * @param non-empty-string $otp + * @param 0|positive-int $counter + * @param null|0|positive-int $window + */ + private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool + { + $window = $this->getWindow($window); + + for ($i = $counter; $i <= $counter + $window; ++$i) { + if ($this->compareOTP($this->at($i), $otp)) { + $this->updateCounter($i + 1); + + return true; + } + } + + return false; + } +} diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/src/HOTPInterface.php b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/HOTPInterface.php new file mode 100644 index 000000000..fe6382d83 --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/HOTPInterface.php @@ -0,0 +1,41 @@ + + */ + private array $parameters = []; + + /** + * @var non-empty-string|null + */ + private null|string $issuer = null; + + /** + * @var non-empty-string|null + */ + private null|string $label = null; + + private bool $issuer_included_as_parameter = true; + + /** + * @param non-empty-string $secret + */ + protected function __construct(string $secret) + { + $this->setSecret($secret); + } + + public function getQrCodeUri(string $uri, string $placeholder): string + { + $provisioning_uri = urlencode($this->getProvisioningUri()); + + return str_replace($placeholder, $provisioning_uri, $uri); + } + + /** + * @param 0|positive-int $input + */ + public function at(int $input): string + { + return $this->generateOTP($input); + } + + /** + * @return array + */ + public function getParameters(): array + { + $parameters = $this->parameters; + + if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) { + $parameters['issuer'] = $this->getIssuer(); + } + + return $parameters; + } + + public function getSecret(): string + { + $value = $this->getParameter('secret'); + (is_string($value) && $value !== '') || throw new InvalidParameterException( + 'Invalid "secret" parameter.', + 'secret', + $value + ); + + return $value; + } + + public function getLabel(): null|string + { + return $this->label; + } + + public function setLabel(string $label): void + { + $this->setParameter('label', $label); + } + + public function withLabel(string $label): static + { + $otp = clone $this; + $otp->setParameter('label', $label); + + return $otp; + } + + public function getIssuer(): null|string + { + return $this->issuer; + } + + public function setIssuer(string $issuer): void + { + $this->setParameter('issuer', $issuer); + } + + public function withIssuer(string $issuer): static + { + $otp = clone $this; + $otp->setParameter('issuer', $issuer); + + return $otp; + } + + public function isIssuerIncludedAsParameter(): bool + { + return $this->issuer_included_as_parameter; + } + + public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void + { + $this->issuer_included_as_parameter = $issuer_included_as_parameter; + } + + public function withIssuerIncludedAsParameter(bool $issuer_included_as_parameter): static + { + $otp = clone $this; + $otp->issuer_included_as_parameter = $issuer_included_as_parameter; + + return $otp; + } + + public function getDigits(): int + { + $value = $this->getParameter('digits'); + (is_int($value) && $value >= 1 && $value <= self::MAX_DIGITS) || throw new InvalidParameterException( + 'Invalid "digits" parameter.', + 'digits', + $value + ); + + return $value; + } + + public function getDigest(): string + { + $value = $this->getParameter('algorithm'); + (is_string($value) && $value !== '') || throw new InvalidParameterException( + 'Invalid "algorithm" parameter.', + 'algorithm', + $value + ); + + return $value; + } + + public function hasParameter(string $parameter): bool + { + return array_key_exists($parameter, $this->parameters); + } + + public function getParameter(string $parameter): mixed + { + if ($this->hasParameter($parameter)) { + return $this->getParameters()[$parameter]; + } + + throw new ParameterNotFoundException(sprintf('Parameter "%s" does not exist', $parameter), $parameter); + } + + public function setParameter(string $parameter, mixed $value): void + { + $map = $this->getParameterMap(); + + if (array_key_exists($parameter, $map) === true) { + $callback = $map[$parameter]; + $value = $callback($value); + } + + if (in_array($parameter, ['label', 'issuer'], true)) { + $this->{$parameter} = $value; + } else { + $this->parameters[$parameter] = $value; + } + } + + public function withParameter(string $parameter, mixed $value): static + { + $otp = clone $this; + $otp->setParameter($parameter, $value); + + return $otp; + } + + public function setSecret(string $secret): void + { + $this->setParameter('secret', $secret); + } + + public function withSecret(string $secret): static + { + $otp = clone $this; + $otp->setParameter('secret', $secret); + + return $otp; + } + + public function setDigits(int $digits): void + { + $this->setParameter('digits', $digits); + } + + public function withDigits(int $digits): static + { + $otp = clone $this; + $otp->setParameter('digits', $digits); + + return $otp; + } + + public function setDigest(string $digest): void + { + $this->setParameter('algorithm', $digest); + } + + public function withDigest(string $digest): static + { + $otp = clone $this; + $otp->setParameter('algorithm', $digest); + + return $otp; + } + + /** + * @param positive-int|null $secretSize + * + * @return non-empty-string + */ + final protected static function generateSecret(?int $secretSize = null): string + { + $secretSize ??= self::DEFAULT_SECRET_SIZE; + $secretSize > 0 || throw new InvalidParameterException( + 'Secret size must be at least 1.', + 'secretSize', + $secretSize + ); + + return Base32::encodeUpper(random_bytes($secretSize)); + } + + /** + * The OTP at the specified input. + * + * @param 0|positive-int $input + * + * @return non-empty-string + */ + protected function generateOTP(int $input): string + { + $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true); + $unpacked = unpack('C*', $hash); + $unpacked !== false || throw new InvalidParameterException('Invalid data.', 'hash', $hash); + /** @var list $hmac */ + $hmac = array_values($unpacked); + + $offset = ($hmac[count($hmac) - 1] & 0xF); + $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF); + $otp = $code % (10 ** $this->getDigits()); + + return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT); + } + + /** + * @param array $options + */ + protected function filterOptions(array &$options): void + { + foreach ([ + 'algorithm' => 'sha1', + 'period' => 30, + 'digits' => 6, + ] as $key => $default) { + if (isset($options[$key]) && $default === $options[$key]) { + unset($options[$key]); + } + } + + ksort($options); + } + + /** + * @param non-empty-string $type + * @param array $options + * + * @return non-empty-string + */ + protected function generateURI(string $type, array $options): string + { + $options = [...$options, ...$this->getParameters()]; + $this->filterOptions($options); + $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&')); + + return sprintf('otpauth://%s/%s?%s', $type, rawurlencode($this->buildProvisioningUriLabel()), $params); + } + + /** + * @param non-empty-string $safe + * @param non-empty-string $user + */ + protected function compareOTP(string $safe, string $user): bool + { + return hash_equals($safe, $user); + } + + /** + * @return array + */ + protected function getParameterMap(): array + { + return [ + 'label' => function (string $value): string { + $value !== '' || throw new InvalidLabelException('Label must not be empty.', 'label', $value); + $this->validateLabel($value); + + return $value; + }, + 'secret' => static fn (string $value): string => strtoupper(trim($value, '=')), + 'algorithm' => static function (string $value): string { + $value = strtolower($value); + in_array($value, hash_hmac_algos(), true) || throw new InvalidParameterException( + sprintf('The "%s" digest is not supported.', $value), + 'algorithm', + $value + ); + $size = strlen(hash($value, '', true)); + $size >= self::MINIMUM_DIGEST_SIZE || throw new InvalidParameterException( + sprintf( + 'The "%s" digest produces a %d-byte hash which is too short for the RFC 4226 dynamic truncation; at least %d bytes are required.', + $value, + $size, + self::MINIMUM_DIGEST_SIZE + ), + 'algorithm', + $value + ); + + return $value; + }, + 'digits' => static function ($value): int { + $value = (int) $value; + ($value >= 1 && $value <= self::MAX_DIGITS) || throw new InvalidParameterException( + sprintf('Digits must be between 1 and %d.', self::MAX_DIGITS), + 'digits', + $value + ); + + return $value; + }, + 'issuer' => function (string $value): string { + $value !== '' || throw new InvalidLabelException('Issuer must not be empty.', 'issuer', $value); + $this->hasColon($value) === false || throw new InvalidLabelException( + 'Issuer must not contain a colon.', + 'issuer', + $value + ); + + return $value; + }, + ]; + } + + /** + * @return non-empty-string + */ + private function getDecodedSecret(): string + { + try { + $decoded = Base32::decodeUpper($this->getSecret()); + } catch (Exception) { + throw new SecretDecodingException('Unable to decode the secret. Is it correctly base32 encoded?'); + } + $decoded !== '' || throw new SecretDecodingException('The decoded secret must not be empty.'); + + return $decoded; + } + + private function intToByteString(int $int): string + { + $result = []; + while ($int !== 0) { + $result[] = chr($int & 0xFF); + $int >>= 8; + } + + return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT); + } + + /** + * @return non-empty-string + */ + private function buildProvisioningUriLabel(): string + { + $issuer = $this->getIssuer(); + $label = $this->getLabel(); + + return match (true) { + $issuer === null && $label === null => throw new InvalidLabelException( + 'The label is not set. Either label or issuer must be set.', + 'label' + ), + $label !== null && $this->hasColon($label) => throw new InvalidLabelException( + 'Label must not contain a colon.', + 'label', + $label + ), + $issuer !== null && $label !== null => $issuer . ':' . $label, + $issuer !== null => $issuer, + default => $label, + }; + } + + /** + * Validates a label according to Google Authenticator spec: + * label = accountname / issuer (":" / "%3A") *"%20" accountname + * Neither issuer nor account name may themselves contain a colon. + * + * Valid examples: + * - alice@gmail.com + * - Provider1:Alice%20Smith + * - Big%20Corporation%3A%20alice%40bigco.com + * + * @param non-empty-string $value + */ + private function validateLabel(string $value): void + { + // Check for colon separators (literal or URL-encoded) + $hasLiteralColon = str_contains($value, ':'); + $hasEncodedColon = str_contains($value, '%3A') || str_contains($value, '%3a'); + + if (! $hasLiteralColon && ! $hasEncodedColon) { + // Simple label (account name only) - no colons allowed anywhere + return; + } + + // Label contains a separator - validate issuer:account format + // Split by literal or encoded colon + $parts = match (true) { + $hasLiteralColon => explode(':', $value, 2), + default => preg_split('/%3[Aa]/', $value, 2), + }; + + if ($parts === false || count($parts) !== 2) { + throw new InvalidLabelException('Label must not contain a colon.', 'label', $value); + } + + [$issuerPart, $accountPart] = $parts; + + // Remove leading %20 (spaces) from account part per spec: *"%20" accountname + $accountPart = ltrim($accountPart, '%20'); + + // Validate that neither part contains additional colons + if ($this->hasColon($issuerPart) || $this->hasColon($accountPart)) { + throw new InvalidLabelException( + 'Neither issuer nor account name in label may contain a colon.', + 'label', + $value + ); + } + } + + /** + * @param non-empty-string $value + */ + private function hasColon(string $value): bool + { + $colons = [':', '%3A', '%3a']; + foreach ($colons as $colon) { + if (str_contains($value, $colon)) { + return true; + } + } + + return false; + } +} diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/src/OTPInterface.php b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/OTPInterface.php new file mode 100644 index 000000000..2ec5e4e43 --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/OTPInterface.php @@ -0,0 +1,185 @@ + + */ + public function getParameters(): array; + + /** + * @param non-empty-string $parameter + * + * @deprecated Deprecated since v11.4, use {@see self::withParameter()} instead + */ + public function setParameter(string $parameter, mixed $value): void; + + /** + * @param non-empty-string $parameter + */ + public function withParameter(string $parameter, mixed $value): self; + + /** + * Get the provisioning URI. + * + * @return non-empty-string + */ + public function getProvisioningUri(): string; + + /** + * Get the provisioning URI. + * + * @param non-empty-string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method. + * @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI + */ + public function getQrCodeUri(string $uri, string $placeholder): string; +} diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/src/TOTP.php b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/TOTP.php new file mode 100644 index 000000000..3cb8b10e8 --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/TOTP.php @@ -0,0 +1,262 @@ +clock = $clock; + } + + public static function create( + null|string $secret = null, + int $period = self::DEFAULT_PERIOD, + string $digest = self::DEFAULT_DIGEST, + int $digits = self::DEFAULT_DIGITS, + int $epoch = self::DEFAULT_EPOCH, + ?ClockInterface $clock = null, + ?int $secretSize = null + ): self { + $totp = $secret !== null + ? self::createFromSecret($secret, $clock) + : self::generate($clock, $secretSize) + ; + $totp->setPeriod($period); + $totp->setDigest($digest); + $totp->setDigits($digits); + $totp->setEpoch($epoch); + + return $totp; + } + + public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self + { + $totp = new self($secret, $clock); + $totp->setPeriod(self::DEFAULT_PERIOD); + $totp->setDigest(self::DEFAULT_DIGEST); + $totp->setDigits(self::DEFAULT_DIGITS); + $totp->setEpoch(self::DEFAULT_EPOCH); + + return $totp; + } + + /** + * @param positive-int|null $secretSize + */ + public static function generate(?ClockInterface $clock = null, ?int $secretSize = null): self + { + return self::createFromSecret(self::generateSecret($secretSize), $clock); + } + + public function getPeriod(): int + { + $value = $this->getParameter('period'); + (is_int($value) && $value > 0) || throw new InvalidParameterException( + 'Invalid "period" parameter.', + 'period', + $value + ); + + return $value; + } + + public function getEpoch(): int + { + $value = $this->getParameter('epoch'); + (is_int($value) && $value >= 0) || throw new InvalidParameterException( + 'Invalid "epoch" parameter.', + 'epoch', + $value + ); + + return $value; + } + + public function expiresIn(): int + { + $period = $this->getPeriod(); + + return $period - (($this->clock->now()->getTimestamp() - $this->getEpoch()) % $period); + } + + /** + * The OTP at the specified input. + * + * @param 0|positive-int $input + */ + public function at(int $input): string + { + return $this->generateOTP($this->timecode($input)); + } + + public function now(): string + { + $timestamp = $this->clock->now() + ->getTimestamp(); + $timestamp >= 0 || throw new InvalidParameterException( + 'The timestamp must return a positive integer.', + 'timestamp', + $timestamp + ); + + return $this->at($timestamp); + } + + /** + * If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will + * allow time drift. The passed value is in seconds. + * + * @param 0|positive-int $timestamp + * @param null|0|positive-int $leeway + */ + public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool + { + $timestamp ??= $this->clock->now() + ->getTimestamp(); + $timestamp >= 0 || throw new InvalidParameterException( + 'Timestamp must be at least 0.', + 'timestamp', + $timestamp + ); + + if ($leeway === null) { + return $this->compareOTP($this->at($timestamp), $otp); + } + + $leeway = abs($leeway); + $leeway < $this->getPeriod() || throw new InvalidParameterException( + 'The leeway must be lower than the TOTP period', + 'leeway', + $leeway + ); + $timestampMinusLeeway = $timestamp - $leeway; + $timestampMinusLeeway >= 0 || throw new InvalidParameterException( + 'The timestamp must be greater than or equal to the leeway.', + 'timestamp', + $timestamp + ); + + return $this->compareOTP($this->at($timestampMinusLeeway), $otp) + || $this->compareOTP($this->at($timestamp), $otp) + || $this->compareOTP($this->at($timestamp + $leeway), $otp); + } + + public function getProvisioningUri(): string + { + $params = []; + if ($this->getPeriod() !== 30) { + $params['period'] = $this->getPeriod(); + } + + if ($this->getEpoch() !== 0) { + $params['epoch'] = $this->getEpoch(); + } + + return $this->generateURI('totp', $params); + } + + public function setPeriod(int $period): void + { + $this->setParameter('period', $period); + } + + public function withPeriod(int $period): self + { + $otp = clone $this; + $otp->setParameter('period', $period); + + return $otp; + } + + public function setEpoch(int $epoch): void + { + $this->setParameter('epoch', $epoch); + } + + public function withEpoch(int $epoch): self + { + $otp = clone $this; + $otp->setParameter('epoch', $epoch); + + return $otp; + } + + /** + * @return array + */ + protected function getParameterMap(): array + { + return [ + ...parent::getParameterMap(), + 'period' => static function ($value): int { + (int) $value > 0 || throw new InvalidParameterException('Period must be at least 1.', 'period', $value); + + return (int) $value; + }, + 'epoch' => static function ($value): int { + (int) $value >= 0 || throw new InvalidParameterException( + 'Epoch must be greater than or equal to 0.', + 'epoch', + $value + ); + + return (int) $value; + }, + ]; + } + + /** + * @param array $options + */ + protected function filterOptions(array &$options): void + { + parent::filterOptions($options); + + if (isset($options['epoch']) && $options['epoch'] === 0) { + unset($options['epoch']); + } + + ksort($options); + } + + /** + * @param 0|positive-int $timestamp + * + * @return 0|positive-int + */ + private function timecode(int $timestamp): int + { + $timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod()); + $timecode >= 0 || throw new InvalidParameterException( + 'Timecode must be at least 0. The timestamp must be greater than or equal to the epoch.', + 'timecode', + $timecode + ); + + return $timecode; + } +} diff --git a/plugins/google2fa/php/vendor/spomky-labs/otphp/src/TOTPInterface.php b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/TOTPInterface.php new file mode 100644 index 000000000..4288aa67f --- /dev/null +++ b/plugins/google2fa/php/vendor/spomky-labs/otphp/src/TOTPInterface.php @@ -0,0 +1,61 @@ + $query + */ + public function __construct( + private readonly string $scheme, + private readonly string $host, + private readonly string $path, + private readonly string $secret, + private readonly array $query + ) { + } + + /** + * @return non-empty-string + */ + public function getScheme(): string + { + return $this->scheme; + } + + /** + * @return non-empty-string + */ + public function getHost(): string + { + return $this->host; + } + + /** + * @return non-empty-string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @return non-empty-string + */ + public function getSecret(): string + { + return $this->secret; + } + + /** + * @return array + */ + public function getQuery(): array + { + return $this->query; + } + + /** + * @param non-empty-string $uri + */ + public static function fromString(string $uri): self + { + $parsed_url = parse_url($uri); + $parsed_url !== false || throw new InvalidProvisioningUriException('Invalid URI.'); + foreach (['scheme', 'host', 'path', 'query'] as $key) { + array_key_exists($key, $parsed_url) || throw new InvalidProvisioningUriException( + 'Not a valid OTP provisioning URI' + ); + } + $scheme = $parsed_url['scheme'] ?? null; + $host = $parsed_url['host'] ?? null; + $path = $parsed_url['path'] ?? null; + $query = $parsed_url['query'] ?? null; + $scheme === 'otpauth' || throw new InvalidProvisioningUriException('Not a valid OTP provisioning URI'); + (is_string($host) && $host !== '') || throw new InvalidProvisioningUriException('Invalid URI.'); + (is_string($path) && $path !== '') || throw new InvalidProvisioningUriException('Invalid URI.'); + is_string($query) || throw new InvalidProvisioningUriException('Invalid URI.'); + $parsedQuery = []; + parse_str($query, $parsedQuery); + array_key_exists('secret', $parsedQuery) || throw new InvalidProvisioningUriException( + 'Not a valid OTP provisioning URI' + ); + $secret = $parsedQuery['secret']; + (is_string($secret) && $secret !== '') || throw new InvalidProvisioningUriException('Invalid URI.'); + unset($parsedQuery['secret']); + + return new self($scheme, $host, $path, $secret, $parsedQuery); + } +} diff --git a/plugins/google2fa/php/vendor/symfony/deprecation-contracts/CHANGELOG.md b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/CHANGELOG.md new file mode 100644 index 000000000..7932e2613 --- /dev/null +++ b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/plugins/google2fa/php/vendor/symfony/deprecation-contracts/LICENSE b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/LICENSE new file mode 100644 index 000000000..0ed3a2465 --- /dev/null +++ b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/google2fa/php/vendor/symfony/deprecation-contracts/README.md b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/README.md new file mode 100644 index 000000000..9814864c0 --- /dev/null +++ b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/README.md @@ -0,0 +1,26 @@ +Symfony Deprecation Contracts +============================= + +A generic function and convention to trigger deprecation notices. + +This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. + +By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, +the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. + +The function requires at least 3 arguments: + - the name of the Composer package that is triggering the deprecation + - the version of the package that introduced the deprecation + - the message of the deprecation + - more arguments can be provided: they will be inserted in the message using `printf()` formatting + +Example: +```php +trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); +``` + +This will generate the following message: +`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` + +While not recommended, the deprecation notices can be completely ignored by declaring an empty +`function trigger_deprecation() {}` in your application. diff --git a/plugins/google2fa/php/vendor/symfony/deprecation-contracts/composer.json b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/composer.json new file mode 100644 index 000000000..9279eb1b0 --- /dev/null +++ b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/deprecation-contracts", + "type": "library", + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1" + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "3.7-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/plugins/google2fa/php/vendor/symfony/deprecation-contracts/function.php b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/function.php new file mode 100644 index 000000000..2d56512ba --- /dev/null +++ b/plugins/google2fa/php/vendor/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/plugins/google2fa/resources/css/google2fa.css b/plugins/google2fa/resources/css/google2fa.css new file mode 100644 index 000000000..a11719c32 --- /dev/null +++ b/plugins/google2fa/resources/css/google2fa.css @@ -0,0 +1,8 @@ +.icon_google2fa_category { + background-image: url(../icons/google2fa_icon_category.png) !important; +} + +.x-btn .x-btn-large .icon_google2fa_category { + background-image: url(../icons/google2fa_icon_category_large.png) !important; +} + diff --git a/plugins/google2fa/resources/icons/google2fa_icon_category.png b/plugins/google2fa/resources/icons/google2fa_icon_category.png new file mode 100644 index 000000000..671c36599 Binary files /dev/null and b/plugins/google2fa/resources/icons/google2fa_icon_category.png differ diff --git a/plugins/google2fa/resources/icons/google2fa_icon_category_large.png b/plugins/google2fa/resources/icons/google2fa_icon_category_large.png new file mode 100644 index 000000000..de33e64bc Binary files /dev/null and b/plugins/google2fa/resources/icons/google2fa_icon_category_large.png differ diff --git a/server/language/de_DE.UTF-8/LC_MESSAGES/grommunio_web.po b/server/language/de_DE.UTF-8/LC_MESSAGES/grommunio_web.po index e3ce2bbe8..3377e55e1 100644 --- a/server/language/de_DE.UTF-8/LC_MESSAGES/grommunio_web.po +++ b/server/language/de_DE.UTF-8/LC_MESSAGES/grommunio_web.po @@ -8136,3 +8136,94 @@ msgstr "Ungültiges UTF-8-Zeichen, möglicherweise falsch enkodiert" #, php-format msgid "Some problem encountered when encoding/decoding JSON data: - %s" msgstr "Bei der En-/Dekodierung von JSON-Daten ist ein Problem aufgetreten: - %s" + +# google2fa plugin +msgid "Google2FA Plugin" +msgstr "Google2FA Plugin" + +msgid "Two-factor authentication" +msgstr "Zwei-Faktor Authentifizierung" + +msgid "Configure two-factor authentication" +msgstr "Konfiguriere Zwei-Faktor Authentifizierung" + +msgid "The two-factor authentication provides an additional protection for the WebApp." +msgstr "Die Zwei-Faktor Authentifizierung liefert einen zusätzlichen Schutz ihrer WebApp." + +msgid "After activation you need next to your password an one-time code to log in." +msgstr "Nach der Aktivierung benötigen sie neben ihrem Passwort ebenfalls einen Einmalcode um sich einzuloggen." + +msgid "To generate an one-time code, you have to configure a second device, usually a smartphone." +msgstr "Um einen Einmalcode zu generieren, konfigurieren sie bitte ein Zweitgerät, normalerweise ein Smartphone." + +msgid "Configuration" +msgstr "Konfiguration" + +msgid "Activate or deactivate the two-factor authentication." +msgstr "Aktivieren oder Deaktivieren der Zwei-Faktor Authentifizierung." + +msgid "Current status" +msgstr "Aktueller Status" + +msgid "Activate / Deactivate" +msgstr "Aktivieren / Deaktivieren" + +msgid "You can completely reset the configuration." +msgstr "Sie können die Konfiguration vollständidg zurücksetzen." + +msgid "This deletes the secret key and deactivates the two-factor authentication." +msgstr "Dies entfernt den geheimen Schlüssel und deaktiviert die Zwei-Faktor Authentifizierung." + +msgid "If fundamental changes in the configuration were made, for example the encryption type, this step might be useful." +msgstr "Falls sie grundlegende Änderungen an der Konfiguration durchgeführt haben, ist dieser Schritt nützlich." + +msgid "Activated" +msgstr "Aktiviert" + +msgid "Deactivated" +msgstr "Deaktiviert" + +msgid "Do you really want to reset the configuration?" +msgstr "Soll die Konfiguration wirklich zurückgesetzt werden?" + +msgid "The configuration has been reset." +msgstr "Doe Konfiguration wurde zurückgesetzt." + +msgid "Please install an authentication App on second device:" +msgstr "Bitte installieren sie eine Authentifizierungs App auf einem Zweitgerät:" + +msgid "Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)" +msgstr "Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)" + +msgid "Open and configure the authentication app by scanning the QR code below." +msgstr "Öffnen sie die Authentifizierungs App und scannen sie den QR-Code." + +msgid "Alternatively, you can manually create an account with the following information." +msgstr "Alternativ können sie manuell einen Account mit folgenden Informationen anlegen." + +msgid "Application" +msgstr "Anwendung" + +msgid "Afterwards test the function with a generated code to ensure that the configurations are correct." +msgstr "Anschließend testen sie bitte die Funktion der generierten Codes um sicherzustellen dass die Konfiguration korrekt ist." + +msgid "Key" +msgstr "Schlüssel" + +msgid "Test generated code" +msgstr "Testen generierter Codes" + +msgid "Please enter code" +msgstr "Bitte geben sie den Code ein" + +msgid "Valid code, you can use the two-factor authentication." +msgstr "Gültiger Code, sie können nun die Zwei-Faktor Authentifizierung verwenden." + +msgid "Invalid code, please check code." +msgstr "Ungültiger Code, bitte prüfen sie den eingegebenen Code." + +msgid "You can use a code only one-time." +msgstr "Sie können einen Code nur einmalig verwenden." + +msgid "Please make sure that time from of server and second device are correct." +msgstr "Bitte stellen sie sicher, dass die Zeit auf dem Server und ihrem Zweitgerät korrekt sind." diff --git a/server/language/en_GB.UTF-8/LC_MESSAGES/grommunio_web.po b/server/language/en_GB.UTF-8/LC_MESSAGES/grommunio_web.po index 1e4bdf783..bb55ba6c3 100644 --- a/server/language/en_GB.UTF-8/LC_MESSAGES/grommunio_web.po +++ b/server/language/en_GB.UTF-8/LC_MESSAGES/grommunio_web.po @@ -8133,3 +8133,94 @@ msgstr "Malformed UTF-8 characters, possibly incorrectly encoded" #, php-format msgid "Some problem encountered when encoding/decoding JSON data: - %s" msgstr "Some problem encountered when encoding/decoding JSON data: - %s" + +# google2fa plugin +msgid "Google2FA Plugin" +msgstr "Google2FA Plugin" + +msgid "Two-factor authentication" +msgstr "Two-factor authentication" + +msgid "Configure two-factor authentication" +msgstr "Configure two-factor authentication" + +msgid "The two-factor authentication provides an additional protection for the WebApp." +msgstr "The two-factor authentication provides an additional protection for the WebApp." + +msgid "After activation you need next to your password an one-time code to log in." +msgstr "After activation you need next to your password an one-time code to log in." + +msgid "To generate an one-time code, you have to configure a second device, usually a smartphone." +msgstr "To generate an one-time code, you have to configure a second device, usually a smartphone." + +msgid "Configuration" +msgstr "Configuration" + +msgid "Activate or deactivate the two-factor authentication." +msgstr "Activate or deactivate the two-factor authentication." + +msgid "Current status" +msgstr "Current status" + +msgid "Activate / Deactivate" +msgstr "Activate / Deactivate" + +msgid "You can completely reset the configuration." +msgstr "You can completely reset the configuration." + +msgid "This deletes the secret key and deactivates the two-factor authentication." +msgstr "This deletes the secret key and deactivates the two-factor authentication." + +msgid "If fundamental changes in the configuration were made, for example the encryption type, this step might be useful." +msgstr "If fundamental changes in the configuration were made, for example the encryption type, this step might be useful." + +msgid "Activated" +msgstr "Activated" + +msgid "Deactivated" +msgstr "Deactivated" + +msgid "Do you really want to reset the configuration?" +msgstr "Do you really want to reset the configuration?" + +msgid "The configuration has been reset." +msgstr "The configuration has been reset." + +msgid "Please install an authentication App on second device:" +msgstr "Please install an authentication App on second device:" + +msgid "Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)" +msgstr "Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)" + +msgid "Open and configure the authentication app by scanning the QR code below." +msgstr "Open and configure the authentication app by scanning the QR code below." + +msgid "Alternatively, you can manually create an account with the following information." +msgstr "Alternatively, you can manually create an account with the following information." + +msgid "Application" +msgstr "Application" + +msgid "Afterwards test the function with a generated code to ensure that the configurations are correct." +msgstr "Afterwards test the function with a generated code to ensure that the configurations are correct." + +msgid "Key" +msgstr "Key" + +msgid "Test generated code" +msgstr "Test generated code" + +msgid "Please enter code" +msgstr "Please enter code" + +msgid "Valid code, you can use the two-factor authentication." +msgstr "Valid code, you can use the two-factor authentication." + +msgid "Invalid code, please check code." +msgstr "Invalid code, please check code." + +msgid "You can use a code only one-time." +msgstr "You can use a code only one-time." + +msgid "Please make sure that time from of server and second device are correct." +msgstr "Please make sure that time from of server and second device are correct." diff --git a/server/language/en_US.UTF-8/LC_MESSAGES/grommunio_web.po b/server/language/en_US.UTF-8/LC_MESSAGES/grommunio_web.po index 74c31e297..f0afc702b 100644 --- a/server/language/en_US.UTF-8/LC_MESSAGES/grommunio_web.po +++ b/server/language/en_US.UTF-8/LC_MESSAGES/grommunio_web.po @@ -8132,3 +8132,94 @@ msgstr "Malformed UTF-8 characters, possibly incorrectly encoded" #, php-format msgid "Some problem encountered when encoding/decoding JSON data: - %s" msgstr "Some problem encountered when encoding/decoding JSON data: - %s" + +# google2fa plugin +msgid "Google2FA Plugin" +msgstr "Google2FA Plugin" + +msgid "Two-factor authentication" +msgstr "Two-factor authentication" + +msgid "Configure two-factor authentication" +msgstr "Configure two-factor authentication" + +msgid "The two-factor authentication provides an additional protection for the WebApp." +msgstr "The two-factor authentication provides an additional protection for the WebApp." + +msgid "After activation you need next to your password an one-time code to log in." +msgstr "After activation you need next to your password an one-time code to log in." + +msgid "To generate an one-time code, you have to configure a second device, usually a smartphone." +msgstr "To generate an one-time code, you have to configure a second device, usually a smartphone." + +msgid "Configuration" +msgstr "Configuration" + +msgid "Activate or deactivate the two-factor authentication." +msgstr "Activate or deactivate the two-factor authentication." + +msgid "Current status" +msgstr "Current status" + +msgid "Activate / Deactivate" +msgstr "Activate / Deactivate" + +msgid "You can completely reset the configuration." +msgstr "You can completely reset the configuration." + +msgid "This deletes the secret key and deactivates the two-factor authentication." +msgstr "This deletes the secret key and deactivates the two-factor authentication." + +msgid "If fundamental changes in the configuration were made, for example the encryption type, this step might be useful." +msgstr "If fundamental changes in the configuration were made, for example the encryption type, this step might be useful." + +msgid "Activated" +msgstr "Activated" + +msgid "Deactivated" +msgstr "Deactivated" + +msgid "Do you really want to reset the configuration?" +msgstr "Do you really want to reset the configuration?" + +msgid "The configuration has been reset." +msgstr "The configuration has been reset." + +msgid "Please install an authentication App on second device:" +msgstr "Please install an authentication App on second device:" + +msgid "Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)" +msgstr "Google Authenticator (Android, iOS, BlackBerry), Authenticator (Windows Phone)" + +msgid "Open and configure the authentication app by scanning the QR code below." +msgstr "Open and configure the authentication app by scanning the QR code below." + +msgid "Alternatively, you can manually create an account with the following information." +msgstr "Alternatively, you can manually create an account with the following information." + +msgid "Application" +msgstr "Application" + +msgid "Afterwards test the function with a generated code to ensure that the configurations are correct." +msgstr "Afterwards test the function with a generated code to ensure that the configurations are correct." + +msgid "Key" +msgstr "Key" + +msgid "Test generated code" +msgstr "Test generated code" + +msgid "Please enter code" +msgstr "Please enter code" + +msgid "Valid code, you can use the two-factor authentication." +msgstr "Valid code, you can use the two-factor authentication." + +msgid "Invalid code, please check code." +msgstr "Invalid code, please check code." + +msgid "You can use a code only one-time." +msgstr "You can use a code only one-time." + +msgid "Please make sure that time from of server and second device are correct." +msgstr "Please make sure that time from of server and second device are correct."