Your IP : 18.219.112.243


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/security/lib/mfa/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/security/lib/mfa/otp.php

<?php

namespace Bitrix\Security\Mfa;

use Bitrix\Main\Application;
use Bitrix\Main\ArgumentOutOfRangeException;
use Bitrix\Main\ArgumentTypeException;
use Bitrix\Main\Config\Option;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Type;
use Bitrix\Main\Security\Sign\BadSignatureException;
use Bitrix\Main\Security\Sign\TimeSigner;
use Bitrix\Main\Security\Random;
use Bitrix\Main\Text\Base32;
use Bitrix\Main\Security\Mfa\OtpAlgorithm;
use Bitrix\Main\Authentication\Policy;

Loc::loadMessages(__FILE__);

class Otp
{
	const TYPE_HOTP = 'hotp';
	const TYPE_TOTP = 'totp';
	const TYPE_DEFAULT = self::TYPE_HOTP;
	const SECRET_LENGTH = 20; // Must be power of 5 for "nicely" App Secret view

	const SKIP_COOKIE = 'OTPH';

	const REJECTED_KEY = 'OTP_REJECT_REASON';
	const REJECT_BY_CODE = 'code';
	const REJECT_BY_MANDATORY = 'mandatory';

	const TAGGED_CACHE_TEMPLATE = 'USER_OTP_%d';

	protected static $availableTypes = array(self::TYPE_HOTP, self::TYPE_TOTP);
	protected static $typeMap = array(
		self::TYPE_HOTP => '\Bitrix\Main\Security\Mfa\HotpAlgorithm',
		self::TYPE_TOTP => '\Bitrix\Main\Security\Mfa\TotpAlgorithm',
	);
	protected $algorithmClass = null;
	protected array $initParams = [];
	protected $regenerated = false;
	/* @var \Bitrix\Main\Context $context */
	protected $context = null;

	protected $userId = null;
	protected $userLogin = null;
	/* @var Policy\RulesCollection*/
	protected $userGroupPolicy;
	protected $active = null;
	protected $userActive = null;
	protected $secret = null;
	protected $issuer = null;
	protected $label = null;
	protected $params = null;
	protected $attempts = null;
	protected $type = null;
	/** @var Type\DateTime */
	protected $initialDate = null;
	protected $skipMandatory = null;
	/** @var Type\DateTime */
	protected $deactivateUntil = null;

	/**
	 * @param string|null $algorithm Class of needed OtpAlgorithm.
	 */
	public function __construct($algorithm = null)
	{
		if ($algorithm === null)
		{
			$this->setType(static::getDefaultType());
		}
		else
		{
			$this->algorithmClass = $algorithm;
		}
	}

	/**
	 * Return new instance for user provided by user ID
	 *
	 * @param int $userId User ID.
	 * @throws ArgumentOutOfRangeException
	 * @throws ArgumentTypeException
	 * @return static New instance, if user does not use OTP - returning NullObject (see Otp::isActivated).
	 */
	public static function getByUser($userId)
	{
		$userId = (int) $userId;

		if ($userId <= 0)
			throw new ArgumentTypeException('userId', 'positive integer');

		$userInfo = UserTable::getList(array(
			'filter' => array('=USER_ID' => $userId),
			'select' => array('ACTIVE', 'USER_ID', 'SECRET', 'INIT_PARAMS', 'PARAMS', 'TYPE', 'ATTEMPTS', 'INITIAL_DATE', 'SKIP_MANDATORY', 'DEACTIVATE_UNTIL', 'USER_ACTIVE' => 'USER.ACTIVE')
		));

		$userInfo = $userInfo->fetch();

		if (!$userInfo)
		{
			// OTP not available for this user
			$instance = new static;
			$instance->setUserId($userId);
			$instance->setActive(false);
		}
		else
		{
			$type = $userInfo['TYPE'] ?: self::TYPE_DEFAULT;
			$userInfo['SECRET'] = pack('H*', $userInfo['SECRET']);
			$userInfo['ACTIVE'] = ($userInfo['ACTIVE'] === 'Y');
			$userInfo['USER_ACTIVE'] = ($userInfo['USER_ACTIVE'] === 'Y');
			$userInfo['SKIP_MANDATORY'] = $userInfo['SKIP_MANDATORY'] === 'Y';

			$instance = static::getByType($type);
			$instance->setUserInfo($userInfo);
		}

		return $instance;
	}

	/**
	 * Return new instance with needed OtpAlgorithm type
	 *
	 * @param string $type Type of OtpAlgorithm (see getAvailableTypes).
	 * @throws ArgumentOutOfRangeException
	 * @return static New instance
	 */
	public static function getByType($type)
	{
		if (!in_array($type, static::$availableTypes))
			throw new ArgumentOutOfRangeException('type', static::$availableTypes);

		$algo = static::$typeMap[$type];
		$instance = new static($algo);
		$instance->setType($type);
		return $instance;
	}

