index/src/Security/CSRFP.php
flash 5150f0cb56 Allow including Index through Composer.
index.php automatically detects if the Composer autoloader exists and doesn't initialise its own autoloader.
Also including phpunit and phpstan through it instead of including the phars, probably makes more sense to do it the other way but they're Big.
2023-07-21 18:04:29 +00:00

105 lines
3.1 KiB
PHP

<?php
// CSRFP.php
// Created: 2021-06-11
// Updated: 2023-07-21
namespace Index\Security;
use Index\Serialisation\Serialiser;
/**
* Contains a mechanism for validating requests.
*/
class CSRFP {
private const TOLERANCE = 30 * 60;
private const EPOCH = 1575158400;
private const HASH_ALGO = 'sha3-256';
private const TIMESTAMP_LENGTH = 4;
private const NONCE_LENGTH = 8;
private const HASH_LENGTH = 32;
private string $secretKey;
private string $identity;
private int $tolerance;
/**
* Creates a new CSRFP instance.
*
* @param string $secretKey Secret key for HMAC hashes.
* @param string $identity Unique identity component for these tokens.
* @param int $tolerance Default amount of time a token should remain valid for.
* @return CSRFP New CSRFP instance.
*/
public function __construct(
string $secretKey,
string $identity,
int $tolerance = self::TOLERANCE
) {
$this->secretKey = $secretKey;
$this->identity = $identity;
$this->tolerance = $tolerance;
}
private static function time(int $time = -1): int {
return ($time < 0 ? time() : $time) - self::EPOCH;
}
private function createHash(int $time, string $nonce): string {
return hash_hmac(self::HASH_ALGO, "{$this->identity}!{$time}!{$nonce}", $this->secretKey, true);
}
/**
* Creates a token string.
*
* @param int $time Timestamp to generate the token for, -1 (default) for now.
* @return string Token string.
*/
public function createToken(int $time = -1): string {
$time = self::time($time);
$nonce = random_bytes(self::NONCE_LENGTH);
$hash = $this->createHash($time, $nonce);
return Serialiser::uriBase64()->serialise(
pack('V', $time) . $nonce . $hash
);
}
/**
* Verifies a token string.
*
* @param string $token Token to test.
* @param int $tolerance Amount of seconds for which the token can remain valid, < 0 for whatever the default value is.
* @param int $time Point in time for which to check validity for this time.
* @return bool true if the token is valid, false if not.
*/
public function verifyToken(string $token, int $tolerance = -1, int $time = -1): bool {
if($tolerance === 0 || empty($token))
return false;
if($tolerance < 0)
$tolerance = $this->tolerance;
$token = Serialiser::uriBase64()->deserialise($token);
if(empty($token))
return false;
$unpacked = unpack('Vts', $token);
$uTime = (int)$unpacked['ts'];
if($uTime < 0)
return false;
$uNonce = substr($token, self::TIMESTAMP_LENGTH, self::NONCE_LENGTH);
if(empty($uNonce))
return false;
$uHash = substr($token, self::TIMESTAMP_LENGTH + self::NONCE_LENGTH, self::HASH_LENGTH);
if(empty($uHash))
return false;
$rTime = self::time($time);
return ($rTime - ($uTime + $tolerance)) < 0
&& hash_equals($this->createHash($uTime, $uNonce), $uHash);
}
}