aiwass/src/HmacVerificationProvider.php

121 lines
4.2 KiB
PHP
Raw Normal View History

2024-08-13 00:54:50 +00:00
<?php
// HmacVerificationProvider.php
// Created: 2024-08-13
// Updated: 2024-08-13
namespace Aiwass;
use InvalidArgumentException;
use Index\UriBase64;
/**
* Implements an HMAC verification provider.
*/
class HmacVerificationProvider implements IVerificationProvider {
/**
* Default list of allowed algorithms.
*
* @var string[]
*/
public const DEFAULT_ALGOS = ['sha256'];
/**
* Amount of time that is added and subtracted from a user timestamp to check if its still valid.
*
* Meaning that if this value is 30, the timestamp the user provided will be considered valid if it is within 30 seconds into the past and 30 seconds into the future.
*
* @var int
*/
public const DEFAULT_TIME_VALID_PLUS_MINUS_SECONDS = 30;
/**
* @param callable(): string $getSecretKey A method that returns the secret key to use.
* @param string[] $algos List of algorithms to allow, first entry will be used by new tokens.
* @param int $timeValidPlusMinusSeconds Amount of time for which the token will be valid in the future and the past.
* @throws InvalidArgumentException If $getSecretKey is not a callable type.
* @throws InvalidArgumentException If $algos is an empty array.
* @throws InvalidArgumentException If $timeValidPlusMinusSeconds is less than 1.
*/
public function __construct(
private $getSecretKey,
private array $algos = self::DEFAULT_ALGOS,
private int $timeValidPlusMinusSeconds = self::DEFAULT_TIME_VALID_PLUS_MINUS_SECONDS
) {
if(!is_callable($getSecretKey))
throw new InvalidArgumentException('$getSecretKey must be a callable');
if(empty($algos))
throw new InvalidArgumentException('$algos must contain at least one entry');
if($timeValidPlusMinusSeconds < 1)
throw new InvalidArgumentException('$timeValidPlusMinusSeconds must be greater than zero');
}
public function sign(bool $isProcedure, string $action, string $paramString): string {
return $this->signWith($this->algos[0], $isProcedure, $action, $paramString);
}
private function signWith(string $algo, bool $isProcedure, string $action, string $paramString): string {
$time = time();
$hash = $this->createHash($algo, $time, $isProcedure, $action, $paramString);
return UriBase64::encode(AiwassMsgPack::encode([
'a' => $algo,
't' => $time,
'h' => $hash,
]));
}
private function createHash(
string $algo,
int $timeStamp,
bool $isProcedure,
string $action,
string $paramString
): string {
$data = sprintf(
'^<=>%d<=>%s<=>%s<=>%s<=>$',
$timeStamp,
$isProcedure ? 'p' : 'q',
$action,
$paramString
);
return hash_hmac($algo, $data, ($this->getSecretKey)(), true);
}
/**
* Checks if a given hashing algorithm is allowed by this verification provider.
*
* @param string $algo Algorithm name.
* @return bool true if it is supported.
*/
public function isValidAlgo(string $algo): bool {
return in_array($algo, $this->algos);
}
public function verify(string $userToken, bool $isProcedure, string $action, string $paramString): bool {
$userToken = UriBase64::decode($userToken);
if($userToken === false)
return false;
$userTokenInfo = AiwassMsgPack::decode($userToken);
if(!is_array($userTokenInfo))
return false;
$userAlgo = $userTokenInfo['a'] ?? null;
if(!is_string($userAlgo) || !$this->isValidAlgo($userAlgo))
return false;
$userTime = $userTokenInfo['t'] ?? null;
$currentTime = time();
if(!is_int($userTime) || $userTime < ($currentTime - $this->timeValidPlusMinusSeconds) || $userTime > ($currentTime + $this->timeValidPlusMinusSeconds))
return false;
$userHash = $userTokenInfo['h'] ?? null;
if(!is_string($userHash))
return false;
$realHash = $this->createHash($userAlgo, $userTime, $isProcedure, $action, $paramString);
return hash_equals($realHash, $userHash);
}
}