	/**
	 * Set new type of OtpAlgorithm
	 *
	 * @param string $type Type of OtpAlgorithm (see getAvailableTypes).
	 * @throws ArgumentOutOfRangeException
	 * @return $this
	 */
	public function setType($type)
	{
		if (!in_array($type, static::$availableTypes))
			throw new ArgumentOutOfRangeException('type', static::$availableTypes);

		$this->algorithmClass = static::$typeMap[$type];
		$this->type = $type;

		return $this;
	}

	/**
	 * Sets initialization parameters for algorithms.
	 *
	 * @param array $params
	 * @return $this
	 */
	public function setInitParams(array $params)
	{
		$this->initParams = $params;
		return $this;
	}

	/**
	 * Returns initialization parameters for algorithms.
	 *
	 * @return array
	 */
	public function getInitParams(): array
	{
		return $this->initParams;
	}

	/**
	 * Return used OtpAlgorithm type
	 *
	 * @return string
	 */
	public function getType()
	{
		return $this->type;
	}

	/**
	 * Return instance of used OtpAlgorithm
	 *
	 * @return OtpAlgorithm
	 */
	public function getAlgorithm()
	{
		/** @var OtpAlgorithm $algorithm */
		$algorithm = new $this->algorithmClass($this->getInitParams());
		$algorithm->setSecret($this->getSecret());

		return $algorithm;
	}

	/**
	 * Return Provision URI according to KeyUriFormat
	 *
	 * @link https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
	 * @param array $opts Additional URI parameters, e.g. ['image' => 'http://example.com/my_logo.png'] .
	 * @return string
	 */
	public function getProvisioningUri(array $opts = array())
	{
		$issuer = $this->getIssuer();
		$opts += array('issuer' => $issuer);

		return $this
			->getAlgorithm()
			->generateUri(
				$this->getLabel($issuer),
				$opts
			);
	}

	/**
	 * Reinitialize OTP (generate new secret, set default algo, etc), must be called before connect new device
	 *
	 * @param null $newSecret Using custom secret.
	 * @return $this
	 */
	public function regenerate($newSecret = null)
	{
		if (!$newSecret)
		{
			$newSecret = Random::getBytes(static::SECRET_LENGTH);
		}

		$this->regenerated = true;
		return $this
			->setType(static::getDefaultType())
			->setAttempts(0)
			->setSkipMandatory(false)
			->setInitialDate(new Type\DateTime)
			->setDeactivateUntil(null)
			->setParams(null)
			->setSecret($newSecret)
			->setActive(true)
		;
	}

	/**
	 * Verify provided input
	 *
	 * @param string $input Input received from user.
	 * @param bool $updateParams Update or not user parameters in DB (e.g. counter for HotpAlgorithm).
	 * @return bool True if input is valid.
	 */
	public function verify($input, $updateParams = true)
	{
		[$result, $newParams] = $this->getAlgorithm()->verify($input, $this->getParams());

		if (
			$updateParams
			&& $newParams !== null
			&& $this->isActivated()
		)
		{
			$this
				->setParams($newParams)
				->save()
			;
		}
		return $result;
	}

	/**
	 * Check is verifying attempts reached according to group security policy
	 * May be used for show Captcha or what ever you want
	 *
	 * @return bool
	 */
	public function isAttemptsReached()
	{
		$attempts  = $this->getAttempts();
		$maxAttempts = $this->getMaxLoginAttempts();
		return (
			$maxAttempts > 0
			&& $attempts >= $maxAttempts
		);
	}

	/**
	 * Return synchronized user params for provided inputs
	 *
	 * @param string $inputA First code.
	 * @param string $inputB Second code.
	 * @throws \Bitrix\Main\Security\OtpException
	 * @return string
	 */
	public function getSyncParameters($inputA, $inputB)
	{
		return $this->getAlgorithm()->getSyncParameters((string) $inputA, (string) $inputB);
	}

	/**
	 * Synchronize user params for provided inputs
	 * Must be called after regenerate and before save!
	 * If something went wrong - throw OtpException with valid description in message
	 *
	 * @param string $inputA First code.
	 * @param string|null $inputB Second code.
	 * @throws OtpException
	 * @return $this
	 */
	public function syncParameters($inputA, $inputB = null)
	{
		if (!$inputA)
			throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_PASS1_EMPTY'));
		elseif (!preg_match('/^\d{6}$/D', $inputA))
			throw new OtpException(getMessage('SECURITY_OTP_ERROR_PASS1_INVALID'));

		if ($this->getAlgorithm()->isTwoCodeRequired())
		{
			if (!$inputB)
				throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_PASS2_EMPTY'));
			elseif (!preg_match('/^\d{6}$/D', $inputB))
				throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_PASS2_INVALID'));
		}

		try
		{
			$params = $this->getSyncParameters($inputA, $inputB);
		}
		catch (\Bitrix\Main\Security\OtpException)
		{
			throw new OtpException(Loc::getMessage('SECURITY_OTP_ERROR_SYNC_ERROR'));
		}

		$this->setParams($params);

		return $this;
	}

