Your IP : 216.73.217.142


Current Path : /var/www/consult-e-syn/public_html/plugins/loginguard/u2f/
Upload File :
Current File : /var/www/consult-e-syn/public_html/plugins/loginguard/u2f/u2f.php

<?php
/**
 * @package   AkeebaLoginGuard
 * @copyright Copyright (c)2016-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

use Akeeba\LoginGuard\Admin\Model\Tfa;
use FOF30\Container\Container;
use Joomla\CMS\Environment\Browser;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;

// Prevent direct access
defined('_JEXEC') or die;

/**
 * Akeeba LoginGuard Plugin for Two Step Verification method "Time-based One Time Password"
 *
 * Requires a 6-digit code generated by Google Authenticator or any compatible application. These codes change
 * automatically every 30 seconds.
 */
class PlgLoginguardU2f extends CMSPlugin
{
	/**
	 * The TFA method name handled by this plugin
	 *
	 * @var   string
	 */
	private $tfaMethodName = 'u2f';

	/**
	 * Should I report myself as enabled?
	 *
	 * @var   bool
	 */
	private $enabled = true;

	/**
	 * U2F Server Library instance
	 *
	 * @var   \u2flib_server\U2F
	 */
	protected $u2f = null;

	/**
	 * Constructor. Loads the language files as well.
	 *
	 * @param   object  &$subject  The object to observe
	 * @param   array   $config    An optional associative array of configuration settings.
	 *                             Recognized key values include 'name', 'group', 'params', 'language'
	 *                             (this list is not meant to be comprehensive).
	 */
	public function __construct($subject, array $config = array())
	{
		parent::__construct($subject, $config);

		// Load the language file
		$this->loadLanguage();

		// Try to load the U2F server library
		if (!class_exists('u2flib_server\\U2F'))
		{
			require_once __DIR__ . '/classes/u2f.php';
		}

		// Make sure I can load the server library
		$this->enabled = class_exists('u2flib_server\\U2F');

		if (!$this->enabled)
		{
			return;
		}

		// Check if we're on a supported browser
		$this->enabled = $this->isSupportedBrowser();

		if (!$this->enabled)
		{
			return;
		}

		// Check OpenSSL version
		$this->enabled = $this->isOpenSSL10OrNewer();

		if (!$this->enabled)
		{
			return;
		}

		// Try to create a server library object
		$jURI = Uri::getInstance();
		$appId = $jURI->toString(array('scheme', 'host', 'port'));

		try
		{
			$this->u2f = new u2flib_server\U2F($appId);
		}
		catch (\Exception $e)
		{
			$this->enabled = false;

			return;
		}

		// Finally, detect old Google Chrome versions and activate U2F support manually.
		$this->loadOldChromeJavascript();
	}

	/**
	 * Gets the identity of this TFA method
	 *
	 * @return  array
	 */
	public function onLoginGuardTfaGetMethod()
	{
		if (!$this->enabled)
		{
			return array();
		}

		$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/U2F');

		return array(
			// Internal code of this TFA method
			'name'               => $this->tfaMethodName,
			// User-facing name for this TFA method
			'display'            => JText::_('PLG_LOGINGUARD_U2F_LBL_DISPLAYEDAS'),
			// Short description of this TFA method displayed to the user
			'shortinfo'          => JText::_('PLG_LOGINGUARD_U2F_LBL_SHORTINFO'),
			// URL to the logo image for this method
			'image'              => 'media/plg_loginguard_u2f/images/u2f.svg',
			// Are we allowed to disable it?
			'canDisable'         => true,
			// Are we allowed to have multiple instances of it per user?
			'allowMultiple'      => true,
			// URL for help content
			'help_url'           => $helpURL,
			// Allow authentication against all entries of this TFA method. Otherwise authentication takes place against a SPECIFIC entry at a time.
			'allowEntryBatching' => 1,
		);
	}

