قرینه از
https://github.com/matomo-org/matomo.git
synced 2025-08-21 22:47:43 +00:00

* Use #[\SensitiveParameter] php attribute to hide sensitive parameters from strack traces * also hide database config parameters * also hide 2fa codes and secrets * hide some more parameters * add attribute to remaining sensitive parameters * update matomo-org/matomo-coding-standards
225 خطوط
6.2 KiB
PHP
225 خطوط
6.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Matomo - free/libre analytics platform
|
|
*
|
|
* @link https://matomo.org
|
|
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*/
|
|
|
|
namespace Piwik\Plugins\TwoFactorAuth;
|
|
|
|
use Piwik\Common;
|
|
use Piwik\Db;
|
|
use Piwik\Option;
|
|
use Piwik\Piwik;
|
|
use Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeDao;
|
|
use Piwik\Plugins\TwoFactorAuth\Dao\TwoFaSecretRandomGenerator;
|
|
use Piwik\Plugins\UsersManager\Model;
|
|
use Exception;
|
|
use Piwik\SettingsPiwik;
|
|
|
|
require_once PIWIK_DOCUMENT_ROOT . '/libs/Authenticator/TwoFactorAuthenticator.php';
|
|
|
|
class TwoFactorAuthentication
|
|
{
|
|
public const OPTION_PREFIX_TWO_FA_CODE_USED = 'twofa_codes_used_';
|
|
|
|
/**
|
|
* Make sure the same fa code was not used in the last X minutes.
|
|
* Technically, even 2 minutes be fine since every token is only valid for 30 sec and we only allow the 2 most
|
|
* recent tokens.
|
|
*/
|
|
public const BLOCK_TWOFA_CODE_MINUTES = 10;
|
|
|
|
/**
|
|
* @var SystemSettings
|
|
*/
|
|
private $settings;
|
|
|
|
/**
|
|
* @var RecoveryCodeDao
|
|
*/
|
|
private $recoveryCodeDao;
|
|
|
|
/**
|
|
* @var TwoFaSecretRandomGenerator
|
|
*/
|
|
private $secretGenerator;
|
|
|
|
public function __construct(SystemSettings $systemSettings, RecoveryCodeDao $recoveryCodeDao, TwoFaSecretRandomGenerator $twoFaSecretRandomGenerator)
|
|
{
|
|
$this->settings = $systemSettings;
|
|
$this->recoveryCodeDao = $recoveryCodeDao;
|
|
$this->secretGenerator = $twoFaSecretRandomGenerator;
|
|
}
|
|
|
|
private static function getUserModel()
|
|
{
|
|
return new Model();
|
|
}
|
|
|
|
public function generateSecret()
|
|
{
|
|
return $this->secretGenerator->generateSecret();
|
|
}
|
|
|
|
public function disable2FAforUser($login)
|
|
{
|
|
$this->saveSecret($login, '');
|
|
$this->recoveryCodeDao->deleteAllRecoveryCodesForLogin($login);
|
|
|
|
Piwik::postEvent('TwoFactorAuth.disabled', array($login));
|
|
}
|
|
|
|
private static function isAnonymous($login)
|
|
{
|
|
return strtolower($login) === 'anonymous';
|
|
}
|
|
|
|
public function saveSecret(
|
|
$login,
|
|
#[\SensitiveParameter]
|
|
$secret
|
|
) {
|
|
if (self::isAnonymous($login)) {
|
|
throw new Exception('Anonymous cannot use two-factor authentication');
|
|
}
|
|
|
|
if (!empty($secret) && !$this->recoveryCodeDao->getAllRecoveryCodesForLogin($login)) {
|
|
// ensures the user has seen and ideally backuped the recovery codes... we don't create them here on demand
|
|
throw new Exception('Cannot enable two-factor authentication, no recovery codes have been created');
|
|
}
|
|
|
|
$model = self::getUserModel();
|
|
$model->updateUserFields($login, array('twofactor_secret' => $secret));
|
|
}
|
|
|
|
public function isUserRequiredToHaveTwoFactorEnabled()
|
|
{
|
|
return $this->settings->twoFactorAuthRequired->getValue();
|
|
}
|
|
|
|
public static function isUserUsingTwoFactorAuthentication($login)
|
|
{
|
|
if (self::isAnonymous($login)) {
|
|
return false; // not possible to use auth code with anonymous
|
|
}
|
|
|
|
$user = self::getUser($login);
|
|
return !empty($user['twofactor_secret']);
|
|
}
|
|
|
|
private static function getUser($login)
|
|
{
|
|
$model = self::getUserModel();
|
|
return $model->getUser($login);
|
|
}
|
|
|
|
private function wasTwoFaCodeUsedRecently(
|
|
$login,
|
|
#[\SensitiveParameter]
|
|
$authCode
|
|
) {
|
|
$time = Option::get($this->gettwoFaCodeUsedKey($login, $authCode));
|
|
if (empty($time)) {
|
|
return false;
|
|
}
|
|
$fiveMinutes = 60 * self::BLOCK_TWOFA_CODE_MINUTES;
|
|
if (time() - $fiveMinutes >= (int)$time) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function gettwoFaCodeUsedKey(
|
|
$login,
|
|
#[\SensitiveParameter]
|
|
$authCode
|
|
) {
|
|
return self::OPTION_PREFIX_TWO_FA_CODE_USED . md5($login . $authCode . SettingsPiwik::getSalt());
|
|
}
|
|
|
|
private function setTwoFaCodeWasUsed(
|
|
$login,
|
|
#[\SensitiveParameter]
|
|
$authCode
|
|
) {
|
|
$table = Common::prefixTable('option');
|
|
$bind = array($this->gettwoFaCodeUsedKey($login, $authCode), time(), 0);
|
|
try {
|
|
Db::query('INSERT INTO `' . $table . '` (option_name, option_value, autoload) VALUES (?, ?, ?) ', $bind);
|
|
return true;
|
|
} catch (Exception $e) {
|
|
// when 2 process try to insert at same time should result in duplicate error
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function cleanupTwoFaCodesUsedRecently()
|
|
{
|
|
$values = Option::getLike(TwoFactorAuthentication::OPTION_PREFIX_TWO_FA_CODE_USED . '%');
|
|
if (!empty($values)) {
|
|
foreach ($values as $optionName => $timeCodeWasUsed) {
|
|
$fiveMinutesAgo = time() - (60 * self::BLOCK_TWOFA_CODE_MINUTES);
|
|
if ($timeCodeWasUsed < $fiveMinutesAgo) {
|
|
// delete any entry created more than 5 min ago
|
|
Option::delete($optionName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function validateAuthCode(
|
|
$login,
|
|
#[\SensitiveParameter]
|
|
$authCode
|
|
) {
|
|
if (!self::isUserUsingTwoFactorAuthentication($login)) {
|
|
return false;
|
|
}
|
|
|
|
$user = self::getUser($login);
|
|
|
|
if ($this->wasTwoFaCodeUsedRecently($user['login'], $authCode)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$this->setTwoFaCodeWasUsed($user['login'], $authCode)) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
!empty($user['twofactor_secret'])
|
|
&& $this->validateAuthCodeDuringSetup($authCode, $user['twofactor_secret'])
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->recoveryCodeDao->useRecoveryCode($user['login'], $authCode)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function validateAuthCodeDuringSetup(
|
|
#[\SensitiveParameter]
|
|
$authCode,
|
|
#[\SensitiveParameter]
|
|
$secret
|
|
) {
|
|
$twoFactorAuth = $this->makeAuthenticator();
|
|
|
|
if (!empty($secret) && $twoFactorAuth->verifyCode($secret, $authCode, 2)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function makeAuthenticator()
|
|
{
|
|
return new \TwoFactorAuthenticator();
|
|
}
|
|
}
|