	/**
	 * Save all OTP data to DB
	 *
	 * @throws OtpException
	 * @return bool
	 */
	public function save()
	{
		$fields = array(
			'ACTIVE' => $this->isActivated()? 'Y': 'N',
			'TYPE' => $this->getType(),
			'INIT_PARAMS' => $this->getInitParams(),
			'ATTEMPTS' => $this->getAttempts(),
			'SECRET' => $this->getHexSecret(),
			'INITIAL_DATE' => $this->getInitialDate()?: new Type\DateTime,
			'PARAMS' => $this->getParams(),
			'SKIP_MANDATORY' => $this->isMandatorySkipped()? 'Y': 'N',
			'DEACTIVATE_UNTIL' => $this->getDeactivateUntil()
		);

		if ($this->regenerated)
		{
			if (!$this->isInitialized())
				throw new OtpException('Missing OTP params, forgot to call syncParameters?');

			// Clear recovery codes when we connect new device
			RecoveryCodesTable::clearByUser($this->getUserId());
		}

		if ($this->isDbRecordExists())
		{
			$result = UserTable::update($this->getUserId(), $fields);
		}
		else
		{
			$fields += array(
				'USER_ID' => $this->getUserId(),
			);
			$result = UserTable::add($fields);
		}

		$this->clearGlobalCache();

		return $result->isSuccess();
	}

	/**
	 * Delete OTP record from DB
	 *
	 * @return $this
	 */
	public function delete()
	{
		UserTable::delete($this->getUserId());

		return $this;
	}

	/**
	 * Activates user's OTP.
	 * OTP must be initialized (have secret, params, etc.) before activate
	 *
	 * @return $this
	 * @throws OtpException
	 */
	public function activate()
	{
		if (!$this->isInitialized())
			throw new OtpException('OTP not initialized, if your activate it - user can\'t login anymore. Do you forgot to call regenerate?');

		$this
			->setActive(true)
			->setDeactivateUntil(null)
			->save();

		return $this;
	}

	/**
	 * Deactivate user OTP for a needed number of days or forever
	 *
	 * @param int $days Days. 0 means "forever".
	 * @return $this
	 * @throws OtpException
	 */
	public function deactivate($days = 0)
	{
		if (!$this->isActivated())
			throw new OtpException('Otp not activated. Do your mean deffer?');

		$this->setActive(false);
		$this->setSkipMandatory();

		if ($days <= 0)
		{
			$this->setDeactivateUntil(null);
		}
		else
		{
			$deactivateDate = Type\DateTime::createFromTimestamp(time() + $days * 86400);
			$this->setDeactivateUntil($deactivateDate);
		}

		$this->save();

		return $this;
	}

	/**
	 * Defer  mandatory user OTP using for a needed number of days or forever
	 *
	 * @param int $days Days. 0 means "forever".
	 * @return $this
	 * @throws OtpException
	 */
	public function defer($days = 0)
	{
		if ($this->isActivated())
			throw new OtpException('Otp already activated. Do your mean deactivate?');

		$this->setSkipMandatory();
		if ($days <= 0)
		{
			$this->setDeactivateUntil(null);
		}
		else
		{
			$deactivateDate = Type\DateTime::createFromTimestamp(time() + $days * 86400);
			$this->setDeactivateUntil($deactivateDate);
		}

		$this->save();

		return $this;
	}

	/**
	 * Set new user information
	 * Mostly used for initialization from DB
	 * Now support:
	 *  - ACTIVE: bool, activating state (see setActive)
	 *  - USER_ID: integer, User ID (see setUserId)
	 *  - ATTEMPTS: integer, Attempts counter (see setAttempts)
	 *  - SECRET: binary, User secret (see setSecret)
	 *  - PARAMS: string, User params (see setParams and getSyncParameters)
	 *  - INITIAL_DATE: Type\Date, OTP initial date (see setInitialDate)
	 *
	 * @param array $userInfo See above.
	 * @return $this
	 */
	public function setUserInfo(array $userInfo)
	{
		$this
			->setActive($userInfo['ACTIVE'])
			->setUserActive($userInfo['USER_ACTIVE'])
			->setUserId($userInfo['USER_ID'])
			->setAttempts($userInfo['ATTEMPTS'])
			->setSecret($userInfo['SECRET'])
			->setInitParams($userInfo['INIT_PARAMS'])
			->setParams($userInfo['PARAMS'])
			->setSkipMandatory($userInfo['SKIP_MANDATORY'])
		;

		// Old users haven't INITIAL_DATE and DEACTIVATE_UNTIL
		// ToDo: maybe it's not the best approach, think about it later
		if ($userInfo['INITIAL_DATE'])
			$this->setInitialDate($userInfo['INITIAL_DATE']);

		if ($userInfo['DEACTIVATE_UNTIL'])
			$this->setDeactivateUntil($userInfo['DEACTIVATE_UNTIL']);

		return $this;
	}