	/**
	 * Returns the information which allows LoginGuard to render the TFA setup page. This is the page which allows the
	 * user to add or modify a TFA method for their user account. If the record does not correspond to your plugin
	 * return an empty array.
	 *
	 * @param   stdClass  $record  The #__loginguard_tfa record currently selected by the user.
	 *
	 * @return  array
	 */
	public function onLoginGuardTfaGetSetup($record)
	{
		// Make sure we are enabled
		if (!$this->enabled)
		{
			return array();
		}

		// Make sure we are actually meant to handle this method
		if ($record->method != $this->tfaMethodName)
		{
			return array();
		}

		// Load the options from the record (if any)
		$options                    = $this->_decodeRecordOptions($record);
		$currentRecordRegistrations = isset($options['registrations']) ? $options['registrations'] : array();

		$registrations = $this->getRegistrationsFor($record->user_id);

		// Get some values assuming that we are NOT setting up U2F (the key is already registered)
		$submitOnClick = '';
		$preMessage    = JText::_('PLG_LOGINGUARD_U2F_LBL_CONFIGURED');
		$u2fRegData    = json_encode($this->u2f->getRegisterData($registrations));
		$type          = 'input';
		$html          = '';
		$helpURL       = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/U2F');

		/**
		 * If there are no security keys set up yet I need to show a different message and take a different action when
		 * my user clicks the submit button.
		 */
		if (empty($currentRecordRegistrations))
		{
			// Load Javascript
			HTMLHelper::_('script', 'plg_loginguard_u2f/u2f-api.min.js', [
				'version'       => 'auto',
				'relative'      => true,
				'detectDebug'   => true,
				'framework'     => true,
				'pathOnly'      => false,
				'detectBrowser' => true,
			], [
				'defer' => false,
				'async' => false,
			]);

			HTMLHelper::_('script', 'plg_loginguard_u2f/u2f.min.js', [
				'version'       => 'auto',
				'relative'      => true,
				'detectDebug'   => true,
				'framework'     => true,
				'pathOnly'      => false,
				'detectBrowser' => true,
			], [
				'defer' => false,
				'async' => false,
			]);

			$js = <<< JS
;; // Defense against broken scripts
window.jQuery(document).ready(function() {
	akeeba.LoginGuard.u2f.regData = $u2fRegData;
});

JS;
			Factory::getDocument()->addScriptDeclaration($js);

			$layoutPath = PluginHelper::getLayoutPath('loginguard', 'u2f', 'register');
			ob_start();
			include $layoutPath;
			$html = ob_get_clean();
			$type = 'custom';

			// Load JS translations
			JText::script('PLG_LOGINGUARD_U2F_ERR_JS_OTHER');
			JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CANNOTPROCESS');
			JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CLIENTCONFIGNOTSUPPORTED');
			JText::script('PLG_LOGINGUARD_U2F_ERR_JS_INELIGIBLE');
			JText::script('PLG_LOGINGUARD_U2F_ERR_JS_TIMEOUT');

			// Save the U2F request to the session
			$session = Factory::getSession();
			$session->set('u2f.request', $u2fRegData, 'com_loginguard');

			// Special button handling
			$submitOnClick = "akeeba.LoginGuard.u2f.setUp(); return false;";

			// Message to display
			$preMessage = JText::_('PLG_LOGINGUARD_U2F_LBL_INSTRUCTIONS');
		}

		return array(
			// Default title if you are setting up this TFA method for the first time
			'default_title'  => JText::_('PLG_LOGINGUARD_U2F_LBL_DISPLAYEDAS'),
			// Custom HTML to display above the TFA setup form
			'pre_message'    => $preMessage,
			// Heading for displayed tabular data. Typically used to display a list of fixed TFA codes, TOTP setup parameters etc
			'table_heading'  => '',
			// Any tabular data to display (label => custom HTML). See above
			'tabular_data'   => array(),
			// Hidden fields to include in the form (name => value)
			'hidden_data'    => array(
				'u2fregdata' => $u2fRegData,
			),
			// How to render the TFA setup code field. "input" (HTML input element) or "custom" (custom HTML)
			'field_type'     => $type,
			// The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
			'input_type'     => 'hidden',
			// Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc.
			'input_value'    => '',
			// 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'          => '',
			// Custom HTML. Only used when field_type = custom.
			'html'           => $html,
			// Should I show the submit button (apply the TFA setup)? Only applies in the Add page.
			'show_submit'    => false,
			// onclick handler for the submit button (apply the TFA setup)?
			'submit_onclick' => $submitOnClick,
			// Custom HTML to display below the TFA setup form
			'post_message'   => '',
			// URL for help content
			'help_url'       => $helpURL,
		);
	}

