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)); } }