	/**
	 * Set new OTP initialization date
	 *
	 * @param Type\DateTime $date Initialization date.
	 * @return $this
	 */
	protected function setInitialDate(Type\DateTime $date)
	{
		$this->initialDate = $date;

		return $this;
	}

	/**
	 * Returns OTP initialization date
	 *
	 * @return Type\DateTime
	 */
	public function getInitialDate()
	{
		return $this->initialDate;
	}

	/**
	 * Set datetime when user OTP must activated back
	 *
	 * @param Type\DateTime|null $date Datetime. "null" means never.
	 * @return $this
	 */
	protected function setDeactivateUntil($date)
	{
		$this->deactivateUntil = $date;

		return $this;
	}

	/**
	 * @return Type\DateTime
	 */
	public function getDeactivateUntil()
	{
		return $this->deactivateUntil;
	}

	/**
	 * Set if user allowed to bypass OTP mandatory using while authorization
	 *
	 * @param bool $isSkipped Allowed or not.
	 * @return $this
	 */
	protected function setSkipMandatory($isSkipped = true)
	{
		$this->skipMandatory = $isSkipped;

		return $this;
	}

	/**
	 * Returns true if user can skip mandatory using
	 *
	 * @return bool
	 */
	public function isMandatorySkipped()
	{
		return $this->skipMandatory;
	}

	/**
	 * Returns Unix timestamp of OTP initialization date
	 *
	 * @return int
	 */
	protected function getInitialTimestamp()
	{
		$initialDate = $this->getInitialDate();
		if (!$initialDate)
			return 0;

		return $initialDate->getTimestamp();
	}

	/**
	 * Set new User ID
	 *
	 * @param int $userId User ID.
	 * @return $this
	 */
	protected function setUserId($userId)
	{
		$this->userId = $userId;

		return $this;
	}

	/**
	 * Return used User ID
	 *
	 * @return int
	 */
	public function getUserId()
	{
		return (int) $this->userId;
	}

	/**
	 * Set new activating state
	 *
	 * @param bool $isActive Otp is activated or not.
	 * @return $this
	 */
	public function setActive($isActive)
	{
		$this->active = $isActive;

		return $this;
	}

	/**
	 * Return is OTP activated or not
	 *
	 * @return bool
	 */
	public function isActivated()
	{
		return (bool) $this->active;
	}

	public function setUserActive($isActive)
	{
		$this->userActive = $isActive;

		return $this;
	}

	public function isUserActive()
	{
		return (bool) $this->userActive;
	}

	/**
	 * @return bool
	 */
	public function isInitialized()
	{
		if ($this->isActivated())
		{
			// Without "hacks" OTP can't be activated without initialization
			return true;
		}

		// ToDo: maybe better add new property with column?
		return (bool) $this->getSecret();
	}

	/**
	 * Set new verifying attempts count
	 *
	 * @param int $attemptsCount Attempts count.
	 * @return $this
	 */
	protected function setAttempts($attemptsCount)
	{
		$this->attempts = $attemptsCount;

		return $this;
	}

	/**
	 * Return verifying attempts count
	 *
	 * @return int
	 */
	public function getAttempts()
	{
		return (int) $this->attempts;
	}

	/**
	 * Set new user params (e.g. counter for HotpAlgorithm)
	 *
	 * @see getSyncParameters
	 * @param string $params User params.
	 * @return $this
	 */
	protected function setParams($params)
	{
		$this->params = $params;

		return $this;
	}

	/**
	 * Return user params (e.g. counter for HotpAlgorithm)
	 *
	 * @return string
	 */
	public function getParams()
	{
		return (string) $this->params;
	}

	/**
	 * Return binary secret
	 *
	 * @return string
	 */
	public function getSecret()
	{
		return $this->secret;
	}

	/**
	 * Return hex-encoded secret
	 *
	 * @return string
	 */
	public function getHexSecret()
	{
		$secret = $this->getSecret();

		return bin2hex($secret);
	}

