<?php
namespace Otp;
/**
* One Time Passwords
*
* Implements HOTP and TOTP
*
* HMAC-Based One-time Password(HOTP) algorithm specified in RFC 4226
* @link https://tools.ietf.org/html/rfc4226
*
* Time-based One-time Password (TOTP) algorithm specified in RFC 6238
* @link https://tools.ietf.org/html/rfc6238
*
* As a note: This code is only 2038 proof on 64 bit PHP installations with
* PHP >= 5.6.3!
*
* Can be easy used with Google Authenticator
* @link https://code.google.com/p/google-authenticator/
*
* @author Christian Riesen <chris.riesen@gmail.com>
* @link http://christianriesen.com
* @license MIT License see LICENSE file
*/
class Otp implements OtpInterface
{
/**
* The digits the code can have
*
* Either 6 or 8.
* Authenticator does only support 6.
*
* @var integer
*/
protected $digits = 6;
/**
* Time in seconds one counter period is long
*
* @var integer
*/
protected $period = 30;
/**
* Possible algorithms
*
* @var array
*/
protected $allowedAlgorithms = array('sha1', 'sha256', 'sha512');
/**
* Currently used algorithm
*
* @var string
*/
protected $algorithm = 'sha1';
/**
* Time offset between system time and GMT in seconds
*
* @var integer
*/
protected $totpOffset = 0;
/* (non-PHPdoc)
* @see Otp.OtpInterface::hotp()
*/
public function hotp($secret, $counter)
{
if (!is_numeric($counter) || $counter < 0) {
throw new \InvalidArgumentException('Invalid counter supplied');
}
$hash = hash_hmac(
$this->algorithm,
$this->getBinaryCounter($counter),
$secret,
true
);
return str_pad($this->truncate($hash), $this->digits, '0', STR_PAD_LEFT);
}
/* (non-PHPdoc)
* @see Otp.OtpInterface::totp()
*/
public function totp($secret, $timecounter = null)
{
if (is_null($timecounter)) {
$timecounter = $this->getTimecounter();
}
return $this->hotp($secret, $timecounter);
}
/* (non-PHPdoc)
* @see Otp.OtpInterface::checkHotp()
*/
public function checkHotp($secret, $counter, $key)
{
return hash_equals($this->hotp($secret, $counter), $key);
}
/* (non-PHPdoc)
* @see Otp.OtpInterface::checkHotpResync()
*/
public function checkHotpResync($secret, $counter, $key, $counterwindow = 2)
{
if (!is_numeric($counter) || $counter < 0) {
throw new \InvalidArgumentException('Invalid counter supplied');
}
if(!is_numeric($counterwindow) || $counterwindow < 0){
throw new \InvalidArgumentException('Invalid counterwindow supplied');
}
for($c = 0; $c <= $counterwindow; $c = $c + 1) {
if(hash_equals($this->hotp($secret, $counter + $c), $key)){
return $counter + $c;
}
}
return false;
}
/* (non-PHPdoc)
* @see Otp.OtpInterface::checkTotp()
*/
public function checkTotp($secret, $key, $timedrift = 1)
{
if (!is_numeric($timedrift) || $timedrift < 0) {
throw new \InvalidArgumentException('Invalid timedrift supplied');
}
// Counter comes from time now
// Also we check the current timestamp as well as previous and future ones
// according to $timerange
$timecounter = $this->getTimecounter();
$start = $timecounter - ($timedrift);
$end = $timecounter + ($timedrift);
// We first try the current, as it is the most likely to work
if (hash_equals($this->totp($secret, $timecounter), $key)) {
return true;
} elseif ($timedrift == 0) {
// When timedrift is 0, this is the end of the checks
return false;
}
// Well, that didn't work, so try the others
for ($t = $start; $t <= $end; $t = $t + 1) {
if ($t == $timecounter) {
// Already tried that one
continue;
}
if (hash_equals($this->totp($secret, $t), $key)) {
return true;
}
}
// if none worked, then return false
return false;
}
/**
* Changing the used algorithm for hashing
*
* Can only be one of the algorithms in the allowedAlgorithms property.
*
* @param string $algorithm
* @throws \InvalidArgumentException
* @return \Otp\Otp
*/
public function setAlgorithm($algorithm)
{
if (!in_array($algorithm, $this->allowedAlgorithms)) {
throw new \InvalidArgumentException('Not an allowed algorithm: ' . $algorithm);
}
$this->algorithm = $algorithm;
return $this;
}
/**
* Get the algorithms name (lowercase)
*
* @return string
*/
public function getAlgorithm()
{
return $this->algorithm;
}
/**
* Setting period length for totp
*
* @param integer $period
* @throws \InvalidArgumentException
* @return \Otp\Otp
*/
public function setPeriod($period)
{
if (!is_int($period)) {
throw new \InvalidArgumentException('Period must be an integer');
}
$this->period = $period;
return $this;
}
/**
* Returns the set period value
*
* @return integer
*/
public function getPeriod()
{
return $this->period;
}
/**
* Setting number of otp digits
*
* @param integer $digits Number of digits for the otp (6 or 8)
* @throws \InvalidArgumentException
* @return \Otp\Otp
*/
public function setDigits($digits)
{
if (!in_array($digits, array(6, 8))) {
throw new \InvalidArgumentException('Digits must be 6 or 8');
}
$this->digits = $digits;
return $this;
}
/**
* Returns number of digits in the otp
*
* @return integer
*/
public function getDigits()
{
return $this->digits;
}
/**
* Set offset between system time and GMT
*
* @param integer $offset GMT - time()
* @throws \InvalidArgumentException
* @return \Otp\Otp
*/
public function setTotpOffset($offset)
{
if (!is_int($offset)) {
throw new \InvalidArgumentException('Offset must be an integer');
}
$this->totpOffset = $offset;
return $this;
}
/**
* Returns offset between system time and GMT in seconds
*
* @return integer
*/
public function getTotpOffset()
{
return $this->totpOffset;
}
/**
* Generates a binary counter for hashing
*
* Warning: use 64 bit PHP >= 5.6.3 to be "2038 safe".
*
* @param integer $counter Counter in integer form
* @return string Binary string
*/
private function getBinaryCounter($counter)
{
// on 64 bit, PHP >= 5.6.3 this is "2038 safe"
if (8 === PHP_INT_SIZE && PHP_VERSION_ID >= 50603) {
return pack('J', $counter);
}
// keep old behavior for 32 bit PHP or PHP < 5.6.3
return pack('N*', 0) . pack('N*', $counter);
}
/**
* Generating time counter
*
* This is the time divided by 30 by default.
*
* @return integer Time counter
*/
private function getTimecounter()
{
return floor((time() + $this->totpOffset) / $this->period);
}
/**
* Creates the basic number for otp from hash
*
* This number is left padded with zeros to the required length by the
* calling function.
*
* @param string $hash hmac hash
* @return number
*/
private function truncate($hash)
{
$offset = ord($hash[strlen($hash)-1]) & 0xf;
return (
((ord($hash[$offset+0]) & 0x7f) << 24 ) |
((ord($hash[$offset+1]) & 0xff) << 16 ) |
((ord($hash[$offset+2]) & 0xff) << 8 ) |
(ord($hash[$offset+3]) & 0xff)
) % pow(10, $this->digits);
}
}