hanyuu/src/OTP/TOTPGenerator.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));
}
}