	/**
	 * Return mobile application secret, using for manual device initialization
	 *
	 * @return string
	 */
	public function getAppSecret()
	{
		$secret = $this->getSecret();
		$secret = Base32::encode($secret);

		return rtrim($secret, '=');
	}

	/**
	 * Set new secret
	 *
	 * @param string $secret Binary secret.
	 * @return $this
	 */
	public function setSecret($secret)
	{
		$this->secret = $secret;
		return $this;
	}

	/**
	 * Set new secret in hex-encoded representation
	 *
	 * @param string $hexValue Hex-encoded secret.
	 * @return $this
	 */
	public function setHexSecret($hexValue)
	{
		$secret = pack('H*', $hexValue);

		return $this->setSecret($secret);
	}

	/**
	 * Set new mobile application secret
	 *
	 * @param string $value Secret.
	 * @return $this
	 */
	public function setAppSecret($value)
	{
		$secret = Base32::decode($value);

		return $this->setSecret($secret);
	}

	/**
	 * Return issuer.
	 * If custom issuer not available - return default (see getDefaultIssuer).
	 *
	 * @return string
	 */
	public function getIssuer()
	{
		if ($this->issuer === null)
			$this->issuer = $this->getDefaultIssuer();

		return $this->issuer;
	}

	/**
	 * Set custom issuer
	 *
	 * @param string $issuer Issuer.
	 * @return $this
	 */
	public function setIssuer($issuer)
	{
		$this->issuer = $issuer;
		return $this;
	}

	/**
	 * Return label for issuer (if provided)
	 * If custom label not available - generate default (see generateLabel)
	 *
	 * @param string|null $issuer Issuer.
	 * @return string
	 */
	public function getLabel($issuer = null)
	{
		if ($this->label === null)
			$this->label = $this->generateLabel($issuer);

		return $this->label;
	}

	/**
	 * Set custom label
	 *
	 * @param string $label Label.
	 * @return $this
	 */
	public function setLabel($label)
	{
		$this->label = $label;
		return $this;
	}

	/**
	 * Returns context of the current request.
	 *
	 * @return \Bitrix\Main\Context
	 */
	public function getContext()
	{
		if ($this->context === null)
			$this->context = Application::getInstance()->getContext();

		return $this->context;
	}

	/**
	 * Set context of the current request.
	 *
	 * @param \Bitrix\Main\Context $context Application context.
	 * @return $this
	 */
	public function setContext(\Bitrix\Main\Context $context)
	{
		$this->context = $context;
		return $this;
	}

	/**
	 * Set custom user login
	 *
	 * @param string $login Login.
	 * @return $this
	 */
	public function setUserLogin($login)
	{
		$this->userLogin = $login;

		return $this;
	}

	/**
	 * Return user login
	 * If custom login not available it will be fetched from DB
	 *
	 * @return string
	 */
	public function getUserLogin()
	{
		if ($this->userLogin === null && $this->userId)
		{
			$this->userLogin = \Bitrix\Main\UserTable::query()
				->addFilter('=ID', $this->getUserId())
				->addSelect('LOGIN')
				->exec()
				->fetch();

			$this->userLogin = $this->userLogin['LOGIN'];
		}

		return $this->userLogin;
	}

	/**
	 * Return default issuer
	 *
	 * @return string
	 */
	protected function getDefaultIssuer()
	{
		$host = Option::get('main', 'server_name');
		if($host)
		{
			return preg_replace('#:\d+$#D', '', $host);
		}
		else
		{
			return Option::get('security', 'otp_issuer', 'Bitrix');
		}
	}

	/**
	 * Generate label, based on current host, user login and issuer (if provided)
	 *
	 * @param string|null $issuer Issuer.
	 * @return string
	 */
	protected function generateLabel($issuer = null)
	{
		if ($issuer)
			return sprintf('%s:%s', $issuer, $this->getUserLogin());
		else
			return $this->getUserLogin();
	}

	/**
	 * Return maximum verifying attempts, based on security group policy
	 *
	 * @return int
	 */
	protected function getMaxLoginAttempts()
	{
		if (!$this->isActivated())
			return 0;

		return (int) $this->getPolicy()->getLoginAttempts();
	}

	/**
	 * Return how long (in sec)remember value are valid
	 *
	 * @return int
	 */
	protected function getRememberLifetime()
	{
		if (!$this->isActivated())
			return 0;

		return ((int) $this->getPolicy()->getStoreTimeout()) * 60;
	}

	/**
	 * Return IP mask for checks remember value
	 *
	 * @return string
	 */
	protected function getRememberIpMask()
	{
		if (!$this->isActivated())
			return '255.255.255.255';

		return $this->getPolicy()->getStoreIpMask();
	}

