forkbb/app/Core/Csrf.php

145 lines
4.1 KiB
PHP

<?php
/**
* This file is part of the ForkBB <https://github.com/forkbb>.
*
* @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
* @license The MIT License (MIT)
*/
declare(strict_types=1);
namespace ForkBB\Core;
use ForkBB\Core\Secury;
use SensitiveParameter;
class Csrf
{
const TOKEN_LIFETIME = 1800;
protected ?string $error = null;
protected int $hashExpiration = 3600;
public function __construct(
protected Secury $secury,
#[SensitiveParameter] protected string $key,
#[SensitiveParameter] protected mixed $externalSalt // сюда и Container может попасть O_o
) {
}
/**
* Устанавливает срок жизни хэша
*/
public function setHashExpiration(int $exp): void
{
$this->hashExpiration = $exp > 0 ? $exp : 3600;
}
/**
* Возвращает csrf токен
*/
public function create(string $marker, array $args = [], int|string $time = null, string $type = 's'): string
{
$marker = $this->argsToStr($marker, $args);
$time = $time ?: \time();
switch ($type) {
case 's':
return $this->secury->hmac($marker, $time . $this->key) . 's' . $time;
case 'x':
if (
\is_string($this->externalSalt)
&& isset($this->externalSalt[9])
) {
return \hash_hmac('sha1', $marker, $time . $this->externalSalt . $_SERVER['REMOTE_ADDR']) . 'x' . $time;
}
default:
return 'n';
}
}
/**
* Возвращает хэш
*/
public function createHash(string $marker, array $args = [], int|string $time = null): string
{
$marker = $this->argsToStr($marker, $args, ['hash']);
$time = $time ?: \time() + $this->hashExpiration;
return $this->secury->hash($marker . $time) . 'e' . $time;
}
protected function argsToStr(string $marker, array $args, array $forDel = []): string
{
$marker .= '|';
if (! empty($forDel)) {
$args = \array_diff_key($args, \array_flip($forDel));
}
unset($args['token'], $args['#']);
\ksort($args);
foreach ($args as $key => $value) {
if (null !== $value) {
$marker .= "{$key}|{$value}|";
}
}
return $marker;
}
/**
* Проверка токена/хэша
*/
public function verify($token, string $marker, array $args = [], int $lifetime = null): bool
{
$this->error = 'Bad token';
$now = \time();
$result = false;
if (
\is_string($token)
&& \preg_match('%(e|s|x)(\d+)$%D', $token, $matches)
) {
switch ($matches[1]) {
// токен
case 's':
case 'x':
if ($matches[2] + ($lifetime ?? self::TOKEN_LIFETIME) < $now) {
// просрочен
$this->error = 'Expired token';
} elseif (
$matches[2] + 0 <= $now
&& \hash_equals($this->create($marker, $args, $matches[2], $matches[1]), $token)
) {
$this->error = null;
$result = true;
}
break;
// хэш
case 'e':
if ($matches[2] + 0 < $now) {
// просрочен
$this->error = 'Expired token';
} elseif (\hash_equals($this->createHash($marker, $args, $matches[2]), $token)) {
$this->error = null;
$result = true;
}
break;
}
}
return $result;
}
/**
* Возвращает ошибку из метода verify
*/
public function getError(): ?string
{
return $this->error;
}
}