75 lines
2.5 KiB
PHP
75 lines
2.5 KiB
PHP
<?php
|
|
namespace Hanyuu\OTP;
|
|
|
|
use InvalidArgumentException;
|
|
use Index\Serialisation\Serialiser;
|
|
|
|
class TOTPGenerator implements IOTPGenerator {
|
|
private const DIGITS = 6;
|
|
private const INTERVAL = 30;
|
|
private const HASH_ALGO = 'sha1';
|
|
|
|
public function __construct(
|
|
private string $secretKey,
|
|
private int $digits = self::DIGITS,
|
|
private int $interval = self::INTERVAL,
|
|
private string $hashAlgo = self::HASH_ALGO
|
|
) {
|
|
if(empty($this->secretKey))
|
|
throw new InvalidArgumentException('$secretKey may not be empty');
|
|
if($this->digits < 1)
|
|
throw new InvalidArgumentException('$digits must be a positive integer');
|
|
if($this->interval < 1)
|
|
throw new InvalidArgumentException('$interval must be a positive integer');
|
|
if(!in_array($this->hashAlgo, hash_hmac_algos(), true))
|
|
throw new InvalidArgumentException('$hashAlgo must be a hashing algorithm suitable for hmac');
|
|
}
|
|
|
|
public function getDigits(): int {
|
|
return $this->digits;
|
|
}
|
|
|
|
public function getInterval(): int {
|
|
return $this->interval;
|
|
}
|
|
|
|
public function getHashAlgo(): string {
|
|
return $this->hashAlgo;
|
|
}
|
|
|
|
public function getTimeCode(int $offset = 0, int $timeStamp = -1): int {
|
|
if($timeStamp < 0)
|
|
$timeStamp = time();
|
|
|
|
// use -1 and 1 to get the previous and next token for user convenience
|
|
if($offset !== 0)
|
|
$timeStamp += $this->interval * $offset;
|
|
|
|
return (int)(($timeStamp * 1000) / ($this->interval * 1000));
|
|
}
|
|
|
|
public function generate(int $offset = 0, int $timeStamp = -1): string {
|
|
$timeCode = pack('J', $this->getTimeCode($offset, $timeStamp));
|
|
$secretKey = Serialiser::base32()->deserialise($this->secretKey);
|
|
$hash = hash_hmac($this->hashAlgo, $timeCode, $secretKey, true);
|
|
|
|
$offset = ord($hash[strlen($hash) - 1]) & 0x0F;
|
|
|
|
$bin = (ord($hash[$offset]) & 0x7F) << 24;
|
|
$bin |= (ord($hash[$offset + 1]) & 0xFF) << 16;
|
|
$bin |= (ord($hash[$offset + 2]) & 0xFF) << 8;
|
|
$bin |= ord($hash[$offset + 3]) & 0xFF;
|
|
|
|
$otp = (string)($bin % pow(10, $this->digits));
|
|
|
|
return str_pad($otp, $this->digits, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
public static function generateKey(int $bytes = 16): string {
|
|
if($length < 1)
|
|
throw new InvalidArgumentException('$bytes must be a positive integer');
|
|
|
|
return Serialiser::base32()->serialise(random_bytes($bytes));
|
|
}
|
|
}
|