	/**
	 * Check if current user can skip OTP mandatory using.
	 * It can skip if:
	 *  - Otp already activated
	 *  - User never login before
	 *  - User not included to mandatory rights
	 *  - The current date is included in the window initialization
	 *
	 * @return bool
	 */
	public function canSkipMandatory()
	{
		$result = $this->isMandatorySkipped();

		if (!$result)
		{
			// Check mandatory rights
			$result = $this->canSkipMandatoryByRights();
		}

		return $result;
	}

	/**
	 * Check if current user not included to mandatory rights
	 *
	 * @return bool
	 */
	public function canSkipMandatoryByRights()
	{
		$targetRights = static::getMandatoryRights();
		$userRights = \CAccess::getUserCodesArray($this->getUserId());
		$existedRights = array_intersect($targetRights, $userRights);
		$result = empty($existedRights);

		return $result;
	}

	/**
	 * Check if user have valid cookie for skip OTP checking ("Remember OTP on this computer")
	 *
	 * @return bool
	 */
	protected function canSkipByCookie()
	{
		if (Option::get('security', 'otp_allow_remember') !== 'Y')
			return false;

		$signedValue = $this->getContext()->getRequest()->getCookie(static::SKIP_COOKIE);

		if (!$signedValue || !is_string($signedValue))
			return false;

		try
		{
			$signer = new TimeSigner();
			$value = $signer
				->setKey($this->getSecret())
				->unsign($signedValue, 'MFA_SAVE');
		}
		catch (BadSignatureException)
		{
			return false;
		}

		return ($value === $this->getSkipCookieValue());
	}

	/**
	 * Generate skip value for save in cookies
	 * Currently based on client IP and mask (see getRememberIpMask)
	 *
	 * @return string
	 */
	protected function getSkipCookieValue()
	{
		// ToDo: must be tied to the ID of "computer" when it will appear in the main module
		$rememberMask = $this->getRememberIpMask();
		$userIp = $this->getContext()->getRequest()->getRemoteAddress();
		return md5(ip2long($rememberMask) & ip2long($userIp));
	}

	/**
	 * Store new value for skip OTP checking ("Remember OTP on this computer") in cookies
	 *
	 * @return $this
	 */
	protected function setSkipCookie()
	{
		/** @global \CMain $APPLICATION */
		global $APPLICATION;

		$signer = new TimeSigner();
		$rememberLifetime = $this->getRememberLifetime();
		$rememberLifetime += time();
		$rememberValue = $this->getSkipCookieValue();

		$signedValue = $signer
			->setKey($this->getSecret())
			->sign($rememberValue, $rememberLifetime, 'MFA_SAVE');

		$isSecure = (
			Option::get('main', 'use_secure_password_cookies', 'N') === 'Y'
			&& $this->getContext()->getRequest()->isHttps()
		);

		$APPLICATION->set_cookie(
			static::SKIP_COOKIE, // $name
			$signedValue,        // $value
			$rememberLifetime,   // $time = false
			'/',                 // $folder = "/"
			false,               // $domain = false
			$isSecure,           // $secure = false
			true,                // $spread = true
			false,               // $name_prefix = false
			true                 // $httpOnly = false
		);

		return $this;
	}

	/**
	 * Check if OTP record exists in DB
	 *
	 * @return bool
	 */
	protected function isDbRecordExists()
	{
		return UserTable::getRowById($this->getUserId()) !== null;
	}

	/**
	 * Return needed group security policy
	 *
	 * @return Policy\RulesCollection
	 */
	protected function getPolicy()
	{
		if (!$this->userGroupPolicy)
		{
			$this->userGroupPolicy = \CUser::getPolicy($this->getUserId());
		}

		return $this->userGroupPolicy;
	}

	/**
	 * Clear cache for this OTP in global scope
	 *
	 * @return $this
	 */
	protected function clearGlobalCache()
	{
		$cache_dir = '/otp/user_id/' . substr(md5($this->getUserId()), -2) . '/' . $this->getUserId() . '/';
		$cache = new \CPHPCache;
		$cache->CleanDir($cache_dir);

		return $this;
	}

