sam002/yii2-otp

View on GitHub
src/Otp.php

Summary

Maintainability
A
45 mins
Test Coverage
<?php
/**
 * Author: Semen Dubina
 * Date: 19.01.16
 * Time: 15:24
 */

namespace sam002\otp;

use ParagonIE\ConstantTime\Base32;
use sam002\otp\helpers\OtpHelper;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\validators\UrlValidator;

/**
 * Class Collection is a single otp module with initialization and code-validation
 *
 * Example application configuration:
 *
 * ~~~
 *  'components' => [
 *      'otp' => [
 *          'class' => 'sam002\otp\Otp',
 *          'algorithm' => sam002\otp\Otp::ALGORITHM_TOTP
 *          'digits' => 6,
 *          'digest' => 'sha256',
 *          'issuer' => 'sam002',
 *          'label' => 'yii2-otp',
 *          'imgLabelUrl' => Yii,
 *          'secretLength' => 16
 *     ]
 *     ...
 * ]
 * ~~~
 *
 * @author Semen Dubina <sam@sam002.net>
 * @package sam002\otp
 */
class Otp extends Component
{

    const ALGORITHM_TOTP = 'totp';
    const ALGORITHM_HOTP = 'hotp';

    const SECRET_LENGTH_MIN = 8;
    const SECRET_LENGTH_MAX = 1024;

    /**
     * @var string
     */
    public $algorithm = self::ALGORITHM_HOTP;

    /**
     * @var int
     */
    public $digits = 6;

    /**
     * @var string
     */
    public $digest = 'sha256';

    /**
     * @var int
     */
    public $interval = 30;

    /**
     * @var int
     */
    public $counter = 0;

    /**
     * @var string
     */
    public $issuer = '';

    /**
     * @var string
     */
    public $label = 'yii2-otp';

    /**
     * @var null
     */
    public $imgLabelUrl = null;

    /**
     * @var int
     */
    public $secretLength = 64;

    private $secret = null;


    /**
     * @var \OTPHP\OTP
     */
    private $otp = null;

    public function init()
    {
        parent::init();
        if ($this->algorithm === self::ALGORITHM_TOTP) {
            $this->otp = OtpHelper::getTotp($this->label, $this->digits, $this->digest, $this->interval, $this->issuer);
        } elseif ($this->algorithm === self::ALGORITHM_HOTP) {
            $this->otp = OtpHelper::getHotp($this->label, $this->digits, $this->digest, $this->counter, $this->issuer);
        } else {
            throw new InvalidConfigException('otp::$algorithm = \"' . $this->algorithm . '\" not allowed, only Otp::ALGORITHM_TOTP or Otp::ALGORITHM_HOTP');
        }

        if (!empty($this->imgLabelUrl) && is_string($this->imgLabelUrl)) {
            $validator = new UrlValidator();
            if ($validator->validate($this->imgLabelUrl)) {
                $this->otp->setParameter('image',$this->imgLabelUrl);
            } else {
                throw new InvalidConfigException($validator->message);
            }
        }
    }

    /**
     * @return \OTPHP\OTP
     */
    public function getOtp()
    {
        $this->otp->setParameter('secret', strtoupper($this->getSecret()));
        return $this->otp;
    }

    /**
     * @return null|string
     * @throws InvalidConfigException
     */
    public function getSecret()
    {
        if (!is_numeric($this->secretLength) || $this->secretLength < self::SECRET_LENGTH_MIN || $this->secretLength > self::SECRET_LENGTH_MAX) {
            throw new InvalidConfigException('otp::$length only integer, min='. self::SECRET_LENGTH_MIN .'and max=' . self::SECRET_LENGTH_MAX);
        }
        if (empty($this->secret)) {
            $this->secret = OtpHelper::generateSecret($this->secretLength);
        }
        return $this->secret;
    }

    public function setSecret($value)
    {
        if(strlen($value) !== $this->secretLength) {
            throw new InvalidConfigException('Otp::setSecret length is not equal to ' . $this->secretLength . ' ([\'length\'] component settenings)');
        } elseif ( strlen(Base32::decodeUpper(strtoupper($value))) < 1 ) {
            throw new InvalidConfigException('Otp::setSecret incorect, encode as Base32');
        }
        $this->otp->setParameter('secret', strtoupper($value));
        $this->secret = $value;
    }

    public function valideteCode($code, $window = null)
    {
        if ($this->counter === 0 && $this->algorithm === self::ALGORITHM_TOTP) {
            //todo add time configuration
            $this->counter = null;
        }
        return $this->otp->verify($code, $this->counter, $window);
    }
}