	/**
	 * Parse the input from the TFA 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   stdClass  $record  The #__loginguard_tfa record currently selected by the user.
	 * @param   JInput    $input   The user input you are going to take into account.
	 *
	 * @return  array  The configuration data to save to the database
	 *
	 * @throws  RuntimeException  In case the validation fails
	 */
	public function onLoginGuardTfaSaveSetup($record, JInput $input)
	{
		// Make sure we are enabled
		if (!$this->enabled)
		{
			return array();
		}

		// Make sure we are actually meant to handle this method
		if ($record->method != $this->tfaMethodName)
		{
			return array();
		}

		// Load the options from the record (if any)
		$options = $this->_decodeRecordOptions($record);

		if (!isset($options['registrations']))
		{
			$options['registrations'] = array();
		}

		// load the registration request from the session
		$session    = Factory::getSession();
		$u2fRegData = $session->get('u2f.request', null, 'com_loginguard');
		$session->set('u2f.request', null, 'com_loginguard');
		$registrationRequest = json_decode($u2fRegData);

		// Load the registration response from the input
		$code             = $input->get('code', null, 'raw');
		$registerResponse = json_decode($code);

		// If there was no registration request BUT there is a registration response throw an error
		if (empty($registrationRequest) && !(empty($code) || empty($registerResponse)))
		{
			throw new RuntimeException(JText::_('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))
		{
			return $options;
		}

		// In any other case try to authorize the registration
		try
		{
			$registration = $this->u2f->doRegister($registrationRequest[0], $registerResponse);
		}
		catch (\u2flib_server\Error $err)
		{
			throw new RuntimeException($err->getMessage(), 403);
		}

		// The code is valid. Unset the request data from the session and update the options
		$options['registrations'][] = $registration;

		// Return the configuration to be serialized
		return $options;
	}

	/**
	 * Returns the information which allows LoginGuard to render the captive TFA page. This is the page which appears
	 * right after you log in and asks you to validate your login with TFA.
	 *
	 * @param   stdClass  $record  The #__loginguard_tfa record currently selected by the user.
	 *
	 * @return  array
	 */
	public function onLoginGuardTfaCaptive($record)
	{
		// Make sure we are enabled
		if (!$this->enabled)
		{
			return [];
		}

		// Make sure we are actually meant to handle this method
		if ($record->method != $this->tfaMethodName)
		{
			return [];
		}

		// Get the media version
		$mediaVersion = Container::getInstance('com_loginguard')->mediaVersion;

		// We are going to load a JS file and use custom on-load JS to intercept the loginguard-captive-button-submit button
		HTMLHelper::_('script', 'plg_loginguard_u2f/u2f-api.min.js', [
			'version'       => $mediaVersion,
			'relative'      => true,
			'detectDebug'   => true,
			'framework'     => true,
			'pathOnly'      => false,
			'detectBrowser' => true,
		], [
			'defer' => false,
			'async' => false,
		]);

		HTMLHelper::_('script', 'plg_loginguard_u2f/u2f.min.js', [
			'version'       => $mediaVersion,
			'relative'      => true,
			'detectDebug'   => true,
			'framework'     => true,
			'pathOnly'      => false,
			'detectBrowser' => true,
		], [
			'defer' => false,
			'async' => false,
		]);

		// Load JS translations
		JText::script('PLG_LOGINGUARD_U2F_ERR_JS_OTHER');
		JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CANNOTPROCESS');
		JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CLIENTCONFIGNOTSUPPORTED');
		JText::script('PLG_LOGINGUARD_U2F_ERR_JS_INELIGIBLE_SIGN');
		JText::script('PLG_LOGINGUARD_U2F_ERR_JS_TIMEOUT');

		// Load the options from the record (if any), or from the entire method if the allowEntryBatching flag is set.
		$registrations = $this->getRegistrations($record);

		/**
		 * 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 U2F key through the Javascript API and signed. The signature is posted back to
		 * the form as the "code" which is read by onLoginGuardTfaValidate. 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 U2F 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 onLoginGuardTfaValidate 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         = Factory::getSession();
		$u2fAuthData     = $this->u2f->getAuthenticateData($registrations);
		$u2fAuthData     = $session->get('u2f.authentication', base64_encode(serialize($u2fAuthData)), 'com_loginguard');
		$u2fAuthData     = unserialize(base64_decode($u2fAuthData));
		$u2fAuthDataJSON = json_encode($u2fAuthData);
		$session->set('u2f.authentication', base64_encode(serialize($u2fAuthData)), 'com_loginguard');

		$js = <<< JS
;; // Defense against broken scripts

function akeebaLoginGuardU2FOnClick()
{
	    window.jQuery('#loginguard-u2f-button').hide();
		akeeba.LoginGuard.u2f.validate();

		return false;
}
		
window.jQuery(document).ready(function($) {
	akeeba.LoginGuard.u2f.authData = $u2fAuthDataJSON;
	
	$('#loginguard-captive-button-submit').click(function() {
	    akeebaLoginGuardU2FOnClick();
	});
	
	setTimeout(function() {
	    akeebaLoginGuardU2FOnClick();
	}, 250);
});

JS;
		Factory::getDocument()->addScriptDeclaration($js);

		$layoutPath = PluginHelper::getLayoutPath('loginguard', 'u2f', 'validate');
		ob_start();
		include $layoutPath;
		$html = ob_get_clean();

		$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/U2F');

		return [
			// Custom HTML to display above the TFA form
			'pre_message'        => JText::_('PLG_LOGINGUARD_U2F_LBL_INSTRUCTIONS'),
			// How to render the TFA code field. "input" (HTML input element) or "custom" (custom HTML)
			'field_type'         => 'custom',
			// The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
			'input_type'         => '',
			// 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'              => '',
			// Custom HTML. Only used when field_type = custom.
			'html'               => $html,
			// Custom HTML to display below the TFA form
			'post_message'       => '',
			// Should I hide the submit button? Useful if you need to render your own buttons or use a method which is meant to auto-submit upon doing a certain action.
			'hide_submit'        => true,
			// URL for help content
			'help_url'           => $helpURL,
			// Allow authentication against all entries of this TFA method. Otherwise authentication takes place against a SPECIFIC entry at a time.
			'allowEntryBatching' => 1,
		];
	}

	/**
	 * Validates the Two Factor Authentication code submitted by the user in the captive Two Step Verification page. If
	 * the record does not correspond to your plugin return FALSE.
	 *
	 * @param   Tfa       $record  The TFA method's record you're validating against
	 * @param   User      $user    The user record
	 * @param   string    $code    The submitted code
	 *
	 * @return  bool
	 */
	public function onLoginGuardTfaValidate(Tfa $record, User $user, $code)
	{
		// Make sure we are enabled
		if (!$this->enabled)
		{
			return false;
		}

		// Make sure we are actually meant to handle this method
		if ($record->method != $this->tfaMethodName)
		{
			return false;
		}

		// Double check the TFA method is for the correct user
		if ($user->id != $record->user_id)
		{
			return false;
		}

		// Load the options from the record (if any), or from the entire method if the allowEntryBatching flag is set.
		$registrations = $this->getRegistrations($record);

		// Get the authentication response
		$authenticateResponse = json_decode($code);

		if (empty($authenticateResponse))
		{
			// Invalid authentication signature response in request
			return false;
		}

		$session = Factory::getSession();
		$authenticationRequest = $session->get('u2f.authentication', null, 'com_loginguard');
		$session->set('u2f.authentication', null, 'com_loginguard');

		if (empty($authenticationRequest))
		{
			// No authentication request in session; do not proceed
			return false;
		}

		$authenticationRequest = unserialize(base64_decode($authenticationRequest));

		if (empty($authenticationRequest))
		{
			// Invalid authentication request in session; do not proceed
			return false;
		}

		// Validate the U2F signature
		try
		{
			$registration = $this->u2f->doAuthenticate($authenticationRequest, $registrations, $authenticateResponse);
		}
		catch (Exception $e)
		{
			return false;
		}

		// The $registration contains the updated registration for the used security key. But WHICH one?
		$id = $record->id;

		/**
		 * Save the updated registration to the database.
		 *
		 * Why? Every time the security key signs a verification request it increases its internal counter monotonical-
		 * ly. Every subsequent signing request will have a counter larger than the previous one. If the library sees a
		 * counter that's lower than the last recorded one we know that we have a cloned security key and we have to
		 * reject it. This protection only works if we "remember" the last counter encountered, i.e. if we save the
		 * updated registration after validation.
		 */
		$update = (object)array(
			'id' => $id,
			'options' => json_encode(array('registrations' => array($registration))),
		);

		$container = Container::getInstance('com_loginguard');
		$container->platform->runPlugins('onLoginGuardBeforeSaveRecord', [&$update]);

		$db = Factory::getDbo();
		$db->updateObject('#__loginguard_tfa', $update, array('id'));

		return true;
	}

	/**
	 * Decodes the options from a #__loginguard_tfa record into an options object.
	 *
	 * @param   stdClass|string  $record  The record object or just the JSON-encoded options
	 *
	 * @return  array
	 */
	private function _decodeRecordOptions($record)
	{
		$options = array(
			'registrations' => array(),
		);

		$recordOptions = null;

		if (is_object($record))
		{
			$recordOptions = $record->options;
		}
		elseif (is_string($record))
		{
			$recordOptions = $record;
		}

		if (!empty($recordOptions))
		{
			/**
			 * The end result is:
			 * $recordOptions is an array with one key, 'registrations'
			 * $recordOptions['registrations'] is a simple (numerically indexed) array. Its contents are objects.
			 * That's exactly what I wanted.
			 */
			$temp = [];

			foreach ($recordOptions['registrations'] as $k => $opt)
			{
				$temp[$k] = (object)$opt;
			}

			$recordOptions = ['registrations' => $temp];

			$options = array_merge($options, $recordOptions);
		}

		return $options;
	}

	/**
	 * Checks if we have OpenSSL 1.0 or newer
	 *
	 * @return  bool
	 */
	private function isOpenSSL10OrNewer()
	{
		// No OpenSSL? No joy.
		if (!defined('OPENSSL_VERSION_TEXT'))
		{
			return false;
		}

		$parts = explode(' ', OPENSSL_VERSION_TEXT);

		// Not actually OpenSSL? No joy.
		if (strtoupper($parts[0]) != 'OPENSSL')
		{
			return false;
		}

		// We can't directly use version compare as it doesn't follow PHP version semantics
		$version = $parts[1];
		$parts = explode('.', $version, 4);
		$version = $parts[0] . '.' . $parts[1] . '.' . (int)$parts[2];

		return version_compare($version, '1.0.0', 'ge');
	}

	/**
	 * Old Google Chrome versions (38, 39 and 40) required a special extension to enable support for U2F. This extension
	 * would only load by default the U2F support on Google sites. Third party sites needed to load the extension's JS
	 * file directly. This method detects Google Chrome 38, 39 and 40 and loads the extension Javascript to activate U2F
	 * support. On newer versions of Google Chrome and browsers made by other vendors this method takes no action. U2F
	 * is supported transparently either natively or through an add-on.
	 *
	 * NB: If you're still using a browser from late 2013 to late 2014 then being able to use a U2F security key is
	 *     probably the least of your worries. I wouldn't run such an old browser even at gunpoint!
	 *
	 * @see  http://stackoverflow.com/questions/27158182/u2f-support-without-the-u2f-chrome-extension
	 *
	 * @return  void
	 */
	private function loadOldChromeJavascript()
	{
		if (!class_exists('JBrowser'))
		{
			JLoader::import('joomla.environment.browser');
		}

		$jBrowser       = Browser::getInstance();
		$browserMake    = $jBrowser->getBrowser() == 'chrome';
		$browserVersion = $jBrowser->getVersion();
		$isOldChrome    = ($browserMake) && version_compare($browserVersion, '38.0', 'ge') && version_compare($browserVersion, '41.0', 'lt');

		if ($isOldChrome)
		{
			Factory::getDocument()->addScript('chrome-extension://pfboblefjcgdjicmnffhdgionmgcdmne/u2f-api.js');
		}
	}

	/**
	 * Get all security key registrations for the specified user
	 *
	 * @param   int  $user_id  The user ID to look for. Leave empty for the current user.
	 *
	 * @return  array
	 */
	private function getRegistrationsFor($user_id = null)
	{
		if (empty($user_id))
		{
			$user_id = Factory::getUser()->id;
		}

		$return = array();

		$container = Container::getInstance('com_loginguard');
		/** @var Tfa $tfaModel */
		$tfaModel = $container->factory->model('Tfa')->tmpInstance();
		$results = $tfaModel->user_id($user_id)->method('u2f')->get(true);

		if ($results->count() < 1)
		{
			return $return;
		}

		foreach ($results as $result)
		{
			$options = $this->_decodeRecordOptions($result);

			if (!isset($options['registrations']) || empty($options['registrations']))
			{
				continue;
			}

			$return[$result->id] = $options['registrations'][0];
		}

		return $return;
	}

	/**
	 * Get the security key registrations for a given record. If the allowEntryBatching flag is 0 (No) we only return
	 * the key registrations for the given record. If the allowEntryBatching flag is 1 (Yes) we return the combined key
	 * registrations for all security key records of the user ID found in the $record object.
	 *
	 * @param   stdClass  $record  The LoginGuard record
	 *
	 * @return  array  Security key registrations for use by the U2F library
	 */
	private function getRegistrations($record)
	{
		$options       = $this->_decodeRecordOptions($record);

		$registrations = array();

		try
		{
			$container = Container::getInstance('com_loginguard');
			/** @var Tfa $tfaModel */
			$tfaModel = $container->factory->model('Tfa')->tmpInstance();
			$records = $tfaModel->user_id($record->user_id)->method($record->method)->get(true);
		}
		catch (Exception $e)
		{
			$records = array();
		}

		// Loop all records, stop if at least one matches
		$container = Container::getInstance('com_loginguard');

		/** @var Tfa $aRecord */
		foreach ($records as $aRecord)
		{
			$recordOptions       = $this->_decodeRecordOptions($aRecord);
			$recordRegistrations = isset($recordOptions['registrations']) ? $recordOptions['registrations'] : array();
			$registrations       = array_merge($registrations, $recordRegistrations);
		}

		return $registrations;
	}

	/**
	 * Deactivate on unsupported browsers.
	 *
	 * Based on our research, the only browsers where U2F is supported are Google Chrome and Opera 15+. This applies
	 * to all operating systems except for iOS (see below).
	 *
	 * Note that even though FF offers experimental U2F support it doesn't work properly. We've found that only
	 * security keys registered by Firefox will work on it. Moreover, registering a security key in the frontend
	 * makes it impossible to use it in the backend (and vice versa). As a result we're now disabling the U2F plugin
	 * on Firefox.
	 *
	 * Finally, iOS devices do not support security keys on any browser. Security keys are USB devices which need a
	 * full USB host implementation and OS support to interact with them. iOS does not provide such a feature.
	 * Please note that "security keys" refers to U2F mode *only*. YubiKey OTP mode still works because the device
	 * works as a USB keyboard or an NFC data tag in that mode. iOS supports USB keyboards if you use the Apple
	 * USB-C/Lightning to USB-A dongle. Moreover, since iOS 11, it is possible to use a third party NFC reader app
	 * to get a YubiKey OTP code over NFC, copy it to clipboard and then paste it in the browser of your choice.
	 *
	 * @return  bool
	 */
	private function isSupportedBrowser()
	{
		$agent = '';

		if (isset($_SERVER['HTTP_USER_AGENT']))
		{
			$agent = trim($_SERVER['HTTP_USER_AGENT']);
		}

		/**
		 * If I have no idea which browser you're using I'm just going to let you try and use U2F. May the Force be with
		 * you.
		 */
		if (empty($agent))
		{
			return true;
		}

		// If you're on iOS I won't let you try and use U2F as it's not supported by your Operating System.
		if (preg_match('/(iPhone|iPod|iPad|iOS)/i', $agent))
		{
			return false;
		}

		// Getting your browser make and model. Hang on.
		$jBrowser       = Browser::getInstance();
		$browserMake    = $jBrowser->getBrowser();
		$browserVersion = $jBrowser->getVersion();

		// Are you on Chrome 38+? Awesome, you can use U2F on Windows, Linux, macOS and Android!
		if (($browserMake == 'chrome') && version_compare($browserVersion, '38.0', 'ge'))
		{
			return true;
		}

		// Are you using Opera 41 or later? Awesome, you can use U2F on Windows, Linux and macOS!
		if (($browserMake == 'opera') && version_compare($browserVersion, '41.0', 'ge'))
		{
			return true;
		}

		return false;
	}
}