HOTP.php 2.9 KB
<?php

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2018 Spomky-Labs
 *
 * This software may be modified and distributed under the terms
 * of the MIT license.  See the LICENSE file for details.
 */

namespace OTPHP;

use Assert\Assertion;

final class HOTP extends OTP implements HOTPInterface
{
    /**
     * HOTP constructor.
     *
     * @param string|null $label
     * @param string|null $secret
     * @param int         $counter
     * @param string      $digest
     * @param int         $digits
     */
    public function __construct($label = null, $secret = null, $counter = 0, $digest = 'sha1', $digits = 6)
    {
        parent::__construct($label, $secret, $digest, $digits);
        $this->setCounter($counter);
    }

    /**
     * @param int $counter
     */
    private function setCounter($counter)
    {
        Assertion::integer($counter, 'Counter must be at least 0.');
        Assertion::greaterOrEqualThan($counter, 0, 'Counter must be at least 0.');

        $this->setParameter('counter', $counter);
    }

    /**
     * {@inheritdoc}
     */
    public function getCounter()
    {
        return $this->getParameter('counter');
    }

    /**
     * @param int $counter
     */
    private function updateCounter($counter)
    {
        $this->setCounter($counter);
    }

    /**
     * {@inheritdoc}
     */
    public function getProvisioningUri()
    {
        return $this->generateURI('hotp', ['counter' => $this->getCounter()]);
    }

    /**
     * If the counter is not provided, the OTP is verified at the actual counter.
     *
     * {@inheritdoc}
     */
    public function verify($otp, $counter = null, $window = null)
    {
        Assertion::string($otp, 'The OTP must be a string');
        Assertion::nullOrInteger($counter, 'The counter must be null or an integer');
        Assertion::greaterOrEqualThan($counter, 0, 'The counter must be at least 0.');
        Assertion::nullOrInteger($window, 'The window parameter must be null or an integer');

        if (null === $counter) {
            $counter = $this->getCounter();
        } elseif ($counter < $this->getCounter()) {
            return false;
        }

        return $this->verifyOtpWithWindow($otp, $counter, $window);
    }

    /**
     * @param null|int $window
     *
     * @return int
     */
    private function getWindow($window)
    {
        if (null === $window) {
            $window = 0;
        }

        return abs($window);
    }

    /**
     * @param string $otp
     * @param int    $counter
     * @param int    $window
     *
     * @return bool
     */
    private function verifyOtpWithWindow($otp, $counter, $window)
    {
        $window = $this->getWindow($window);

        for ($i = $counter; $i <= $counter + $window; $i++) {
            if ($this->compareOTP($this->at($i), $otp)) {
                $this->updateCounter($i + 1);

                return true;
            }
        }

        return false;
    }
}