Skip to content

Commit

Permalink
Added HOTP, reorganized the code.
Browse files Browse the repository at this point in the history
  • Loading branch information
paragonie-security committed Jun 17, 2016
1 parent 5f5427a commit 9be7156
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 84 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ composer require paragonie/multi-factor
```php
<?php
use ParagonIE\MuiltiFactor\FIDOU2F;
use ParagonIE\MultiFactor\OTP\TOTP;

$seed = random_bytes(20);

$fido = new FIDOU2F($seed);
// You can use TOTP or HOTP
$fido = new FIDOU2F($seed, new TOTP());

if (\password_verify($_POST['password'], $storedHash)) {
if ($fido->validateCode($_POST['2facode'])) {
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"require": {
"php": "^7",
"bacon/bacon-qr-code": "^1",
"paragonie/constant_time_encoding": "^2"
},
"require-dev": {
Expand Down
64 changes: 20 additions & 44 deletions src/FIDOU2F.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
declare(strict_types=1);
namespace ParagonIE\MultiFactor;

use ParagonIE\MultiFactor\Traits\TOTP;
use ParagonIE\MultiFactor\OTP\{
OTPInterface,
TOTP
};

/**
* Class FIDOU2F
Expand All @@ -13,84 +16,57 @@
*/
class FIDOU2F implements MultiFactorInterface
{
use TOTP;

/**
* @var string
*/
protected $algo;

/**
* @var int
* @var OTPInterface
*/
protected $length;
protected $otp;

/**
* @var string
*/
private $secretKey;

/**
* @var int
*/
protected $startTime;

/**
* @var int
*/
protected $timeStep;
protected $secretKey;

/**
* FIDOU2F constructor.
*
* @param string $secretKey
* @param int $startTime
* @param int $timeStep
* @param int $length
* @param string $algo
* @param OTPInterface $otp
*/
public function __construct(
string $secretKey = '',
int $startTime = 0,
int $timeStep = 30,
int $length = 6,
string $algo = 'sha1'
OTPInterface $otp = null
) {
$this->secretKey = $secretKey;
$this->startTime = $startTime;
$this->timeStep = $timeStep;
$this->length = $length;
$this->algo = $algo;
if (!$otp) {
$otp = new TOTP();
}
$this->otp = $otp;
}

/**
* Generate a TOTP code for 2FA
*
* @param int $offset - How many steps backwards to count?
* @param int $counterValue
* @return string
*/
public function generateCode(int $offset = 0): string
public function generateCode(int $counterValue = 0): string
{
return $this->getTOTPCode(
return $this->otp->getCode(
$this->secretKey,
\time() - ($this->timeStep * $offset),
$this->startTime,
$this->timeStep,
$this->length,
$this->algo
$counterValue
);
}

/**
* Validate a user-provided code
*
* @param string $code
* @param int $offset - How many steps backwards to count?
* @param int $counterValue
* @return bool
*/
public function validateCode(string $code, int $offset = 0): bool
public function validateCode(string $code, int $counterValue = 0): bool
{
$expected = $this->generateCode($offset);
$expected = $this->generateCode($counterValue);
return \hash_equals($code, $expected);
}
}
118 changes: 118 additions & 0 deletions src/OTP/HOTP.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace ParagonIE\MultiFactor\OTP;

use ParagonIE\ConstantTime\{
Binary,
Hex
};

/**
* Class HOTP
* @package ParagonIE\MultiFactor\OTP
*/
class HOTP
{
/**
* @var string
*/
protected $algo;

/**
* @var int
*/
protected $length;

/**
* HOTP constructor.
*
* @param int $length How many digits should each HOTP be?
* @param string $algo Hash function to use
*/
public function __construct(
int $length = 6,
string $algo = 'sha1'
) {
$this->length = $length;
$this->algo = $algo;
}

/**
* Generate a HOTP secret in accordance with RFC 4226
*
* @ref https://tools.ietf.org/html/rfc4226
* @param string $sharedSecret The key to use for determining the HOTP
* @param int $counterValue Current time or HOTP counter
* @return string
* @throws \OutOfRangeException
*/
public function getCode(
string $sharedSecret,
int $counterValue
): string {
if ($this->length < 1 || $this->length > 10) {
throw new \OutOfRangeException(
'Length must be between 1 and 10, as a consequence of RFC 6238.'
);
}
$msg = $this->getTValue($counterValue, true);
$bytes = \hash_hmac($this->algo, $msg, $sharedSecret, true);

$byteLen = Binary::safeStrlen($bytes);

// Per the RFC
$offset = \unpack('C', $bytes[$byteLen - 1])[1];
$offset &= 0x0f;

$unpacked = \array_values(
\unpack('C*', Binary::safeSubstr($bytes, $offset, 4))
);

$intValue = (
(($unpacked[0] & 0x7f) << 24)
| (($unpacked[1] & 0xff) << 16)
| (($unpacked[2] & 0xff) << 8)
| (($unpacked[3] & 0xff) )
);

$intValue %= 10 ** $this->length;

return \str_pad(
(string) $intValue,
$this->length,
'0',
\STR_PAD_LEFT
);
}

/**
* @return int
*/
public function getLength(): int
{
return $this->length;
}

/**
* Get the binary T value
*
* @param int $unixTimestamp
* @param bool $rawOutput
* @return string
*/
protected function getTValue(
int $counter,
bool $rawOutput = false
): string {
$hex = \str_pad(
\dechex($counter),
16,
'0',
STR_PAD_LEFT
);
if ($rawOutput) {
return Hex::decode($hex);
}
return $hex;
}
}
25 changes: 25 additions & 0 deletions src/OTP/OTPInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace ParagonIE\MultiFactor\OTP;

/**
* Interface OTPInterface
* @package ParagonIE\MultiFactor\OTP
*/
interface OTPInterface
{
/**
* Get the code we need
*
* @param string $sharedSecret The key to use for determining the TOTP
* @param int $counterValue Current time or HOTP counter
* @return string
* @throws \OutOfRangeException
*/
public function getCode(
string $sharedSecret,
int $counterValue
): string;

public function getLength(): int;
}
Loading

0 comments on commit 9be7156

Please sign in to comment.