uawdijnntqw1x1x1
IP : 216.73.216.84
Hostname : webm003.cluster107.gra.hosting.ovh.net
Kernel : Linux webm003.cluster107.gra.hosting.ovh.net 5.15.167-ovh-vps-grsec-zfs-classid #1 SMP Tue Sep 17 08:14:20 UTC 2024 x86_64
Disable Function : _dyuweyrj4,_dyuweyrj4r,dl
OS : Linux
PATH:
/
home
/
opticamezl
/
www
/
newok
/
07d6c
/
..
/
1a0f4
/
..
/
c9989
/
..
/
assets
/
..
/
.
/
.
/
cli
/
..
/
multifactorauth.zip
/
/
PKpd�\ Ө��email/email.xmlnu�[���<?xml version="1.0" encoding="UTF-8"?> <extension type="plugin" group="multifactorauth" method="upgrade"> <name>plg_multifactorauth_email</name> <author>Joomla! Project</author> <creationDate>2022-05</creationDate> <copyright>(C) 2022 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>4.2.0</version> <description>PLG_MULTIFACTORAUTH_EMAIL_XML_DESCRIPTION</description> <namespace path="src">Joomla\Plugin\Multifactorauth\Email</namespace> <files> <folder plugin="email">services</folder> <folder>src</folder> </files> <languages> <language tag="en-GB">language/en-GB/plg_multifactorauth_email.ini</language> <language tag="en-GB">language/en-GB/plg_multifactorauth_email.sys.ini</language> </languages> <config> <fields name="params"> <fieldset name="basic"> <field name="force_enable" type="radio" layout="joomla.form.field.radio.switcher" label="PLG_MULTIFACTORAUTH_EMAIL_CONFIG_FORCE_ENABLE_LABEL" description="PLG_MULTIFACTORAUTH_EMAIL_CONFIG_FORCE_ENABLE_DESC" default="0" > <option value="0">JNO</option> <option value="1">JYES</option> </field> <field name="timestep" type="list" label="PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_LABEL" description="PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_DESC" default="120" > <option value="30">PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_30</option> <option value="60">PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_60</option> <option value="120">PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_120</option> <option value="180">PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_180</option> <option value="300">PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_300</option> </field> </fieldset> </fields> </config> </extension> PKpd�\�Pp��email/services/provider.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.email * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') || die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\User\UserFactoryInterface; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Multifactorauth\Email\Extension\Email; return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.2.0 */ public function register(Container $container) { $container->set( PluginInterface::class, function (Container $container) { $config = (array) PluginHelper::getPlugin('multifactorauth', 'email'); $subject = $container->get(DispatcherInterface::class); $plugin = new Email($subject, $config); $plugin->setApplication(Factory::getApplication()); $plugin->setUserFactory($container->get(UserFactoryInterface::class)); return $plugin; } ); } }; PKpd�\�o; �T�Temail/src/Extension/Email.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.email * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Email\Extension; use Exception; use Joomla\CMS\Encrypt\Totp; use Joomla\CMS\Event\MultiFactor\BeforeDisplayMethods; use Joomla\CMS\Event\MultiFactor\Captive; use Joomla\CMS\Event\MultiFactor\GetMethod; use Joomla\CMS\Event\MultiFactor\GetSetup; use Joomla\CMS\Event\MultiFactor\SaveSetup; use Joomla\CMS\Event\MultiFactor\Validate; use Joomla\CMS\Factory; use Joomla\CMS\Input\Input; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Mail\Exception\MailDisabledException; use Joomla\CMS\Mail\MailTemplate; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryAwareTrait; use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; use Joomla\Component\Users\Administrator\Table\MfaTable; use Joomla\Event\SubscriberInterface; use PHPMailer\PHPMailer\Exception as phpMailerException; use RuntimeException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Multi-factor Authentication using a Validation Code sent by Email. * * Requires entering a 6-digit code sent to the user through email. These codes change automatically * on a frequency set in the plugin options (30 seconds to 5 minutes, default 2 minutes). * * @since 4.2.0 */ class Email extends CMSPlugin implements SubscriberInterface { use UserFactoryAwareTrait; /** * Generated OTP length. Constant: 6 numeric digits. * * @since 4.2.0 */ private const CODE_LENGTH = 6; /** * Length of the secret key used for generating the OTPs. Constant: 20 characters. * * @since 4.2.0 */ private const SECRET_KEY_LENGTH = 20; /** * Should I try to detect and register legacy event listeners, i.e. methods which accept unwrapped arguments? While * this maintains a great degree of backwards compatibility to Joomla! 3.x-style plugins it is much slower. You are * advised to implement your plugins using proper Listeners, methods accepting an AbstractEvent as their sole * parameter, for best performance. Also bear in mind that Joomla! 5.x onwards will only allow proper listeners, * removing support for legacy Listeners. * * @var boolean * @since 4.2.0 * * @deprecated 4.3 will be removed in 6.0 * Implement your plugin methods accepting an AbstractEvent object * Example: * onEventTriggerName(AbstractEvent $event) { * $context = $event->getArgument(...); * } */ protected $allowLegacyListeners = false; /** * Autoload this plugin's language files * * @var boolean * @since 4.2.0 */ protected $autoloadLanguage = true; /** * The MFA Method name handled by this plugin * * @var string * @since 4.2.0 */ private $mfaMethodName = 'email'; /** * Returns an array of events this subscriber will listen to. * * @return array * * @since 4.2.0 */ public static function getSubscribedEvents(): array { return [ 'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod', 'onUserMultifactorCaptive' => 'onUserMultifactorCaptive', 'onUserMultifactorGetSetup' => 'onUserMultifactorGetSetup', 'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup', 'onUserMultifactorValidate' => 'onUserMultifactorValidate', 'onUserMultifactorBeforeDisplayMethods' => 'onUserMultifactorBeforeDisplayMethods', ]; } /** * Gets the identity of this MFA Method * * @param GetMethod $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetMethod(GetMethod $event): void { $event->addResult( new MethodDescriptor( [ 'name' => $this->mfaMethodName, 'display' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_DISPLAYEDAS'), 'shortinfo' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_SHORTINFO'), 'image' => 'media/plg_multifactorauth_email/images/email.svg', ] ) ); } /** * Returns the information which allows Joomla to render the Captive MFA page. This is the page * which appears right after you log in and asks you to validate your login with MFA. * * @param Captive $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorCaptive(Captive $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $key = $options['key'] ?? ''; // Send an email message with a new code and ask the user to enter it. $user = $this->getUserFactory()->loadUserById($record->user_id); try { $this->sendCode($key, $user); } catch (\Exception $e) { return; } $event->addResult( new CaptiveRenderOptions( [ // Custom HTML to display above the MFA form 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_PRE_MESSAGE'), // How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML) 'field_type' => 'input', // The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. 'input_type' => 'text', // The attributes for the HTML input box. 'input_attributes' => [ 'pattern' => '[0-9]{6}', 'maxlength' => '6', 'inputmode' => 'numeric', 'required' => 'true', 'autocomplete' => 'one-time-code', 'aria-autocomplete' => 'none', ], // Placeholder text for the HTML input box. Leave empty if you don't need it. 'placeholder' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_SETUP_PLACEHOLDER'), // Label to show above the HTML input box. Leave empty if you don't need it. 'label' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_LABEL'), // Custom HTML. Only used when field_type = custom. 'html' => '', // Custom HTML to display below the MFA form 'post_message' => '', // Should I hide the default Submit button? 'hide_submit' => false, // Is this MFA method validating against all configured authenticators of the same type? 'allowEntryBatching' => false, ] ) ); } /** * Returns the information which allows Joomla to render the MFA setup page. This is the page * which allows the user to add or modify a MFA Method for their user account. If the record * does not correspond to your plugin return an empty array. * * @param GetSetup $event The event we are handling * * @return void * @throws \Exception * @since 4.2.0 */ public function onUserMultifactorGetSetup(GetSetup $event): void { /** @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $key = $options['key'] ?? ''; $isKeyAlreadySetup = !empty($key); // If there's a key in the session use that instead. $session = $this->getApplication()->getSession(); $session->get('plg_multifactorauth_email.emailcode.key', $key); // Initialize objects $timeStep = min(max((int) $this->params->get('timestep', 120), 30), 900); $totp = new Totp($timeStep, self::CODE_LENGTH, self::SECRET_KEY_LENGTH); // If there's still no key in the options, generate one and save it in the session if (!$isKeyAlreadySetup) { $key = $totp->generateSecret(); $session->set('plg_multifactorauth_email.emailcode.key', $key); $session->set('plg_multifactorauth_email.emailcode.user_id', $record->user_id); $user = $this->getUserFactory()->loadUserById($record->user_id); $this->sendCode($key, $user); $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_DISPLAYEDAS'), 'hidden_data' => [ 'key' => $key, ], 'field_type' => 'input', 'input_type' => 'text', 'input_attributes' => [ 'pattern' => '[0-9]{6}', 'maxlength' => '6', 'inputmode' => 'numeric', 'required' => 'true', 'autocomplete' => 'one-time-code', 'aria-autocomplete' => 'none', ], 'input_value' => '', 'placeholder' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_SETUP_PLACEHOLDER'), 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_PRE_MESSAGE'), 'label' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_LABEL'), ] ) ); } else { $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_DISPLAYEDAS'), 'input_type' => 'hidden', 'html' => '', ] ) ); } } /** * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The * message of the exception will be displayed to the user. If the record does not correspond to your plugin return * an empty array. * * @param SaveSetup $event The event we are handling * * @return void The configuration data to save to the database * @since 4.2.0 */ public function onUserMultifactorSaveSetup(SaveSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. * @var Input $input The user input you are going to take into account. */ $record = $event['record']; $input = $event['input']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $key = $options['key'] ?? ''; $isKeyAlreadySetup = !empty($key); $session = $this->getApplication()->getSession(); // If there is no key in the options fetch one from the session if (empty($key)) { $key = $session->get('plg_multifactorauth_email.emailcode.key', null); } // If there is still no key in the options throw an error if (empty($key)) { throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); } /** * If the code is empty but the key already existed in $options someone is simply changing the title / default * Method status. We can allow this and stop checking anything else now. */ $code = $input->getCmd('code'); if (empty($code) && $isKeyAlreadySetup) { $event->addResult($options); return; } // In any other case validate the submitted code $timeStep = min(max((int) $this->params->get('timestep', 120), 30), 900); $totp = new Totp($timeStep, self::CODE_LENGTH, self::SECRET_KEY_LENGTH); $isValid = $totp->checkCode((string) $key, (string) $code); if (!$isValid) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_EMAIL_ERR_INVALID_CODE'), 500); } // The code is valid. Unset the key from the session. $session->set('plg_multifactorauth_email.emailcode.key', null); // Return the configuration to be serialized $event->addResult(['key' => $key]); } /** * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor * Authentication page. If the record does not correspond to your plugin return FALSE. * * @param Validate $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorValidate(Validate $event): void { /** * @var MfaTable $record The MFA Method's record you're validating against * @var User $user The user record * @var string|null $code The submitted code */ $record = $event['record']; $user = $event['user']; $code = $event['code']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { $event->addResult(false); return; } // Double check the MFA Method is for the correct user if ($user->id != $record->user_id) { $event->addResult(false); return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $key = $options['key'] ?? ''; // If there is no key in the options throw an error if (empty($key)) { $event->addResult(false); return; } // Check the MFA code for validity $timeStep = min(max((int) $this->params->get('timestep', 120), 30), 900); $totp = new Totp($timeStep, self::CODE_LENGTH, self::SECRET_KEY_LENGTH); $event->addResult($totp->checkCode($key, (string) $code)); } /** * Executes before showing the MFA Methods for the user. Used for the Force Enable feature. * * @param BeforeDisplayMethods $event The event we are handling * * @return void * @throws \Exception * @since 4.2.0 */ public function onUserMultifactorBeforeDisplayMethods(BeforeDisplayMethods $event): void { /** @var ?User $user */ $user = $event['user']; // Is the forced enable feature activated? if ($this->params->get('force_enable', 0) != 1) { return; } // Get MFA Methods for this user $userMfaRecords = MfaHelper::getUserMfaRecords($user->id); // If there are no Methods go back if (\count($userMfaRecords) < 1) { return; } // If the only Method is backup codes go back if (\count($userMfaRecords) == 1) { /** @var MfaTable $record */ $record = reset($userMfaRecords); if ($record->method == 'backupcodes') { return; } } // If I already have the email Method go back $emailRecords = array_filter( $userMfaRecords, function (MfaTable $record) { return $record->method == 'email'; } ); if (\count($emailRecords)) { return; } // Add the email Method try { /** @var MVCFactoryInterface $factory */ $factory = $this->getApplication()->bootComponent('com_users')->getMVCFactory(); /** @var MfaTable $record */ $record = $factory->createTable('Mfa', 'Administrator'); $record->reset(); $timeStep = min(max((int) $this->params->get('timestep', 120), 30), 900); $totp = new Totp($timeStep, self::CODE_LENGTH, self::SECRET_KEY_LENGTH); $record->save( [ 'method' => 'email', 'title' => Text::_('PLG_MULTIFACTORAUTH_EMAIL_LBL_DISPLAYEDAS'), 'options' => [ 'key' => ($totp)->generateSecret(), ], 'default' => 0, 'user_id' => $user->id, ] ); } catch (\Exception $event) { // Fail gracefully } } /** * Decodes the options from a record into an options object. * * @param MfaTable $record The record to decode * * @return array * @since 4.2.0 */ private function decodeRecordOptions(MfaTable $record): array { $options = [ 'key' => '', ]; if (!empty($record->options)) { $recordOptions = $record->options; $options = array_merge($options, $recordOptions); } return $options; } /** * Creates a new TOTP code based on secret key $key and sends it to the user via email. * * @param string $key The TOTP secret key * @param User|null $user The Joomla! user to use * * @return void * @throws \Exception * @since 4.2.0 */ private function sendCode(string $key, ?User $user = null) { static $alreadySent = false; // Make sure we have a user if (!is_object($user) || !($user instanceof User)) { $user = $this->getApplication()->getIdentity() ?: $this->getUserFactory()->loadUserById(0); } if ($alreadySent) { return; } $alreadySent = true; // Get the API objects $timeStep = min(max((int) $this->params->get('timestep', 120), 30), 900); $totp = new Totp($timeStep, self::CODE_LENGTH, self::SECRET_KEY_LENGTH); // Create the list of variable replacements $code = $totp->getCode($key); $replacements = [ 'code' => $code, 'sitename' => $this->getApplication()->get('sitename'), 'siteurl' => Uri::base(), 'username' => $user->username, 'email' => $user->email, 'fullname' => $user->name, ]; try { $jLanguage = $this->getApplication()->getLanguage(); $mailer = new MailTemplate('plg_multifactorauth_email.mail', $jLanguage->getTag()); $mailer->addRecipient($user->email, $user->name); $mailer->addTemplateData($replacements); $didSend = $mailer->send(); } catch (MailDisabledException | phpMailerException $exception) { try { Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); } catch (\RuntimeException $exception) { $this->getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); } } try { // The user somehow managed to not install the mail template. I'll send the email the traditional way. if (isset($didSend) && !$didSend) { $subject = Text::_('PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT'); $body = Text::_('PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY'); foreach ($replacements as $key => $value) { $subject = str_replace('{' . strtoupper($key) . '}', $value, $subject); $body = str_replace('{' . strtoupper($key) . '}', $value, $body); } $mailer = Factory::getMailer(); $mailer->setSubject($subject); $mailer->setBody($body); $mailer->addRecipient($user->email, $user->name); $mailer->Send(); } } catch (MailDisabledException | phpMailerException $exception) { try { Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); } catch (\RuntimeException $exception) { $this->getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); } } } } PKpd�\X<G���fixed/services/provider.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.fixed * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') || die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Multifactorauth\Fixed\Extension\Fixed; return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.2.0 */ public function register(Container $container) { $container->set( PluginInterface::class, function (Container $container) { $config = (array) PluginHelper::getPlugin('multifactorauth', 'fixed'); $subject = $container->get(DispatcherInterface::class); return new Fixed($subject, $config); } ); } }; PKpd�\hD�ӄ�fixed/fixed.xmlnu�[���<?xml version="1.0" encoding="UTF-8"?> <extension type="plugin" group="multifactorauth" method="upgrade"> <name>plg_multifactorauth_fixed</name> <author>Joomla! Project</author> <creationDate>2022-05</creationDate> <copyright>(C) 2022 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>4.2.0</version> <description>PLG_MULTIFACTORAUTH_FIXED_XML_DESCRIPTION</description> <namespace path="src">Joomla\Plugin\Multifactorauth\Fixed</namespace> <files> <folder plugin="fixed">services</folder> <folder>src</folder> </files> <languages> <language tag="en-GB">language/en-GB/plg_multifactorauth_fixed.ini</language> <language tag="en-GB">language/en-GB/plg_multifactorauth_fixed.sys.ini</language> </languages> </extension> PKpd�\����.�.fixed/src/Extension/Fixed.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.fixed * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Fixed\Extension; use Joomla\CMS\Event\MultiFactor\Captive; use Joomla\CMS\Event\MultiFactor\GetMethod; use Joomla\CMS\Event\MultiFactor\GetSetup; use Joomla\CMS\Event\MultiFactor\SaveSetup; use Joomla\CMS\Event\MultiFactor\Validate; use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\User\User; use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; use Joomla\Component\Users\Administrator\Table\MfaTable; use Joomla\Event\SubscriberInterface; use Joomla\Input\Input; use RuntimeException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Multi-factor Authentication using a fixed code. * * Requires a static string (password), different for each user. It effectively works as a second * password. The fixed code is stored hashed, like a regular password. * * This is NOT to be used on production sites. It serves as a demonstration plugin and as a template * for developers to create their own custom Multi-factor Authentication plugins. * * @since 4.2.0 */ class Fixed extends CMSPlugin implements SubscriberInterface { /** * Affects constructor behavior. If true, language files will be loaded automatically. * * @var boolean * @since 4.2.0 */ protected $autoloadLanguage = true; /** * The MFA Method name handled by this plugin * * @var string * @since 4.2.0 */ private $mfaMethodName = 'fixed'; /** * Should I try to detect and register legacy event listeners, i.e. methods which accept unwrapped arguments? While * this maintains a great degree of backwards compatibility to Joomla! 3.x-style plugins it is much slower. You are * advised to implement your plugins using proper Listeners, methods accepting an AbstractEvent as their sole * parameter, for best performance. Also bear in mind that Joomla! 5.x onwards will only allow proper listeners, * removing support for legacy Listeners. * * @var boolean * @since 4.2.0 * * @deprecated 4.3 will be removed in 6.0 * Implement your plugin methods accepting an AbstractEvent object * Example: * onEventTriggerName(AbstractEvent $event) { * $context = $event->getArgument(...); * } */ protected $allowLegacyListeners = false; /** * Returns an array of events this subscriber will listen to. * * @return array * * @since 4.2.0 */ public static function getSubscribedEvents(): array { return [ 'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod', 'onUserMultifactorCaptive' => 'onUserMultifactorCaptive', 'onUserMultifactorGetSetup' => 'onUserMultifactorGetSetup', 'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup', 'onUserMultifactorValidate' => 'onUserMultifactorValidate', ]; } /** * Gets the identity of this MFA Method * * @param GetMethod $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetMethod(GetMethod $event): void { $event->addResult( new MethodDescriptor( [ 'name' => $this->mfaMethodName, 'display' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_DISPLAYEDAS'), 'shortinfo' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_SHORTINFO'), 'image' => 'media/plg_multifactorauth_fixed/images/fixed.svg', ] ) ); } /** * Returns the information which allows Joomla to render the Captive MFA page. This is the page * which appears right after you log in and asks you to validate your login with MFA. * * @param Captive $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorCaptive(Captive $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } $event->addResult( new CaptiveRenderOptions( [ // Custom HTML to display above the MFA form 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_PREMESSAGE'), // How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML) 'field_type' => 'input', // The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. 'input_type' => 'password', // Placeholder text for the HTML input box. Leave empty if you don't need it. 'placeholder' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_PLACEHOLDER'), // Label to show above the HTML input box. Leave empty if you don't need it. 'label' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_LABEL'), // Custom HTML. Only used when field_type = custom. 'html' => '', // Custom HTML to display below the MFA form 'post_message' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_POSTMESSAGE'), ] ) ); } /** * Returns the information which allows Joomla to render the MFA setup page. This is the page * which allows the user to add or modify a MFA Method for their user account. If the record * does not correspond to your plugin return an empty array. * * @param GetSetup $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetSetup(GetSetup $event): void { /** @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); /** * Return the parameters used to render the GUI. * * Some MFA Methods need to display a different interface before and after the setup. For example, when setting * up Google Authenticator or a hardware OTP dongle you need the user to enter a MFA code to verify they are in * possession of a correctly configured device. After the setup is complete you don't want them to see that * field again. In the first state you could use the tabular_data to display the setup values, pre_message to * display the QR code and field_type=input to let the user enter the MFA code. In the second state do the same * BUT set field_type=custom, set html='' and show_submit=false to effectively hide the setup form from the * user. */ $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_DEFAULTTITLE'), 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_SETUP_PREMESSAGE'), 'field_type' => 'input', 'input_type' => 'password', 'input_value' => $options->fixed_code, 'placeholder' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_PLACEHOLDER'), 'label' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_LABEL'), 'post_message' => Text::_('PLG_MULTIFACTORAUTH_FIXED_LBL_SETUP_POSTMESSAGE'), ] ) ); } /** * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The * message of the exception will be displayed to the user. If the record does not correspond to your plugin return * an empty array. * * @param SaveSetup $event The event we are handling * * @return void The configuration data to save to the database * @since 4.2.0 */ public function onUserMultifactorSaveSetup(SaveSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. * @var Input $input The user input you are going to take into account. */ $record = $event['record']; $input = $event['input']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); // Merge with the submitted form data $code = $input->get('code', $options->fixed_code, 'raw'); // Make sure the code is not empty if (empty($code)) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_FIXED_ERR_EMPTYCODE')); } // Return the configuration to be serialized $event->addResult(['fixed_code' => $code]); } /** * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor * Authentication. If the record does not correspond to your plugin return FALSE. * * @param Validate $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorValidate(Validate $event): void { /** * @var MfaTable $record The MFA Method's record you're validating against * @var User $user The user record * @var string|null $code The submitted code */ $record = $event['record']; $user = $event['user']; $code = $event['code']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { $event->addResult(false); return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); // Double check the MFA Method is for the correct user if ($user->id != $record->user_id) { $event->addResult(false); return; } // Check the MFA code for validity $event->addResult(hash_equals($options->fixed_code, $code ?? '')); } /** * Decodes the options from a record into an options object. * * @param MfaTable $record The record to decode options for * * @return object * @since 4.2.0 */ private function decodeRecordOptions(MfaTable $record): object { $options = [ 'fixed_code' => '', ]; if (!empty($record->options)) { $recordOptions = $record->options; $options = array_merge($options, $recordOptions); } return (object) $options; } } PKpd�\�}^yubikey/services/provider.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.yubikey * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') || die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Multifactorauth\Yubikey\Extension\Yubikey; return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.2.0 */ public function register(Container $container) { $container->set( PluginInterface::class, function (Container $container) { $config = (array) PluginHelper::getPlugin('multifactorauth', 'yubikey'); $subject = $container->get(DispatcherInterface::class); $plugin = new Yubikey($subject, $config); $plugin->setApplication(Factory::getApplication()); return $plugin; } ); } }; PKpd�\�C0�R�R!yubikey/src/Extension/Yubikey.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.yubikey * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Yubikey\Extension; use Exception; use Joomla\CMS\Event\MultiFactor\Captive; use Joomla\CMS\Event\MultiFactor\GetMethod; use Joomla\CMS\Event\MultiFactor\GetSetup; use Joomla\CMS\Event\MultiFactor\SaveSetup; use Joomla\CMS\Event\MultiFactor\Validate; use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; use Joomla\Component\Users\Administrator\Table\MfaTable; use Joomla\Event\SubscriberInterface; use Joomla\Input\Input; use RuntimeException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Multi-factor Authentication using Yubikey Plugin * * @since 4.2.0 */ class Yubikey extends CMSPlugin implements SubscriberInterface { /** * Affects constructor behavior. If true, language files will be loaded automatically. * * @var boolean * @since 3.2 */ protected $autoloadLanguage = true; /** * The MFA Method name handled by this plugin * * @var string * @since 4.2.0 */ private $mfaMethodName = 'yubikey'; /** * Should I try to detect and register legacy event listeners, i.e. methods which accept unwrapped arguments? While * this maintains a great degree of backwards compatibility to Joomla! 3.x-style plugins it is much slower. You are * advised to implement your plugins using proper Listeners, methods accepting an AbstractEvent as their sole * parameter, for best performance. Also bear in mind that Joomla! 5.x onwards will only allow proper listeners, * removing support for legacy Listeners. * * @var boolean * @since 4.2.0 * * @deprecated 4.3 will be removed in 6.0 * Implement your plugin methods accepting an AbstractEvent object * Example: * onEventTriggerName(AbstractEvent $event) { * $context = $event->getArgument(...); * } */ protected $allowLegacyListeners = false; /** * Returns an array of events this subscriber will listen to. * * @return array * * @since 4.2.0 */ public static function getSubscribedEvents(): array { return [ 'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod', 'onUserMultifactorCaptive' => 'onUserMultifactorCaptive', 'onUserMultifactorGetSetup' => 'onUserMultifactorGetSetup', 'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup', 'onUserMultifactorValidate' => 'onUserMultifactorValidate', ]; } /** * Gets the identity of this MFA Method * * @param GetMethod $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetMethod(GetMethod $event): void { $event->addResult( new MethodDescriptor( [ 'name' => $this->mfaMethodName, 'display' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), 'shortinfo' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_SHORTINFO'), 'image' => 'media/plg_multifactorauth_yubikey/images/yubikey.svg', 'allowEntryBatching' => true, ] ) ); } /** * Returns the information which allows Joomla to render the Captive MFA page. This is the page * which appears right after you log in and asks you to validate your login with MFA. * * @param Captive $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorCaptive(Captive $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } $event->addResult( new CaptiveRenderOptions( [ // Custom HTML to display above the MFA form 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_CAPTIVE_PROMPT'), // How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML) 'field_type' => 'input', // The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. 'input_type' => 'text', // Placeholder text for the HTML input box. Leave empty if you don't need it. 'placeholder' => '', // Label to show above the HTML input box. Leave empty if you don't need it. 'label' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_CODE_LABEL'), // Custom HTML. Only used when field_type = custom. 'html' => '', // Custom HTML to display below the MFA form 'post_message' => '', // Allow authentication against all entries of this MFA Method. 'allowEntryBatching' => 1, ] ) ); } /** * Returns the information which allows Joomla to render the MFA setup page. This is the page * which allows the user to add or modify a MFA Method for their user account. If the record * does not correspond to your plugin return an empty array. * * @param GetSetup $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetSetup(GetSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $keyID = $options['id'] ?? ''; if (empty($keyID)) { $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_LBL_SETUP_INSTRUCTIONS'), 'field_type' => 'input', 'input_type' => 'text', 'input_value' => $keyID, 'placeholder' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_LBL_SETUP_PLACEHOLDER'), 'label' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_LBL_SETUP_LABEL'), ] ) ); } else { $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), 'pre_message' => Text::sprintf('PLG_MULTIFACTORAUTH_YUBIKEY_LBL_AFTERSETUP_INSTRUCTIONS', $keyID), 'input_type' => 'hidden', ] ) ); } } /** * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The * message of the exception will be displayed to the user. If the record does not correspond to your plugin return * an empty array. * * @param SaveSetup $event The event we are handling * * @return void The configuration data to save to the database * @throws \Exception * @since 4.2.0 */ public function onUserMultifactorSaveSetup(SaveSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. * @var Input $input The user input you are going to take into account. */ $record = $event['record']; $input = $event['input']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $keyID = $options['id'] ?? ''; $isKeyAlreadySetup = !empty($keyID); /** * If the submitted code is 12 characters and identical to our existing key there is no change, perform no * further checks. */ $code = $input->getString('code'); if ($isKeyAlreadySetup || ((strlen($code) == 12) && ($code == $keyID))) { $event->addResult($options); return; } // If an empty code or something other than 44 characters was submitted I'm not having any of this! if (empty($code) || (strlen($code) != 44)) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_ERR_VALIDATIONFAILED'), 500); } // Validate the code $isValid = $this->validateYubikeyOtp($code); if (!$isValid) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_ERR_VALIDATIONFAILED'), 500); } // The code is valid. Keep the Yubikey ID (first twelve characters) $keyID = substr($code, 0, 12); // Return the configuration to be serialized $event->addResult(['id' => $keyID]); } /** * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor * Authentication page. If the record does not correspond to your plugin return FALSE. * * @param Validate $event The event we are handling * * @return void * @throws \Exception * @since 4.2.0 */ public function onUserMultifactorValidate(Validate $event): void { /** * @var MfaTable $record The MFA Method's record you're validating against * @var User $user The user record * @var string $code The submitted code */ $record = $event['record']; $user = $event['user']; $code = $event['code']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { $event->addResult(false); return; } // Double check the MFA Method is for the correct user if ($user->id != $record->user_id) { $event->addResult(false); return; } try { $records = MfaHelper::getUserMfaRecords($record->user_id); $records = array_filter( $records, function ($rec) use ($record) { return $rec->method === $record->method; } ); } catch (\Exception $e) { $records = []; } // Loop all records, stop if at least one matches $result = array_reduce( $records, function (bool $carry, $aRecord) use ($code) { return $carry || $this->validateAgainstRecord($aRecord, $code); }, false ); $event->addResult($result); } /** * Validates a Yubikey OTP against the Yubikey servers * * @param string $otp The OTP generated by your Yubikey * * @return boolean True if it's a valid OTP * @throws \Exception * @since 4.2.0 */ private function validateYubikeyOtp(string $otp): bool { // Let the user define a client ID and a secret key in the plugin's configuration $clientID = $this->params->get('client_id', 1); $secretKey = $this->params->get('secret', ''); $serverQueue = trim($this->params->get('servers', '')); if (!empty($serverQueue)) { $serverQueue = explode("\r", $serverQueue); } if (empty($serverQueue)) { $serverQueue = [ 'https://api.yubico.com/wsapi/2.0/verify', 'https://api2.yubico.com/wsapi/2.0/verify', 'https://api3.yubico.com/wsapi/2.0/verify', 'https://api4.yubico.com/wsapi/2.0/verify', 'https://api5.yubico.com/wsapi/2.0/verify', ]; } shuffle($serverQueue); $gotResponse = false; $http = HttpFactory::getHttp(); $token = $this->getApplication()->getFormToken(); $nonce = md5($token . uniqid(random_int(0, mt_getrandmax()))); $response = null; while (!$gotResponse && !empty($serverQueue)) { $server = array_shift($serverQueue); $uri = new Uri($server); // The client ID for signing the response $uri->setVar('id', $clientID); // The OTP we read from the user $uri->setVar('otp', $otp); // This prevents a REPLAYED_OTP status if the token doesn't change after a user submits an invalid OTP $uri->setVar('nonce', $nonce); // Minimum service level required: 50% (at least 50% of the YubiCloud servers must reply positively for the // OTP to validate) $uri->setVar('sl', 50); // Timeout waiting for YubiCloud servers to reply: 5 seconds. $uri->setVar('timeout', 5); // Set up the optional HMAC-SHA1 signature for the request. $this->signRequest($uri, $secretKey); if ($uri->hasVar('h')) { $uri->setVar('h', urlencode($uri->getVar('h'))); } try { $response = $http->get($uri->toString(), [], 6); if (!empty($response)) { $gotResponse = true; } else { continue; } } catch (\Exception $exc) { // No response, continue with the next server continue; } } if (empty($response)) { $gotResponse = false; } // No server replied; we can't validate this OTP if (!$gotResponse) { return false; } // Parse response $lines = explode("\n", $response->body); $data = []; foreach ($lines as $line) { $line = trim($line); $parts = explode('=', $line, 2); if (count($parts) < 2) { continue; } $data[$parts[0]] = $parts[1]; } // Validate the signature $h = $data['h'] ?? null; $fakeUri = Uri::getInstance('http://www.example.com'); $fakeUri->setQuery($data); $this->signRequest($fakeUri, $secretKey); $calculatedH = $fakeUri->getVar('h', null); if ($calculatedH != $h) { return false; } // Validate the response - We need an OK message reply if ($data['status'] !== 'OK') { return false; } // Validate the response - We need a confidence level over 50% if ($data['sl'] < 50) { return false; } // Validate the response - The OTP must match if ($data['otp'] != $otp) { return false; } // Validate the response - The token must match if ($data['nonce'] != $nonce) { return false; } return true; } /** * Sign the request to YubiCloud. * * @param Uri $uri The request URI to sign * @param string $secret The secret key to sign with * * @return void * @since 4.2.0 * * @see https://developers.yubico.com/yubikey-val/Validation_Protocol_V2.0.html */ private function signRequest(Uri $uri, string $secret): void { // Make sure we have an encoding secret $secret = trim($secret); if (empty($secret)) { return; } // I will need base64 encoding and decoding if (!function_exists('base64_encode') || !function_exists('base64_decode')) { return; } // I need HMAC-SHA-1 support. Therefore I check for HMAC and SHA1 support in the PHP 'hash' extension. if (!function_exists('hash_hmac') || !function_exists('hash_algos')) { return; } $algos = hash_algos(); if (!in_array('sha1', $algos)) { return; } // Get the parameters /** @var array $vars I have to explicitly state the type because the Joomla docblock is wrong :( */ $vars = $uri->getQuery(true); // 'h' is the hash and it doesn't participate in the calculation of itself. if (isset($vars['h'])) { unset($vars['h']); } // Alphabetically sort the set of key/value pairs by key order. ksort($vars); /** * Construct a single line with each ordered key/value pair concatenated using &, and each key and value * concatenated with =. Do not add any line breaks. Do not add whitespace. * * Now, if you thought I can't really write PHP code, a.k.a. why not use http_build_query, read on. * * The way YubiKey expects the query to be built is UTTERLY WRONG. They are doing string concatenation, not * URL query building! Therefore you cannot use http_build_query(). Instead, you need to use dumb string * concatenation. I kid you not. If you want to laugh (or cry) read their Auth_Yubico class. It's 1998 all over * again. */ $stringToSign = ''; foreach ($vars as $k => $v) { $stringToSign .= '&' . $k . '=' . $v; } $stringToSign = ltrim($stringToSign, '&'); /** * Apply the HMAC-SHA-1 algorithm on the line as an octet string using the API key as key (remember to * base64decode the API key obtained from Yubico). */ $decodedKey = base64_decode($secret); $hash = hash_hmac('sha1', $stringToSign, $decodedKey, true); /** * Base 64 encode the resulting value according to RFC 4648, for example, t2ZMtKeValdA+H0jVpj3LIichn4= */ $h = base64_encode($hash); /** * Append the value under key h to the message. */ $uri->setVar('h', $h); } /** * Decodes the options from a record into an options object. * * @param MfaTable $record The record to decode * * @return array * @since 4.2.0 */ private function decodeRecordOptions(MfaTable $record): array { $options = [ 'id' => '', ]; if (!empty($record->options)) { $recordOptions = $record->options; $options = array_merge($options, $recordOptions); } return $options; } /** * @param MfaTable $record The record to validate against * @param string $code The code given to us by the user * * @return boolean * @throws \Exception * @since 4.2.0 */ private function validateAgainstRecord(MfaTable $record, string $code): bool { // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $keyID = $options['id'] ?? ''; // If there is no key in the options throw an error if (empty($keyID)) { return false; } // If the submitted code is empty throw an error if (empty($code)) { return false; } // If the submitted code length is wrong throw an error if (strlen($code) != 44) { return false; } // If the submitted code's key ID does not match the stored throw an error if (substr($code, 0, 12) != $keyID) { return false; } // Check the OTP code for validity return $this->validateYubikeyOtp($code); } } PKpd�\=�z��yubikey/yubikey.xmlnu�[���<?xml version="1.0" encoding="UTF-8"?> <extension type="plugin" group="multifactorauth" method="upgrade"> <name>plg_multifactorauth_yubikey</name> <author>Joomla! Project</author> <creationDate>2013-09</creationDate> <copyright>(C) 2013 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>3.2.0</version> <description>PLG_MULTIFACTORAUTH_YUBIKEY_XML_DESCRIPTION</description> <namespace path="src">Joomla\Plugin\Multifactorauth\Yubikey</namespace> <files> <folder plugin="yubikey">services</folder> <folder>src</folder> </files> <languages> <language tag="en-GB">language/en-GB/plg_multifactorauth_yubikey.ini</language> <language tag="en-GB">language/en-GB/plg_multifactorauth_yubikey.sys.ini</language> </languages> </extension> PKpd�\m�F7979totp/src/Extension/Totp.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.totp * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Totp\Extension; use Joomla\CMS\Encrypt\Totp as TotpHelper; use Joomla\CMS\Event\MultiFactor\Captive; use Joomla\CMS\Event\MultiFactor\GetMethod; use Joomla\CMS\Event\MultiFactor\GetSetup; use Joomla\CMS\Event\MultiFactor\SaveSetup; use Joomla\CMS\Event\MultiFactor\Validate; use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryAwareTrait; use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; use Joomla\Component\Users\Administrator\Table\MfaTable; use Joomla\Event\SubscriberInterface; use Joomla\Input\Input; use RuntimeException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Multi-factor Authentication using Google Authenticator TOTP Plugin * * @since 3.2 */ class Totp extends CMSPlugin implements SubscriberInterface { use UserFactoryAwareTrait; /** * Affects constructor behavior. If true, language files will be loaded automatically. * * @var boolean * @since 3.2 */ protected $autoloadLanguage = true; /** * The MFA Method name handled by this plugin * * @var string * @since 4.2.0 */ private $mfaMethodName = 'totp'; /** * Should I try to detect and register legacy event listeners, i.e. methods which accept unwrapped arguments? While * this maintains a great degree of backwards compatibility to Joomla! 3.x-style plugins it is much slower. You are * advised to implement your plugins using proper Listeners, methods accepting an AbstractEvent as their sole * parameter, for best performance. Also bear in mind that Joomla! 5.x onwards will only allow proper listeners, * removing support for legacy Listeners. * * @var boolean * @since 4.2.0 * * @deprecated 4.3 will be removed in 6.0 * Implement your plugin methods accepting an AbstractEvent object * Example: * onEventTriggerName(AbstractEvent $event) { * $context = $event->getArgument(...); * } */ protected $allowLegacyListeners = false; /** * Returns an array of events this subscriber will listen to. * * @return array * * @since 4.2.0 */ public static function getSubscribedEvents(): array { return [ 'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod', 'onUserMultifactorCaptive' => 'onUserMultifactorCaptive', 'onUserMultifactorGetSetup' => 'onUserMultifactorGetSetup', 'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup', 'onUserMultifactorValidate' => 'onUserMultifactorValidate', ]; } /** * Gets the identity of this MFA Method * * @param GetMethod $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetMethod(GetMethod $event): void { $event->addResult( new MethodDescriptor( [ 'name' => $this->mfaMethodName, 'display' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), 'shortinfo' => Text::_('PLG_MULTIFACTORAUTH_TOTP_SHORTINFO'), 'image' => 'media/plg_multifactorauth_totp/images/totp.svg', ] ) ); } /** * Returns the information which allows Joomla to render the Captive MFA page. This is the page * which appears right after you log in and asks you to validate your login with MFA. * * @param Captive $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorCaptive(Captive $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method !== $this->mfaMethodName) { return; } $event->addResult( new CaptiveRenderOptions( [ // Custom HTML to display above the MFA form 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_TOTP_CAPTIVE_PROMPT'), // How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML) 'field_type' => 'input', // The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. 'input_type' => 'text', // The attributes for the HTML input box. 'input_attributes' => [ 'pattern' => '[0-9]{6}', 'maxlength' => '6', 'inputmode' => 'numeric', 'required' => 'true', 'autocomplete' => 'one-time-code', 'aria-autocomplete' => 'none', ], // Placeholder text for the HTML input box. Leave empty if you don't need it. 'placeholder' => '', // Label to show above the HTML input box. Leave empty if you don't need it. 'label' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_LABEL'), // Custom HTML. Only used when field_type = custom. 'html' => '', // Custom HTML to display below the MFA form 'post_message' => '', ] ) ); } /** * Returns the information which allows Joomla to render the MFA setup page. This is the page * which allows the user to add or modify a MFA Method for their user account. If the record * does not correspond to your plugin return an empty array. * * @param GetSetup $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetSetup(GetSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method !== $this->mfaMethodName) { return; } $totp = new TotpHelper(); // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $key = $options['key'] ?? ''; $session = $this->getApplication()->getSession(); $isConfigured = !empty($key); // If there's a key in the session use that instead. $sessionKey = $session->get('com_users.totp.key', null); if (!empty($sessionKey)) { $key = $sessionKey; } // If there's still no key in the options, generate one and save it in the session if (empty($key)) { $key = $totp->generateSecret(); $session->set('com_users.totp.key', $key); } // Generate a QR code for the key $user = $this->getUserFactory()->loadUserById($record->user_id); $hostname = Uri::getInstance()->toString(['host']); $otpURL = sprintf("otpauth://totp/%s@%s?secret=%s", $user->username, $hostname, $key); $document = $this->getApplication()->getDocument(); $wam = $document->getWebAssetManager(); $document->addScriptOptions('plg_multifactorauth_totp.totp.qr', $otpURL); $wam->getRegistry()->addExtensionRegistryFile('plg_multifactorauth_totp'); $wam->useScript('plg_multifactorauth_totp.setup'); $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), 'pre_message' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_INSTRUCTIONS'), 'table_heading' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_HEADING'), 'tabular_data' => [ '' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_SUBHEAD'), Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_KEY') => $key, Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_QR') => "<span id=\"users-mfa-totp-qrcode\" />", Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK') => Text::sprintf('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK_TEXT', $otpURL) . '<br/><small>' . Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK_NOTE') . '</small>', ], 'hidden_data' => [ 'key' => $key, ], 'input_type' => $isConfigured ? 'hidden' : 'text', 'input_attributes' => [ 'pattern' => '[0-9]{6}', 'maxlength' => '6', 'inputmode' => 'numeric', 'required' => 'true', 'autocomplete' => 'one-time-code', 'aria-autocomplete' => 'none', ], 'input_value' => '', 'placeholder' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_PLACEHOLDER'), 'label' => Text::_('PLG_MULTIFACTORAUTH_TOTP_LBL_LABEL'), ] ) ); } /** * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The * message of the exception will be displayed to the user. If the record does not correspond to your plugin return * an empty array. * * @param SaveSetup $event The event we are handling * * @return void The configuration data to save to the database * @since 4.2.0 */ public function onUserMultifactorSaveSetup(SaveSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. * @var Input $input The user input you are going to take into account. */ $record = $event['record']; $input = $event['input']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $optionsKey = $options['key'] ?? ''; $key = $optionsKey; $session = $this->getApplication()->getSession(); // If there is no key in the options fetch one from the session if (empty($key)) { $key = $session->get('com_users.totp.key', null); } // If there is still no key in the options throw an error if (empty($key)) { throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); } /** * If the code is empty but the key already existed in $options someone is simply changing the title / default * Method status. We can allow this and stop checking anything else now. */ $code = $input->getInt('code'); if (empty($code) && !empty($optionsKey)) { $event->addResult($options); return; } // In any other case validate the submitted code $totp = new TotpHelper(); $isValid = $totp->checkCode($key, $code); if (!$isValid) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_TOTP_ERR_VALIDATIONFAILED'), 500); } // The code is valid. Unset the key from the session. $session->set('com_users.totp.key', null); // Return the configuration to be serialized $event->addResult( [ 'key' => $key, ] ); } /** * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor * Authentication page. If the record does not correspond to your plugin return FALSE. * * @param Validate $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorValidate(Validate $event): void { /** * @var MfaTable $record The MFA Method's record you're validating against * @var User $user The user record * @var string $code The submitted code */ $record = $event['record']; $user = $event['user']; $code = $event['code']; // Make sure we are actually meant to handle this Method if ($record->method !== $this->mfaMethodName) { $event->addResult(false); return; } // Double check the MFA Method is for the correct user if ($user->id != $record->user_id) { $event->addResult(false); return; } // Load the options from the record (if any) $options = $this->decodeRecordOptions($record); $key = $options['key'] ?? ''; // If there is no key in the options throw an error if (empty($key)) { $event->addResult(false); return; } // Check the MFA code for validity $event->addResult((new TotpHelper())->checkCode($key, $code)); } /** * Decodes the options from a record into an options object. * * @param MfaTable $record The record to decode options for * * @return array * @since 4.2.0 */ private function decodeRecordOptions(MfaTable $record): array { $options = [ 'key' => '', ]; if (!empty($record->options)) { $recordOptions = $record->options; $options = array_merge($options, $recordOptions); } return $options; } } PKpd�\�n~~ totp/totp.xmlnu�[���<?xml version="1.0" encoding="UTF-8"?> <extension type="plugin" group="multifactorauth" method="upgrade"> <name>plg_multifactorauth_totp</name> <author>Joomla! Project</author> <creationDate>2013-08</creationDate> <copyright>(C) 2013 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>3.2.0</version> <description>PLG_MULTIFACTORAUTH_TOTP_XML_DESCRIPTION</description> <namespace path="src">Joomla\Plugin\Multifactorauth\Totp</namespace> <files> <folder plugin="totp">services</folder> <folder>src</folder> </files> <languages> <language tag="en-GB">language/en-GB/plg_multifactorauth_totp.ini</language> <language tag="en-GB">language/en-GB/plg_multifactorauth_totp.sys.ini</language> </languages> </extension> PKpd�\H?D��totp/services/provider.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.totp * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') || die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\User\UserFactoryInterface; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Multifactorauth\Totp\Extension\Totp; return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.2.0 */ public function register(Container $container) { $container->set( PluginInterface::class, function (Container $container) { $config = (array) PluginHelper::getPlugin('multifactorauth', 'totp'); $subject = $container->get(DispatcherInterface::class); $plugin = new Totp($subject, $config); $plugin->setApplication(Factory::getApplication()); $plugin->setUserFactory($container->get(UserFactoryInterface::class)); return $plugin; } ); } }; PKpd�\�d���webauthn/services/provider.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') || die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\User\UserFactoryInterface; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Multifactorauth\Webauthn\Extension\Webauthn; return new class () implements ServiceProviderInterface { /** * Registers the service provider with a DI container. * * @param Container $container The DI container. * * @return void * * @since 4.2.0 */ public function register(Container $container) { $container->set( PluginInterface::class, function (Container $container) { $config = (array) PluginHelper::getPlugin('multifactorauth', 'webauthn'); $subject = $container->get(DispatcherInterface::class); $plugin = new Webauthn($subject, $config); $plugin->setApplication(Factory::getApplication()); $plugin->setUserFactory($container->get(UserFactoryInterface::class)); return $plugin; } ); } }; PKpd�\a���webauthn/tmpl/default.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ // Prevent direct access defined('_JEXEC') || die; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; // This method is only available on HTTPS if (Uri::getInstance()->getScheme() !== 'https') : ?> <div id="multifactorauth-webauthn-nothttps" class="my-2"> <div class="alert alert-danger"> <h2 class="alert-heading"> <span class="icon-cancel-circle" aria-hidden="true"></span> <?php echo Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTHTTPS_HEAD'); ?> </h2> <p> <?php echo Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTHTTPS_BODY'); ?> </p> </div> </div> <?php return; endif; $this->getApplication()->getDocument()->getWebAssetManager()->useScript('plg_multifactorauth_webauthn.webauthn'); ?> <div id="multifactorauth-webauthn-missing" class="my-2"> <div class="alert alert-danger"> <h2 class="alert-heading"> <span class="icon-cancel-circle" aria-hidden="true"></span> <?php echo Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_HEAD'); ?> </h2> <p> <?php echo Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_BODY'); ?> </p> </div> </div> PKpd�\6�28C8C#webauthn/src/Extension/Webauthn.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Webauthn\Extension; use Exception; use Joomla\CMS\Event\MultiFactor\Captive; use Joomla\CMS\Event\MultiFactor\GetMethod; use Joomla\CMS\Event\MultiFactor\GetSetup; use Joomla\CMS\Event\MultiFactor\SaveSetup; use Joomla\CMS\Event\MultiFactor\Validate; use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryAwareTrait; use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; use Joomla\Component\Users\Administrator\Table\MfaTable; use Joomla\Event\SubscriberInterface; use Joomla\Input\Input; use Joomla\Plugin\Multifactorauth\Webauthn\Helper\Credentials; use RuntimeException; use Webauthn\PublicKeyCredentialRequestOptions; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla Multi-factor Authentication plugin for WebAuthn * * @since 4.2.0 */ class Webauthn extends CMSPlugin implements SubscriberInterface { use UserFactoryAwareTrait; /** * Auto-load the plugin's language files * * @var boolean * @since 4.2.0 */ protected $autoloadLanguage = true; /** * The MFA Method name handled by this plugin * * @var string * @since 4.2.0 */ private $mfaMethodName = 'webauthn'; /** * Returns an array of events this subscriber will listen to. * * @return array * * @since 4.2.0 */ public static function getSubscribedEvents(): array { return [ 'onUserMultifactorGetMethod' => 'onUserMultifactorGetMethod', 'onUserMultifactorCaptive' => 'onUserMultifactorCaptive', 'onUserMultifactorGetSetup' => 'onUserMultifactorGetSetup', 'onUserMultifactorSaveSetup' => 'onUserMultifactorSaveSetup', 'onUserMultifactorValidate' => 'onUserMultifactorValidate', ]; } /** * Gets the identity of this MFA Method * * @param GetMethod $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorGetMethod(GetMethod $event): void { $event->addResult( new MethodDescriptor( [ 'name' => $this->mfaMethodName, 'display' => Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_DISPLAYEDAS'), 'shortinfo' => Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_SHORTINFO'), 'image' => 'media/plg_multifactorauth_webauthn/images/webauthn.svg', 'allowMultiple' => true, 'allowEntryBatching' => true, ] ) ); } /** * Returns the information which allows Joomla to render the MFA setup page. This is the page * which allows the user to add or modify a MFA Method for their user account. If the record * does not correspond to your plugin return an empty array. * * @param GetSetup $event The event we are handling * * @return void * @throws \Exception * @since 4.2.0 */ public function onUserMultifactorGetSetup(GetSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Get some values assuming that we are NOT setting up U2F (the key is already registered) $submitClass = ''; $submitIcon = 'icon icon-ok'; $submitText = 'JSAVE'; $preMessage = Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_CONFIGURED'); $type = 'input'; $html = ''; $hiddenData = []; /** * If there are no authenticators set up yet I need to show a different message and take a different action when * my user clicks the submit button. */ if (!is_array($record->options) || empty($record->options['credentialId'] ?? '')) { $document = $this->getApplication()->getDocument(); $wam = $document->getWebAssetManager(); $wam->getRegistry()->addExtensionRegistryFile('plg_multifactorauth_webauthn'); $layoutPath = PluginHelper::getLayoutPath('multifactorauth', 'webauthn'); ob_start(); include $layoutPath; $html = ob_get_clean(); $type = 'custom'; // Load JS translations Text::script('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_HEAD'); $document->addScriptOptions('com_users.pagetype', 'setup', false); // Save the WebAuthn request to the session $user = $this->getApplication()->getIdentity() ?: $this->getUserFactory()->loadUserById(0); $hiddenData['pkRequest'] = base64_encode(Credentials::requestAttestation($user)); // Special button handling $submitClass = "multifactorauth_webauthn_setup"; $submitIcon = 'icon icon-lock'; $submitText = 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_REGISTERKEY'; // Message to display $preMessage = Text::sprintf( 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_INSTRUCTIONS', Text::_($submitText) ); } $event->addResult( new SetupRenderOptions( [ 'default_title' => Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_DISPLAYEDAS'), 'pre_message' => $preMessage, 'hidden_data' => $hiddenData, 'field_type' => $type, 'input_type' => 'hidden', 'html' => $html, 'show_submit' => true, 'submit_class' => $submitClass, 'submit_icon' => $submitIcon, 'submit_text' => $submitText, ] ) ); } /** * Parse the input from the MFA setup page and return the configuration information to be saved to the database. If * the information is invalid throw a RuntimeException to signal the need to display the editor page again. The * message of the exception will be displayed to the user. If the record does not correspond to your plugin return * an empty array. * * @param SaveSetup $event The event we are handling * * @return void The configuration data to save to the database * @since 4.2.0 */ public function onUserMultifactorSaveSetup(SaveSetup $event): void { /** * @var MfaTable $record The record currently selected by the user. * @var Input $input The user input you are going to take into account. */ $record = $event['record']; $input = $event['input']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } // Editing an existing authenticator: only the title is saved if (is_array($record->options) && !empty($record->options['credentialId'] ?? '')) { $event->addResult($record->options); return; } $code = $input->get('code', null, 'base64'); $session = $this->getApplication()->getSession(); $registrationRequest = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null); // If there was no registration request BUT there is a registration response throw an error if (empty($registrationRequest) && !empty($code)) { throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); } // If there is no registration request (and there isn't a registration response) we are just saving the title. if (empty($registrationRequest)) { $event->addResult($record->options); return; } // In any other case try to authorize the registration try { $publicKeyCredentialSource = Credentials::verifyAttestation($code); } catch (\Exception $err) { throw new \RuntimeException($err->getMessage(), 403); } finally { // Unset the request data from the session. $session->set('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null); $session->set('plg_multifactorauth_webauthn.registration_user_id', null); } // Return the configuration to be serialized $event->addResult( [ 'credentialId' => base64_encode($publicKeyCredentialSource->getAttestedCredentialData()->getCredentialId()), 'pubkeysource' => json_encode($publicKeyCredentialSource), 'counter' => 0, ] ); } /** * Returns the information which allows Joomla to render the Captive MFA page. This is the page * which appears right after you log in and asks you to validate your login with MFA. * * @param Captive $event The event we are handling * * @return void * @throws \Exception * @since 4.2.0 */ public function onUserMultifactorCaptive(Captive $event): void { /** * @var MfaTable $record The record currently selected by the user. */ $record = $event['record']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { return; } /** * The following code looks stupid. An explanation is in order. * * What we normally want to do is save the authentication data returned by getAuthenticateData into the session. * This is what is sent to the authenticator through the Javascript API and signed. The signature is posted back * to the form as the "code" which is read by onUserMultifactorauthValidate. That Method will read the authentication * data from the session and pass it along with the key registration data (from the database) and the * authentication response (the "code" submitted in the form) to the WebAuthn library for validation. * * Validation will work as long as the challenge recorded in the encrypted AUTHENTICATION RESPONSE matches, upon * decryption, the challenge recorded in the AUTHENTICATION DATA. * * I observed that for whatever stupid reason the browser was sometimes sending TWO requests to the server's * Captive login page but only rendered the FIRST. This meant that the authentication data sent to the key had * already been overwritten in the session by the "invisible" second request. As a result the challenge would * not match and we'd get a validation error. * * The code below will attempt to read the authentication data from the session first. If it exists it will NOT * try to replace it (technically it replaces it with a copy of the same data - same difference!). If nothing * exists in the session, however, it WILL store the (random seeded) result of the getAuthenticateData Method. * Therefore the first request to the Captive login page will store a new set of authentication data whereas the * second, "invisible", request will just reuse the same data as the first request, fixing the observed issue in * a way that doesn't compromise security. * * In case you are wondering, yes, the data is removed from the session in the onUserMultifactorauthValidate Method. * In fact it's the first thing we do after reading it, preventing constant reuse of the same set of challenges. * * That was fun to debug - for "poke your eyes with a rusty fork" values of fun. */ $session = $this->getApplication()->getSession(); $pkOptionsEncoded = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null); $force = $this->getApplication()->getInput()->getInt('force', 0); try { if ($force) { throw new \RuntimeException('Expected exception (good): force a new key request'); } if (empty($pkOptionsEncoded)) { throw new \RuntimeException('Expected exception (good): we do not have a pending key request'); } $serializedOptions = base64_decode($pkOptionsEncoded); $pkOptions = unserialize($serializedOptions); if (!is_object($pkOptions) || empty($pkOptions) || !($pkOptions instanceof PublicKeyCredentialRequestOptions)) { throw new \RuntimeException('The pending key request is corrupt; a new one will be created'); } $pkRequest = json_encode($pkOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } catch (\Exception $e) { $pkRequest = Credentials::requestAssertion($record->user_id); } $document = $this->getApplication()->getDocument(); $wam = $document->getWebAssetManager(); $wam->getRegistry()->addExtensionRegistryFile('plg_multifactorauth_webauthn'); try { $document->addScriptOptions('com_users.authData', base64_encode($pkRequest), false); $layoutPath = PluginHelper::getLayoutPath('multifactorauth', 'webauthn'); ob_start(); include $layoutPath; $html = ob_get_clean(); } catch (\Exception $e) { return; } // Load JS translations Text::script('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_HEAD'); Text::script('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NO_STORED_CREDENTIAL'); $document->addScriptOptions('com_users.pagetype', 'validate', false); $event->addResult( new CaptiveRenderOptions( [ 'pre_message' => Text::sprintf( 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_INSTRUCTIONS', Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_VALIDATEKEY') ), 'field_type' => 'custom', 'input_type' => 'hidden', 'placeholder' => '', 'label' => '', 'html' => $html, 'post_message' => '', 'hide_submit' => false, 'submit_icon' => 'icon icon-lock', 'submit_text' => 'PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_VALIDATEKEY', 'allowEntryBatching' => true, ] ) ); } /** * Validates the Multi-factor Authentication code submitted by the user in the Multi-Factor * Authentication page. If the record does not correspond to your plugin return FALSE. * * @param Validate $event The event we are handling * * @return void * @since 4.2.0 */ public function onUserMultifactorValidate(Validate $event): void { // This method is only available on HTTPS if (Uri::getInstance()->getScheme() !== 'https') { $event->addResult(false); return; } /** * @var MfaTable $record The MFA Method's record you're validating against * @var User $user The user record * @var string $code The submitted code */ $record = $event['record']; $user = $event['user']; $code = $event['code']; // Make sure we are actually meant to handle this Method if ($record->method != $this->mfaMethodName) { $event->addResult(false); return; } // Double check the MFA Method is for the correct user if ($user->id != $record->user_id) { $event->addResult(false); return; } try { Credentials::verifyAssertion($code); } catch (\Exception $e) { try { $this->getApplication()->enqueueMessage($e->getMessage(), 'error'); } catch (\Exception $e) { } $event->addResult(false); return; } $event->addResult(true); } } PKpd�\����#�#:webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt * @copyright (C) 2014-2019 Spomky-Labs * @license This software may be modified and distributed under the terms * of the MIT license. * See libraries/vendor/web-auth/webauthn-lib/LICENSE */ namespace Joomla\Plugin\Multifactorauth\Webauthn\Hotfix; use Assert\Assertion; use CBOR\Decoder; use CBOR\MapObject; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Key\Ec2Key; use Webauthn\AttestationStatement\AttestationStatement; use Webauthn\AttestationStatement\AttestationStatementSupport; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\StringStream; use Webauthn\TrustPath\CertificateTrustPath; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * We had to fork the key attestation support object from the WebAuthn server package to address an * issue with PHP 8. * * We are currently using an older version of the WebAuthn library (2.x) which was written before * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of * Joomla's Semantic Versioning promise. * * The FidoU2FAttestationStatementSupport class forces an assertion on the result of the * openssl_pkey_get_public() function, assuming it will return a resource. However, starting with * PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a * result, you cannot use Android or FIDO U2F keys with WebAuthn. * * The assertion check is in a private method, therefore we have to fork both attestation support * class to change the assertion. The assertion takes place through a third party library we cannot * (and should not!) modify. * * @since 4.2.0 * * @deprecated 4.2 will be removed in 6.0 * Will be removed without replacement * We will upgrade the WebAuthn library to version 3 or later and this will go away. */ final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport { /** * @var Decoder * @since 4.2.0 */ private $decoder; /** * @var MetadataStatementRepository|null * @since 4.2.0 */ private $metadataStatementRepository; /** * @param Decoder|null $decoder Obvious * @param MetadataStatementRepository|null $metadataStatementRepository Obvious * * @since 4.2.0 */ public function __construct( ?Decoder $decoder = null, ?MetadataStatementRepository $metadataStatementRepository = null ) { if ($decoder !== null) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } if ($metadataStatementRepository === null) { @trigger_error( 'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', E_USER_DEPRECATED ); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->metadataStatementRepository = $metadataStatementRepository; } /** * @return string * @since 4.2.0 */ public function name(): string { return 'fido-u2f'; } /** * @param array $attestation Obvious * * @return AttestationStatement * @throws \Assert\AssertionFailedException * * @since 4.2.0 */ public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); foreach (['sig', 'x5c'] as $key) { Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); } $certificates = $attestation['attStmt']['x5c']; Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.'); Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.'); Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with one certificate.'); reset($certificates); $certificates = CertificateToolbox::convertAllDERToPEM($certificates); $this->checkCertificate($certificates[0]); return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); } /** * @param string $clientDataJSONHash Obvious * @param AttestationStatement $attestationStatement Obvious * @param AuthenticatorData $authenticatorData Obvious * * @return boolean * @throws \Assert\AssertionFailedException * @since 4.2.0 */ public function isValid( string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData ): bool { Assertion::eq( $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), '00000000-0000-0000-0000-000000000000', 'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"' ); if ($this->metadataStatementRepository !== null) { CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), [], $this->metadataStatementRepository ); } $trustPath = $attestationStatement->getTrustPath(); Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); $dataToVerify = "\0"; $dataToVerify .= $authenticatorData->getRpIdHash(); $dataToVerify .= $clientDataJSONHash; $dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId(); $dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()); return openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256) === 1; } /** * @param string|null $publicKey Obvious * * @return string * @throws \Assert\AssertionFailedException * @since 4.2.0 */ private function extractPublicKey(?string $publicKey): string { Assertion::notNull($publicKey, 'The attested credential data does not contain a valid public key.'); $publicKeyStream = new StringStream($publicKey); $coseKey = $this->decoder->decode($publicKeyStream); Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.'); $publicKeyStream->close(); Assertion::isInstanceOf($coseKey, MapObject::class, 'The attested credential data does not contain a valid public key.'); $coseKey = $coseKey->getNormalizedData(); $ec2Key = new Ec2Key($coseKey + [Ec2Key::TYPE => 2, Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256]); return "\x04" . $ec2Key->x() . $ec2Key->y(); } /** * @param string $publicKey Obvious * * @return void * @throws \Assert\AssertionFailedException * @since 4.2.0 */ private function checkCertificate(string $publicKey): void { try { $resource = openssl_pkey_get_public($publicKey); if (version_compare(PHP_VERSION, '8.0', 'lt')) { Assertion::isResource($resource, 'Unable to read the certificate'); } else { /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate'); } } catch (\Throwable $throwable) { throw new \InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable); } $details = openssl_pkey_get_details($resource); Assertion::keyExists($details, 'ec', 'Invalid certificate or certificate chain'); Assertion::keyExists($details['ec'], 'curve_name', 'Invalid certificate or certificate chain'); Assertion::eq($details['ec']['curve_name'], 'prime256v1', 'Invalid certificate or certificate chain'); Assertion::keyExists($details['ec'], 'curve_oid', 'Invalid certificate or certificate chain'); Assertion::eq($details['ec']['curve_oid'], '1.2.840.10045.3.1.7', 'Invalid certificate or certificate chain'); } } PKpd�\N���,�,=webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt * @copyright (C) 2014-2019 Spomky-Labs * @license This software may be modified and distributed under the terms * of the MIT license. * See libraries/vendor/web-auth/webauthn-lib/LICENSE */ namespace Joomla\Plugin\Multifactorauth\Webauthn\Hotfix; use Assert\Assertion; use CBOR\Decoder; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Algorithms; use Cose\Key\Ec2Key; use Cose\Key\Key; use Cose\Key\RsaKey; use FG\ASN1\ASNObject; use FG\ASN1\ExplicitlyTaggedObject; use FG\ASN1\Universal\OctetString; use FG\ASN1\Universal\Sequence; use Webauthn\AttestationStatement\AttestationStatement; use Webauthn\AttestationStatement\AttestationStatementSupport; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\StringStream; use Webauthn\TrustPath\CertificateTrustPath; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * We had to fork the key attestation support object from the WebAuthn server package to address an * issue with PHP 8. * * We are currently using an older version of the WebAuthn library (2.x) which was written before * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of * Joomla's Semantic Versioning promise. * * The AndroidKeyAttestationStatementSupport class forces an assertion on the result of the * openssl_pkey_get_public() function, assuming it will return a resource. However, starting with * PHP 8.0 this function returns an OpenSSLAsymmetricKey object and the assertion fails. As a * result, you cannot use Android or FIDO U2F keys with WebAuthn. * * The assertion check is in a private method, therefore we have to fork both attestation support * class to change the assertion. The assertion takes place through a third party library we cannot * (and should not!) modify. * * @since 4.2.0 * * @deprecated 4.2 will be removed in 6.0 * Will be removed without replacement * We will upgrade the WebAuthn library to version 3 or later and this will go away. */ final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport { /** * @var Decoder * @since 4.2.0 */ private $decoder; /** * @var MetadataStatementRepository|null * @since 4.2.0 */ private $metadataStatementRepository; /** * @param Decoder|null $decoder Obvious * @param MetadataStatementRepository|null $metadataStatementRepository Obvious * * @since 4.2.0 */ public function __construct( ?Decoder $decoder = null, ?MetadataStatementRepository $metadataStatementRepository = null ) { if ($decoder !== null) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } if ($metadataStatementRepository === null) { @trigger_error( 'Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', E_USER_DEPRECATED ); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->metadataStatementRepository = $metadataStatementRepository; } /** * @return string * @since 4.2.0 */ public function name(): string { return 'android-key'; } /** * @param array $attestation Obvious * * @return AttestationStatement * @throws \Assert\AssertionFailedException * @since 4.2.0 */ public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); foreach (['sig', 'x5c', 'alg'] as $key) { Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); } $certificates = $attestation['attStmt']['x5c']; Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); Assertion::greaterThan(\count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.'); Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); $certificates = CertificateToolbox::convertAllDERToPEM($certificates); return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); } /** * @param string $clientDataJSONHash Obvious * @param AttestationStatement $attestationStatement Obvious * @param AuthenticatorData $authenticatorData Obvious * * @return boolean * @throws \Assert\AssertionFailedException * @since 4.2.0 */ public function isValid( string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData ): bool { $trustPath = $attestationStatement->getTrustPath(); Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); $certificates = $trustPath->getCertificates(); if ($this->metadataStatementRepository !== null) { $certificates = CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), $certificates, $this->metadataStatementRepository ); } // Decode leaf attestation certificate $leaf = $certificates[0]; $this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData); $signedData = $authenticatorData->getAuthData() . $clientDataJSONHash; $alg = $attestationStatement->get('alg'); return openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg)) === 1; } /** * @param string $certificate Obvious * @param string $clientDataHash Obvious * @param AuthenticatorData $authenticatorData Obvious * * @return void * @throws \Assert\AssertionFailedException * @throws \FG\ASN1\Exception\ParserException * @since 4.2.0 */ private function checkCertificateAndGetPublicKey( string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData ): void { $resource = openssl_pkey_get_public($certificate); if (version_compare(PHP_VERSION, '8.0', 'lt')) { Assertion::isResource($resource, 'Unable to read the certificate'); } else { /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */ Assertion::isInstanceOf($resource, \OpenSSLAsymmetricKey::class, 'Unable to read the certificate'); } $details = openssl_pkey_get_details($resource); Assertion::isArray($details, 'Unable to read the certificate'); // Check that authData publicKey matches the public key in the attestation certificate $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); Assertion::notNull($attestedCredentialData, 'No attested credential data found'); $publicKeyData = $attestedCredentialData->getCredentialPublicKey(); Assertion::notNull($publicKeyData, 'No attested public key found'); $publicDataStream = new StringStream($publicKeyData); $coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false); Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.'); $publicDataStream->close(); $publicKey = Key::createFromData($coseKey); Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type'); Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key'); $certDetails = openssl_x509_parse($certificate); // Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension'); Assertion::isArray($certDetails['extensions'], 'The certificate has no extension'); Assertion::keyExists( $certDetails['extensions'], '1.3.6.1.4.1.11129.2.1.17', 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing' ); $extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17']; $extensionAsAsn1 = ASNObject::fromBinary($extension); Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $objects = $extensionAsAsn1->getChildren(); // Check that attestationChallenge is set to the clientDataHash. Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid'); // Check that both teeEnforced and softwareEnforced structures don’t contain allApplications(600) tag. Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $softwareEnforcedFlags = $objects[6]; Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags); Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $teeEnforcedFlags = $objects[6]; Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags); } /** * @param Sequence $sequence Obvious * * @return void * @throws \Assert\AssertionFailedException * @since 4.2.0 */ private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void { foreach ($sequence->getChildren() as $tag) { Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag'); /** * @var ExplicitlyTaggedObject $tag It is silly that I have to do that for PHPCS to be happy. */ Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found'); } } } PKpd�\3�UY(I(Iwebauthn/src/Hotfix/Server.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt * @copyright (C) 2014-2019 Spomky-Labs * @license This software may be modified and distributed under the terms * of the MIT license. * See libraries/vendor/web-auth/webauthn-lib/LICENSE */ namespace Joomla\Plugin\Multifactorauth\Webauthn\Hotfix; use Assert\Assertion; use Cose\Algorithm\Algorithm; use Cose\Algorithm\ManagerFactory; use Cose\Algorithm\Signature\ECDSA; use Cose\Algorithm\Signature\EdDSA; use Cose\Algorithm\Signature\RSA; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport; use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AttestationStatement\NoneAttestationStatementSupport; use Webauthn\AttestationStatement\PackedAttestationStatementSupport; use Webauthn\AttestationStatement\TPMAttestationStatementSupport; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\AuthenticatorAssertionResponse; use Webauthn\AuthenticatorAssertionResponseValidator; use Webauthn\AuthenticatorAttestationResponse; use Webauthn\AuthenticatorAttestationResponseValidator; use Webauthn\AuthenticatorSelectionCriteria; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialLoader; use Webauthn\PublicKeyCredentialParameters; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialSourceRepository; use Webauthn\PublicKeyCredentialUserEntity; use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Customised WebAuthn server object. * * We had to fork the server object from the WebAuthn server package to address an issue with PHP 8. * * We are currently using an older version of the WebAuthn library (2.x) which was written before * PHP 8 was developed. We cannot upgrade the WebAuthn library to a newer major version because of * Joomla's Semantic Versioning promise. * * The FidoU2FAttestationStatementSupport and AndroidKeyAttestationStatementSupport classes force * an assertion on the result of the openssl_pkey_get_public() function, assuming it will return a * resource. However, starting with PHP 8.0 this function returns an OpenSSLAsymmetricKey object * and the assertion fails. As a result, you cannot use Android or FIDO U2F keys with WebAuthn. * * The assertion check is in a private method, therefore we have to fork both attestation support * classes to change the assertion. The assertion takes place through a third party library we * cannot (and should not!) modify. * * The assertions objects, however, are injected to the attestation support manager in a private * method of the Server object. Because literally everything in this class is private we have no * option than to fork the entire class to apply our two forked attestation support classes. * * This is marked as deprecated because we'll be able to upgrade the WebAuthn library on Joomla 5. * * @since 4.2.0 * * @deprecated 4.2 will be removed in 6.0 * Will be removed without replacement * We will upgrade the WebAuthn library to version 3 or later and this will go away. */ class Server extends \Webauthn\Server { /** * @var integer * @since 4.2.0 */ public $timeout = 60000; /** * @var integer * @since 4.2.0 */ public $challengeSize = 32; /** * @var PublicKeyCredentialRpEntity * @since 4.2.0 */ private $rpEntity; /** * @var ManagerFactory * @since 4.2.0 */ private $coseAlgorithmManagerFactory; /** * @var PublicKeyCredentialSourceRepository * @since 4.2.0 */ private $publicKeyCredentialSourceRepository; /** * @var TokenBindingNotSupportedHandler * @since 4.2.0 */ private $tokenBindingHandler; /** * @var ExtensionOutputCheckerHandler * @since 4.2.0 */ private $extensionOutputCheckerHandler; /** * @var string[] * @since 4.2.0 */ private $selectedAlgorithms; /** * @var MetadataStatementRepository|null * @since 4.2.0 */ private $metadataStatementRepository; /** * @var ClientInterface * @since 4.2.0 */ private $httpClient; /** * @var string * @since 4.2.0 */ private $googleApiKey; /** * @var RequestFactoryInterface * @since 4.2.0 */ private $requestFactory; /** * Overridden constructor. * * @param PublicKeyCredentialRpEntity $relayingParty Obvious * @param PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository Obvious * @param MetadataStatementRepository|null $metadataStatementRepository Obvious * * @since 4.2.0 */ public function __construct( PublicKeyCredentialRpEntity $relayingParty, PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, ?MetadataStatementRepository $metadataStatementRepository ) { $this->rpEntity = $relayingParty; $this->coseAlgorithmManagerFactory = new ManagerFactory(); $this->coseAlgorithmManagerFactory->add('RS1', new RSA\RS1()); $this->coseAlgorithmManagerFactory->add('RS256', new RSA\RS256()); $this->coseAlgorithmManagerFactory->add('RS384', new RSA\RS384()); $this->coseAlgorithmManagerFactory->add('RS512', new RSA\RS512()); $this->coseAlgorithmManagerFactory->add('PS256', new RSA\PS256()); $this->coseAlgorithmManagerFactory->add('PS384', new RSA\PS384()); $this->coseAlgorithmManagerFactory->add('PS512', new RSA\PS512()); $this->coseAlgorithmManagerFactory->add('ES256', new ECDSA\ES256()); $this->coseAlgorithmManagerFactory->add('ES256K', new ECDSA\ES256K()); $this->coseAlgorithmManagerFactory->add('ES384', new ECDSA\ES384()); $this->coseAlgorithmManagerFactory->add('ES512', new ECDSA\ES512()); $this->coseAlgorithmManagerFactory->add('Ed25519', new EdDSA\Ed25519()); $this->selectedAlgorithms = ['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512', 'Ed25519']; $this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository; $this->tokenBindingHandler = new TokenBindingNotSupportedHandler(); $this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler(); $this->metadataStatementRepository = $metadataStatementRepository; } /** * @param string[] $selectedAlgorithms Obvious * * @return void * @since 4.2.0 */ public function setSelectedAlgorithms(array $selectedAlgorithms): void { $this->selectedAlgorithms = $selectedAlgorithms; } /** * @param TokenBindingNotSupportedHandler $tokenBindingHandler Obvious * * @return void * @since 4.2.0 */ public function setTokenBindingHandler(TokenBindingNotSupportedHandler $tokenBindingHandler): void { $this->tokenBindingHandler = $tokenBindingHandler; } /** * @param string $alias Obvious * @param Algorithm $algorithm Obvious * * @return void * @since 4.2.0 */ public function addAlgorithm(string $alias, Algorithm $algorithm): void { $this->coseAlgorithmManagerFactory->add($alias, $algorithm); $this->selectedAlgorithms[] = $alias; $this->selectedAlgorithms = array_unique($this->selectedAlgorithms); } /** * @param ExtensionOutputCheckerHandler $extensionOutputCheckerHandler Obvious * * @return void * @since 4.2.0 */ public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void { $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; } /** * @param string|null $userVerification Obvious * @param PublicKeyCredentialDescriptor[] $allowedPublicKeyDescriptors Obvious * @param AuthenticationExtensionsClientInputs|null $extensions Obvious * * @return PublicKeyCredentialRequestOptions * @throws \Exception * @since 4.2.0 */ public function generatePublicKeyCredentialRequestOptions( ?string $userVerification = PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, array $allowedPublicKeyDescriptors = [], ?AuthenticationExtensionsClientInputs $extensions = null ): PublicKeyCredentialRequestOptions { return new PublicKeyCredentialRequestOptions( random_bytes($this->challengeSize), $this->timeout, $this->rpEntity->getId(), $allowedPublicKeyDescriptors, $userVerification, $extensions ?? new AuthenticationExtensionsClientInputs() ); } /** * @param PublicKeyCredentialUserEntity $userEntity Obvious * @param string|null $attestationMode Obvious * @param PublicKeyCredentialDescriptor[] $excludedPublicKeyDescriptors Obvious * @param AuthenticatorSelectionCriteria|null $criteria Obvious * @param AuthenticationExtensionsClientInputs|null $extensions Obvious * * @return PublicKeyCredentialCreationOptions * @throws \Exception * @since 4.2.0 */ public function generatePublicKeyCredentialCreationOptions( PublicKeyCredentialUserEntity $userEntity, ?string $attestationMode = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, array $excludedPublicKeyDescriptors = [], ?AuthenticatorSelectionCriteria $criteria = null, ?AuthenticationExtensionsClientInputs $extensions = null ): PublicKeyCredentialCreationOptions { $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms); $publicKeyCredentialParametersList = []; foreach ($coseAlgorithmManager->all() as $algorithm) { $publicKeyCredentialParametersList[] = new PublicKeyCredentialParameters( PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $algorithm::identifier() ); } $criteria = $criteria ?? new AuthenticatorSelectionCriteria(); $extensions = $extensions ?? new AuthenticationExtensionsClientInputs(); $challenge = random_bytes($this->challengeSize); return new PublicKeyCredentialCreationOptions( $this->rpEntity, $userEntity, $challenge, $publicKeyCredentialParametersList, $this->timeout, $excludedPublicKeyDescriptors, $criteria, $attestationMode, $extensions ); } /** * @param string $data Obvious * @param PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions Obvious * @param ServerRequestInterface $serverRequest Obvious * * @return PublicKeyCredentialSource * @throws \Assert\AssertionFailedException * @since 4.2.0 */ public function loadAndCheckAttestationResponse( string $data, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $serverRequest ): PublicKeyCredentialSource { $attestationStatementSupportManager = $this->getAttestationStatementSupportManager(); $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager); $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader); $publicKeyCredential = $publicKeyCredentialLoader->load($data); $authenticatorResponse = $publicKeyCredential->getResponse(); Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAttestationResponse::class, 'Not an authenticator attestation response'); $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator( $attestationStatementSupportManager, $this->publicKeyCredentialSourceRepository, $this->tokenBindingHandler, $this->extensionOutputCheckerHandler ); return $authenticatorAttestationResponseValidator->check($authenticatorResponse, $publicKeyCredentialCreationOptions, $serverRequest); } /** * @param string $data Obvious * @param PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions Obvious * @param PublicKeyCredentialUserEntity|null $userEntity Obvious * @param ServerRequestInterface $serverRequest Obvious * * @return PublicKeyCredentialSource * @throws \Assert\AssertionFailedException * @since 4.2.0 */ public function loadAndCheckAssertionResponse( string $data, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ?PublicKeyCredentialUserEntity $userEntity, ServerRequestInterface $serverRequest ): PublicKeyCredentialSource { $attestationStatementSupportManager = $this->getAttestationStatementSupportManager(); $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager); $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader); $publicKeyCredential = $publicKeyCredentialLoader->load($data); $authenticatorResponse = $publicKeyCredential->getResponse(); Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAssertionResponse::class, 'Not an authenticator assertion response'); $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator( $this->publicKeyCredentialSourceRepository, null, $this->tokenBindingHandler, $this->extensionOutputCheckerHandler, $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms) ); return $authenticatorAssertionResponseValidator->check( $publicKeyCredential->getRawId(), $authenticatorResponse, $publicKeyCredentialRequestOptions, $serverRequest, null !== $userEntity ? $userEntity->getId() : null ); } /** * @param ClientInterface $client Obvious * @param string $apiKey Obvious * @param RequestFactoryInterface $requestFactory Obvious * * @return void * @since 4.2.0 */ public function enforceAndroidSafetyNetVerification( ClientInterface $client, string $apiKey, RequestFactoryInterface $requestFactory ): void { $this->httpClient = $client; $this->googleApiKey = $apiKey; $this->requestFactory = $requestFactory; } /** * @return AttestationStatementSupportManager * @since 4.2.0 */ private function getAttestationStatementSupportManager(): AttestationStatementSupportManager { $attestationStatementSupportManager = new AttestationStatementSupportManager(); $attestationStatementSupportManager->add(new NoneAttestationStatementSupport()); if ($this->metadataStatementRepository !== null) { $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms); $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport(null, $this->metadataStatementRepository)); /** * Work around a third party library (web-token/jwt-signature-algorithm-eddsa) bug. * * On PHP 8 libsodium is compiled into PHP, it is not an extension. However, the third party library does * not check if the libsodium function are available; it checks if the "sodium" extension is loaded. This of * course causes an immediate failure with a Runtime exception EVEN IF the attested data isn't attested by * Android Safety Net. Therefore we have to not even load the AndroidSafetyNetAttestationStatementSupport * class in this case... */ if (function_exists('sodium_crypto_sign_seed_keypair') && function_exists('extension_loaded') && extension_loaded('sodium')) { $attestationStatementSupportManager->add( new AndroidSafetyNetAttestationStatementSupport( $this->httpClient, $this->googleApiKey, $this->requestFactory, 2000, 60000, $this->metadataStatementRepository ) ); } $attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport(null, $this->metadataStatementRepository)); $attestationStatementSupportManager->add(new TPMAttestationStatementSupport($this->metadataStatementRepository)); $attestationStatementSupportManager->add( new PackedAttestationStatementSupport( null, $coseAlgorithmManager, $this->metadataStatementRepository ) ); } return $attestationStatementSupportManager; } } PKpd�\\5353#webauthn/src/Helper/Credentials.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Webauthn\Helper; use Exception; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\CMS\User\UserFactoryInterface; use Joomla\Plugin\Multifactorauth\Webauthn\CredentialRepository; use Joomla\Plugin\Multifactorauth\Webauthn\Hotfix\Server; use Joomla\Session\SessionInterface; use Laminas\Diactoros\ServerRequestFactory; use Webauthn\AttestedCredentialData; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; use Webauthn\AuthenticatorSelectionCriteria; use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialRpEntity; use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialUserEntity; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Helper class to aid in credentials creation (link an authenticator to a user account) * * @since 4.2.0 */ abstract class Credentials { /** * Authenticator registration step 1: create a public key for credentials attestation. * * The result is a JSON string which can be used in Javascript code with navigator.credentials.create(). * * @param User $user The Joomla user to create the public key for * * @return string * @throws \Exception On error * @since 4.2.0 */ public static function requestAttestation(User $user): string { $publicKeyCredentialCreationOptions = self::getWebauthnServer($user->id) ->generatePublicKeyCredentialCreationOptions( self::getUserEntity($user), PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, self::getPubKeyDescriptorsForUser($user), new AuthenticatorSelectionCriteria( AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, false, AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED ), new AuthenticationExtensionsClientInputs() ); // Save data in the session $session = Factory::getApplication()->getSession(); $session->set( 'plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', base64_encode(serialize($publicKeyCredentialCreationOptions)) ); $session->set('plg_multifactorauth_webauthn.registration_user_id', $user->id); return json_encode($publicKeyCredentialCreationOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } /** * Authenticator registration step 2: verify the credentials attestation by the authenticator * * This returns the attested credential data on success. * * An exception will be returned on error. Also, under very rare conditions, you may receive NULL instead of * attested credential data which means that something was off in the returned data from the browser. * * @param string $data The JSON-encoded data returned by the browser during the authentication flow * * @return AttestedCredentialData|null * @throws \Exception When something does not check out * @since 4.2.0 */ public static function verifyAttestation(string $data): ?PublicKeyCredentialSource { $session = Factory::getApplication()->getSession(); // Retrieve the PublicKeyCredentialCreationOptions object created earlier and perform sanity checks $encodedOptions = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialCreationOptions', null); if (empty($encodedOptions)) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK')); } try { $publicKeyCredentialCreationOptions = unserialize(base64_decode($encodedOptions)); } catch (\Exception $e) { $publicKeyCredentialCreationOptions = null; } if (!is_object($publicKeyCredentialCreationOptions) || !($publicKeyCredentialCreationOptions instanceof PublicKeyCredentialCreationOptions)) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK')); } // Retrieve the stored user ID and make sure it's the same one in the request. $storedUserId = $session->get('plg_multifactorauth_webauthn.registration_user_id', 0); $myUser = Factory::getApplication()->getIdentity() ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); $myUserId = $myUser->id; if (($myUser->guest) || ($myUserId != $storedUserId)) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_USER')); } return self::getWebauthnServer($myUser->id)->loadAndCheckAttestationResponse( base64_decode($data), $publicKeyCredentialCreationOptions, ServerRequestFactory::fromGlobals() ); } /** * Authentication step 1: create a challenge for key verification * * @param int $userId The user ID to create a WebAuthn PK for * * @return string * @throws \Exception On error * @since 4.2.0 */ public static function requestAssertion(int $userId): string { $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); $publicKeyCredentialRequestOptions = self::getWebauthnServer($userId) ->generatePublicKeyCredentialRequestOptions( PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, self::getPubKeyDescriptorsForUser($user) ); // Save in session. This is used during the verification stage to prevent replay attacks. /** @var SessionInterface $session */ $session = Factory::getApplication()->getSession(); $session->set('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', base64_encode(serialize($publicKeyCredentialRequestOptions))); $session->set('plg_multifactorauth_webauthn.userHandle', $userId); $session->set('plg_multifactorauth_webauthn.userId', $userId); // Return the JSON encoded data to the caller return json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } /** * Authentication step 2: Checks if the browser's response to our challenge is valid. * * @param string $response Base64-encoded response * * @return void * @throws \Exception When something does not check out. * @since 4.2.0 */ public static function verifyAssertion(string $response): void { /** @var SessionInterface $session */ $session = Factory::getApplication()->getSession(); $encodedPkOptions = $session->get('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null); $userHandle = $session->get('plg_multifactorauth_webauthn.userHandle', null); $userId = $session->get('plg_multifactorauth_webauthn.userId', null); $session->set('plg_multifactorauth_webauthn.publicKeyCredentialRequestOptions', null); $session->set('plg_multifactorauth_webauthn.userHandle', null); $session->set('plg_multifactorauth_webauthn.userId', null); if (empty($userId)) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Make sure the user exists $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); if ($user->id != $userId) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Make sure the user is ourselves (we cannot perform MFA on behalf of another user!) $currentUser = Factory::getApplication()->getIdentity() ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); if ($currentUser->id != $userId) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Make sure the public key credential request options in the session are valid $serializedOptions = base64_decode($encodedPkOptions); $publicKeyCredentialRequestOptions = unserialize($serializedOptions); if ( !is_object($publicKeyCredentialRequestOptions) || empty($publicKeyCredentialRequestOptions) || !($publicKeyCredentialRequestOptions instanceof PublicKeyCredentialRequestOptions) ) { throw new \RuntimeException(Text::_('PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); } // Unserialize the browser response data $data = base64_decode($response); self::getWebauthnServer($user->id)->loadAndCheckAssertionResponse( $data, $publicKeyCredentialRequestOptions, self::getUserEntity($user), ServerRequestFactory::fromGlobals() ); } /** * Get the user's avatar (through Gravatar) * * @param User $user The Joomla user object * @param int $size The dimensions of the image to fetch (default: 64 pixels) * * @return string The URL to the user's avatar * * @since 4.2.0 */ private static function getAvatar(User $user, int $size = 64) { $scheme = Uri::getInstance()->getScheme(); $subdomain = ($scheme == 'https') ? 'secure' : 'www'; return sprintf('%s://%s.gravatar.com/avatar/%s.jpg?s=%u&d=mm', $scheme, $subdomain, md5($user->email), $size); } /** * Get a WebAuthn user entity for a Joomla user * * @param User $user The user to get an entity for * * @return PublicKeyCredentialUserEntity * @since 4.2.0 */ private static function getUserEntity(User $user): PublicKeyCredentialUserEntity { return new PublicKeyCredentialUserEntity( $user->username, $user->id, $user->name, self::getAvatar($user, 64) ); } /** * Get the WebAuthn library server object * * @param int|null $userId The user ID holding the list of valid authenticators * * @return Server * @since 4.2.0 */ private static function getWebauthnServer(?int $userId): Server { /** @var CMSApplication $app */ try { $app = Factory::getApplication(); $siteName = $app->get('sitename'); } catch (\Exception $e) { $siteName = 'Joomla! Site'; } // Credentials repository $repository = new CredentialRepository($userId); // Relaying Party -- Our site $rpEntity = new PublicKeyCredentialRpEntity( $siteName ?? 'Joomla! Site', Uri::getInstance()->toString(['host']), '' ); $refClass = new \ReflectionClass(Server::class); $refConstructor = $refClass->getConstructor(); $params = $refConstructor->getParameters(); if (count($params) === 3) { // WebAuthn library 2, 3 $server = new Server($rpEntity, $repository, null); } else { // WebAuthn library 4 (based on the deprecated comments in library version 3) $server = new Server($rpEntity, $repository); } // Ed25519 is only available with libsodium if (!function_exists('sodium_crypto_sign_seed_keypair')) { $server->setSelectedAlgorithms(['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512']); } return $server; } /** * Returns an array of the PK credential descriptors (registered authenticators) for the given user. * * @param User $user The user to get the descriptors for * * @return PublicKeyCredentialDescriptor[] * @since 4.2.0 */ private static function getPubKeyDescriptorsForUser(User $user): array { $userEntity = self::getUserEntity($user); $repository = new CredentialRepository($user->id); $descriptors = []; $records = $repository->findAllForUserEntity($userEntity); foreach ($records as $record) { $descriptors[] = $record->getPublicKeyCredentialDescriptor(); } return $descriptors; } } PKpd�\��yK%K%%webauthn/src/CredentialRepository.phpnu�[���<?php /** * @package Joomla.Plugin * @subpackage Multifactorauth.webauthn * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Multifactorauth\Webauthn; use Joomla\CMS\Factory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\User\UserFactoryInterface; use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; use Joomla\Component\Users\Administrator\Table\MfaTable; use Webauthn\AttestationStatement\AttestationStatement; use Webauthn\AttestedCredentialData; use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialSourceRepository; use Webauthn\PublicKeyCredentialUserEntity; use Webauthn\TrustPath\EmptyTrustPath; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Implementation of the credentials repository for the WebAuthn library. * * Important assumption: interaction with Webauthn through the library is only performed for the currently logged in * user. Therefore all Methods which take a credential ID work by checking the Joomla MFA records of the current * user only. This is a necessity. The records are stored encrypted, therefore we cannot do a partial search in the * table. We have to load the records, decrypt them and inspect them. We cannot do that for thousands of records but * we CAN do that for the few records each user has under their account. * * This behavior can be changed by passing a user ID in the constructor of the class. * * @since 4.2.0 */ class CredentialRepository implements PublicKeyCredentialSourceRepository { /** * The user ID we will operate with * * @var integer * @since 4.2.0 */ private $userId = 0; /** * CredentialRepository constructor. * * @param int $userId The user ID this repository will be working with. * * @throws \Exception * @since 4.2.0 */ public function __construct(int $userId = 0) { if (empty($userId)) { $user = Factory::getApplication()->getIdentity() ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); $userId = $user->id; } $this->userId = $userId; } /** * Finds a WebAuthn record given a credential ID * * @param string $publicKeyCredentialId The public credential ID to look for * * @return PublicKeyCredentialSource|null * @since 4.2.0 */ public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource { $publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', ''); $credentials = $this->findAllForUserEntity($publicKeyCredentialUserEntity); foreach ($credentials as $record) { if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialId) { continue; } return $record; } return null; } /** * Find all WebAuthn entries given a user entity * * @param PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity The user entity to search by * * @return array|PublicKeyCredentialSource[] * @throws \Exception * @since 4.2.0 */ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array { if (empty($publicKeyCredentialUserEntity)) { $userId = $this->userId; } else { $userId = $publicKeyCredentialUserEntity->getId(); } $return = []; $results = MfaHelper::getUserMfaRecords($userId); if (count($results) < 1) { return $return; } /** @var MfaTable $result */ foreach ($results as $result) { $options = $result->options; if (!is_array($options) || empty($options)) { continue; } if (!isset($options['attested']) && !isset($options['pubkeysource'])) { continue; } if (isset($options['attested']) && is_string($options['attested'])) { $options['attested'] = json_decode($options['attested'], true); $return[$result->id] = $this->attestedCredentialToPublicKeyCredentialSource( AttestedCredentialData::createFromArray($options['attested']), $userId ); } elseif (isset($options['pubkeysource']) && is_string($options['pubkeysource'])) { $options['pubkeysource'] = json_decode($options['pubkeysource'], true); $return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']); } elseif (isset($options['pubkeysource']) && is_array($options['pubkeysource'])) { $return[$result->id] = PublicKeyCredentialSource::createFromArray($options['pubkeysource']); } } return $return; } /** * Converts a legacy AttestedCredentialData object stored in the database into a PublicKeyCredentialSource object. * * This makes several assumptions which can be problematic and the reason why the WebAuthn library version 2 moved * away from attested credentials to public key credential sources: * * - The credential is always of the public key type (that's safe as the only option supported) * - You can access it with any kind of authenticator transport: USB, NFC, Internal or Bluetooth LE (possibly * dangerous) * - There is no attestations (generally safe since browsers don't seem to support attestation yet) * - There is no trust path (generally safe since browsers don't seem to provide one) * - No counter was stored (dangerous since it can lead to replay attacks). * * @param AttestedCredentialData $record Legacy attested credential data object * @param int $userId User ID we are getting the credential source for * * @return PublicKeyCredentialSource * @since 4.2.0 */ private function attestedCredentialToPublicKeyCredentialSource(AttestedCredentialData $record, int $userId): PublicKeyCredentialSource { return new PublicKeyCredentialSource( $record->getCredentialId(), PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, [ PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB, PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC, PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL, PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE, ], AttestationStatement::TYPE_NONE, new EmptyTrustPath(), $record->getAaguid(), $record->getCredentialPublicKey(), $userId, 0 ); } /** * Save a WebAuthn record * * @param PublicKeyCredentialSource $publicKeyCredentialSource The record to save * * @return void * @throws \Exception * @since 4.2.0 */ public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void { // I can only create or update credentials for the user this class was created for if ($publicKeyCredentialSource->getUserHandle() != $this->userId) { throw new \RuntimeException('Cannot create or update WebAuthn credentials for a different user.', 403); } // Do I have an existing record for this credential? $recordId = null; $publicKeyCredentialUserEntity = new PublicKeyCredentialUserEntity('', $this->userId, '', ''); $credentials = $this->findAllForUserEntity($publicKeyCredentialUserEntity); foreach ($credentials as $id => $record) { if ($record->getAttestedCredentialData()->getCredentialId() != $publicKeyCredentialSource->getAttestedCredentialData()->getCredentialId()) { continue; } $recordId = $id; break; } // Create or update a record /** @var MVCFactoryInterface $factory */ $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); /** @var MfaTable $mfaTable */ $mfaTable = $factory->createTable('Mfa', 'Administrator'); if ($recordId) { $mfaTable->load($recordId); $options = $mfaTable->options; if (isset($options['attested'])) { unset($options['attested']); } $options['pubkeysource'] = $publicKeyCredentialSource; $mfaTable->save( [ 'options' => $options, ] ); } else { $mfaTable->reset(); $mfaTable->save( [ 'user_id' => $this->userId, 'title' => 'WebAuthn auto-save', 'method' => 'webauthn', 'default' => 0, 'options' => ['pubkeysource' => $publicKeyCredentialSource], ] ); } } } PKpd�\�ɧ��webauthn/webauthn.xmlnu�[���<?xml version="1.0" encoding="UTF-8"?> <extension type="plugin" group="multifactorauth" method="upgrade"> <name>plg_multifactorauth_webauthn</name> <author>Joomla! Project</author> <creationDate>2022-05</creationDate> <copyright>(C) 2022 Open Source Matters, Inc.</copyright> <license>GNU General Public License version 2 or later; see LICENSE.txt</license> <authorEmail>admin@joomla.org</authorEmail> <authorUrl>www.joomla.org</authorUrl> <version>4.2.0</version> <description>PLG_MULTIFACTORAUTH_WEBAUTHN_XML_DESCRIPTION</description> <namespace path="src">Joomla\Plugin\Multifactorauth\Webauthn</namespace> <files> <folder plugin="webauthn">services</folder> <folder>src</folder> <folder>tmpl</folder> </files> <languages> <language tag="en-GB">language/en-GB/plg_multifactorauth_webauthn.ini</language> <language tag="en-GB">language/en-GB/plg_multifactorauth_webauthn.sys.ini</language> </languages> </extension> PKpd�\ Ө��email/email.xmlnu�[���PKpd�\�Pp���email/services/provider.phpnu�[���PKpd�\�o; �T�T� email/src/Extension/Email.phpnu�[���PKpd�\X<G����bfixed/services/provider.phpnu�[���PKpd�\hD�ӄ��gfixed/fixed.xmlnu�[���PKpd�\����.�.[kfixed/src/Extension/Fixed.phpnu�[���PKpd�\�}^[�yubikey/services/provider.phpnu�[���PKpd�\�C0�R�R!Ɵyubikey/src/Extension/Yubikey.phpnu�[���PKpd�\=�z����yubikey/yubikey.xmlnu�[���PKpd�\m�F7979|�totp/src/Extension/Totp.phpnu�[���PKpd�\�n~~ �/totp/totp.xmlnu�[���PKpd�\H?D���3totp/services/provider.phpnu�[���PKpd�\�d����9webauthn/services/provider.phpnu�[���PKpd�\a����?webauthn/tmpl/default.phpnu�[���PKpd�\6�28C8C#�Ewebauthn/src/Extension/Webauthn.phpnu�[���PKpd�\����#�#::�webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.phpnu�[���PKpd�\N���,�,=v�webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.phpnu�[���PKpd�\3�UY(I(I��webauthn/src/Hotfix/Server.phpnu�[���PKpd�\\5353#K$webauthn/src/Helper/Credentials.phpnu�[���PKpd�\��yK%K%%�Wwebauthn/src/CredentialRepository.phpnu�[���PKpd�\�ɧ��s}webauthn/webauthn.xmlnu�[���PK�f�
/home/opticamezl/www/newok/07d6c/../1a0f4/../c9989/../assets/../././cli/../multifactorauth.zip