index/src/Security/CSRFP.php
2023-07-21 21:47:41 +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\UriBase64;
/**
* 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 UriBase64::encode(
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 = UriBase64::decode($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);
}
}