	/**
	 * Most complex method, can check everything:-)
	 * ToDo: describe after refactoring
	 *
	 * @param array $params Event parameters.
	 * @return bool
	 */
	public static function verifyUser(array $params)
	{
		global $APPLICATION;

		if (!static::isOtpEnabled()) // OTP disabled in settings
			return true;

		$isSuccess = false;

		// ToDo: review and refactoring needed
		$otp = static::getByUser($params['USER_ID']);

		if (!$otp->isActivated())
		{
			// User does not use OTP
			$isSuccess = true;

			if (
				static::isMandatoryUsing()
				&& !$otp->canSkipMandatory()
			)
			{
				// Grace full period ends. We must reject authorization and defer reject reason
				if (!$otp->isDbRecordExists() && static::getSkipMandatoryDays())
				{
					// If mandatory enabled and user never use OTP - let us deffer initialization
					$otp->defer(static::getSkipMandatoryDays());

					// We forgive the user for the first time
					static::setDeferredParams(null);
					return true;
				}

				// Save a flag which indicates that an OTP is required, but user doesn't use it :-(
				$params[static::REJECTED_KEY] = static::REJECT_BY_MANDATORY;
				static::setDeferredParams($params);
				return false;
			}
		}
		else
		{
			if (!$otp->isUserActive())
			{
				//non-active user can't log in by OTP
				return false;
			}
		}


		if (!$isSuccess)
		{
			// User skip OTP on this browser by cookie
			$isSuccess = $otp->canSkipByCookie();
		}

		if (!$isSuccess)
		{
			$isCaptchaChecked = (
				!$otp->isAttemptsReached()
				|| $APPLICATION->captchaCheckCode($params['CAPTCHA_WORD'], $params['CAPTCHA_SID'])
			);
			$isRememberNeeded = (
				$params['OTP_REMEMBER']
				&& Option::get('security', 'otp_allow_remember') === 'Y'
			);

			if (!$isCaptchaChecked && !$APPLICATION->NeedCAPTHA())
			{
				// Backward compatibility with old login page
				$APPLICATION->SetNeedCAPTHA(true);
			}

			$isOtpPassword = (bool) preg_match('/^\d{6}$/D', $params['OTP']);
			$isRecoveryCode = (
				static::isRecoveryCodesEnabled()
				&& preg_match(RecoveryCodesTable::CODE_PATTERN, $params['OTP'])
			);

			if ($isCaptchaChecked && ($isOtpPassword || $isRecoveryCode))
			{
				if ($isOtpPassword)
					$isSuccess = $otp->verify($params['OTP']);
				elseif ($isRecoveryCode)
					$isSuccess = RecoveryCodesTable::useCode($otp->getUserId(), $params['OTP']);
				else
					$isSuccess = false;

				if (!$isSuccess)
				{
					$otp
						->setAttempts($otp->getAttempts() + 1)
						->save();
				}
				else
				{
					if ($otp->getAttempts() > 0)
					{
						// Clear OTP input attempts
						$otp
							->setAttempts(0)
							->save();
					}

					if ($isRememberNeeded && $isOtpPassword)
					{
						// If user provide otp password (not recovery codes)
						// Sets cookie for bypass OTP checking
						$otp->setSkipCookie();
					}
				}
			}
		}

		if ($isSuccess)
		{
			static::setDeferredParams(null);
		}
		else
		{
			// Save a flag which indicates that a form for OTP is required
			$params[static::REJECTED_KEY] = static::REJECT_BY_CODE;
			static::setDeferredParams($params);

			//the OTP form will be shown on the next hit, send the event
			static::sendEvent($otp);

			//write to the log ("on" by default)
			if(Option::get("security", "otp_log") <> "N")
			{
				\CSecurityEvent::getInstance()->doLog("SECURITY", "SECURITY_OTP", $otp->getUserId(), "");
			}
		}

		return $isSuccess;
	}

	protected static function sendEvent(Otp $otp)
	{
		$code = null;
		$algo = $otp->getAlgorithm();

		//code value only for TOTP
		if($algo instanceof \Bitrix\Main\Security\Mfa\TotpAlgorithm)
		{
			//value based on the current time
			$timeCode = $algo->timecode(time());
			$code = $algo->generateOTP($timeCode);
		}

		$eventParams = [
			"userId" => $otp->getUserId(),
			"code" => $code,
		];

		$event = new \Bitrix\Main\Event("security", "onOtpRequired", $eventParams);
		$event->send();
	}

	/**
	 * Returns true if user must provide password from device
	 *
	 * @return bool
	 */
	public static function isOtpRequired()
	{
		return static::getDeferredParams() !== null;
	}

	/**
	 * Returns true if user doesn't use OTP, but it required and grace full period ends
	 *
	 * @return bool
	 */
	public static function isOtpRequiredByMandatory()
	{
		$params = static::getDeferredParams();
		if (
			!$params
			|| !isset($params[static::REJECTED_KEY])
		)
		{
			return false;
		}

		return $params[static::REJECTED_KEY] === static::REJECT_BY_MANDATORY;
	}

	/**
	 * Return if user must provide captcha code before checking OTP password
	 *
	 * @return bool
	 */
	public static function isCaptchaRequired()
	{
		$params = static::getDeferredParams();

		if (!$params || !isset($params['USER_ID']))
			return false;

		$otp = static::getByUser($params['USER_ID']);
		return $otp && $otp->isAttemptsReached();
	}

	/**
	 * Return deferred params (see verifyUser)
	 *
	 * @return array|null
	 */
	public static function getDeferredParams()
	{
		$kernelSession = Application::getInstance()->getKernelSession();
		if (isset($kernelSession['BX_SECURITY_OTP']) && is_array($kernelSession['BX_SECURITY_OTP']))
		{
			return $kernelSession['BX_SECURITY_OTP'];
		}

		return null;
	}

	/**
	 * Set or delete deferred params (see verifyUser)
	 *
	 * @param array|null $params Params, null means deleting params from storage.
	 * @return void
	 */
	public static function setDeferredParams($params)
	{
		$kernelSession = Application::getInstance()->getKernelSession();
		if ($params === null)
		{
			unset($kernelSession['BX_SECURITY_OTP']);
		}
		else
		{
			// Probably we do not need saving password in deferred params
			// Or need? I don't know right now...
			if (isset($params['PASSWORD']))
				unset($params['PASSWORD']);

			$kernelSession['BX_SECURITY_OTP'] = $params;
		}
	}

	/**
	 * Set initialization window (in days) for mandatory using checking
	 *
	 * @param int $days Days of initialization window. "0" means immediately (on next user authorization).
	 * @return void
	 */
	public static function setSkipMandatoryDays($days = 2)
	{
		Option::set('security', 'otp_mandatory_skip_days', (int) $days, null);
	}

	/**
	 * Return initialization window (in days) for mandatory using checking
	 *
	 * @return int
	 */
	public static function getSkipMandatoryDays()
	{
		return (int) Option::get('security', 'otp_mandatory_skip_days');
	}

	/**
	 * Activate or deactivate mandatory OTP using
	 *
	 * @param bool $isMandatory Active or not.
	 * @return void
	 */
	public static function setMandatoryUsing($isMandatory = true)
	{
		Option::set('security', 'otp_mandatory_using', $isMandatory? 'Y': 'N', null);
	}

	/**
	 * Return is mandatory OTP using activated
	 *
	 * @return bool
	 */
	public static function isMandatoryUsing()
	{
		return (Option::get('security', 'otp_mandatory_using') === 'Y');
	}

	/**
	 * Set user rights who must use OTP in mandatory way
	 *
	 * @param array $rights Needed rights. E.g. ['G1'] for administrators.
	 * @return void
	 */
	public static function setMandatoryRights(array $rights)
	{
		Option::set('security', 'otp_mandatory_rights', serialize($rights), null);
	}

	/**
	 * Return user rights who must use OTP in mandatory way
	 *
	 * @return array
	 */
	public static function getMandatoryRights()
	{
		$targetRights = Option::get('security', 'otp_mandatory_rights');
		$targetRights = unserialize($targetRights, ['allowed_classes' => false]);
		if (!is_array($targetRights))
			$targetRights = array();

		return $targetRights;
	}

	/**
	 * Set default OtpAlgorithm type
	 *
	 * @param string $value OtpAlgorithm type (see getAvailableTypes).
	 * @throws ArgumentOutOfRangeException
	 * @return void
	 */
	public static function setDefaultType($value)
	{
		if (!in_array($value, static::$availableTypes))
			throw new ArgumentOutOfRangeException('value', static::$availableTypes);

		Option::set('security', 'otp_default_algo', $value, null);
	}


	/**
	 * Return default OtpAlgorithm type
	 *
	 * @return string
	 */
	public static function getDefaultType()
	{
		return Option::get('security', 'otp_default_algo');
	}

	/**
	 * Return available OtpAlgorithm types
	 *
	 * @return array
	 */
	public static function getAvailableTypes()
	{
		return static::$availableTypes;
	}

	/**
	 * Return available OtpAlgorithm types description
	 *
	 * @return array
	 */
	public static function getTypesDescription()
	{
		return array(
			self::TYPE_HOTP => array(
				'type' => self::TYPE_HOTP,
				'title' => Loc::getMessage('SECURITY_HOTP_TITLE'),
				'required_two_code' => true,
			),
			self::TYPE_TOTP => array(
				'type' => self::TYPE_TOTP,
				'title' => Loc::getMessage('SECURITY_TOTP_TITLE'),
				'required_two_code' => false,
			)
		);
	}

	/**
	 * Returns if OTP enabled
	 *
	 * @return bool
	 */
	public static function isOtpEnabled()
	{
		return (Option::get('security', 'otp_enabled') === 'Y');
	}

	/**
	 * Returns if "Recovery codes" are enabled
	 *
	 * @return bool
	 */
	public static function isRecoveryCodesEnabled()
	{
		return (Option::get('security', 'otp_allow_recovery_codes') === 'Y');
	}
}