Skip to content

Commit

Permalink
Merge pull request #4 from abmmhasan/feature/enhancement
Browse files Browse the repository at this point in the history
updated otp & totp
  • Loading branch information
abmmhasan authored Jan 3, 2024
2 parents ac94d39 + 5c165c9 commit 7a9d150
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 16 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ on:

jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: [ ubuntu-latest ]
php-versions: [ '8.0', '8.1', '8.2', '8.3' ]
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Simple but Secure Generic OTP, TOTP (RFC6238), HOTP (RFC4226) solution!
Language: PHP 8.0/+

_Note: v1.x.x supports PHP 7.x.x series_
_Note: v2.x.x supports PHP 8.x.x series_

## Installation

Expand Down Expand Up @@ -88,6 +89,9 @@ $otp = (new \AbmmHasan\OTP\TOTP($secret))->getOTP(1604820275);

/**
* Verify
*
* on 3rd parameter it supports, enabling leeway.
* if enabled, it will also check with last segment's generated otp
*/
(new \AbmmHasan\OTP\TOTP($secret))->verify($otp);
// or verify for a specified time
Expand All @@ -98,7 +102,10 @@ $otp = (new \AbmmHasan\OTP\TOTP($secret))->getOTP(1604820275);

```php
/**
* Initiate (Param 1 is OTP length, Param 2 is validity in seconds)
* Initiate
* Param 1 is OTP length (default 6)
* Param 2 is validity in seconds (default 30 seconds)
* Param 3 is retry count on failure (default 3)
*/
$otpInstance = new \AbmmHasan\OTP\OTP(4, 60);

Expand Down
67 changes: 58 additions & 9 deletions src/OTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Exception;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\CacheItem;

class OTP
{
Expand All @@ -19,6 +20,7 @@ class OTP
public function __construct(
private int $digitCount = 6,
private int $validUpto = 30,
private int $retry = 3,
private string $hashAlgorithm = 'sha256'
) {
$this->cacheAdapter = new FilesystemAdapter();
Expand All @@ -33,10 +35,10 @@ public function __construct(
*/
public function generate(string $signature): int
{
$this->validateRequirements();
$otpAdapter = $this->cacheAdapter->getItem('ao-otp_' . base64_encode($signature));
$otp = $this->number($this->digitCount);
$otpAdapter->set(hash($this->hashAlgorithm, $otp))->expiresAfter($this->validUpto);
$this->cacheAdapter->save($otpAdapter);
$this->storeData($otpAdapter, hash($this->hashAlgorithm, $otp), $this->retry, $this->validUpto);
return $otp;
}

Expand All @@ -56,12 +58,16 @@ public function verify(string $signature, int $otp, bool $deleteIfFound = true):
}
$signature = 'ao-otp_' . base64_encode($signature);
$otpAdapter = $this->cacheAdapter->getItem($signature);
if ($otpAdapter->isHit()) {
$isVerified = hash_equals($otpAdapter->get(), hash($this->hashAlgorithm, $otp));
($deleteIfFound || $isVerified) && $this->cacheAdapter->deleteItem($signature);
return $isVerified;
if (!$otpAdapter->isHit()) {
return false;
}
return false;
['secret' => $secret, 'retry' => $retry, 'expiresAt' => $expiresAt] = $otpAdapter->get();
$isVerified = hash_equals($secret, hash($this->hashAlgorithm, $otp));
match (true) {
$deleteIfFound || $isVerified || $retry < 1 => $this->cacheAdapter->deleteItem($signature),
default => $this->storeData($otpAdapter, $secret, --$retry, $expiresAt - time())
};
return $isVerified;
}

/**
Expand All @@ -86,6 +92,49 @@ public function flush(): bool
return $this->cacheAdapter->clear();
}

/**
* Stores the data in the cache.
*
* @param CacheItem $otpAdapter The OTP adapter.
* @param string $secret The secret.
* @param int $retry The number of retries.
* @param int $ttl The time to live in seconds.
* @return void
*/
private function storeData(CacheItem $otpAdapter, string $secret, int $retry, int $ttl): void
{
if ($ttl < 1) {
return;
}
$this->cacheAdapter->save(
$otpAdapter->set([
'secret' => $secret,
'retry' => $retry,
'expiresAt' => time() + $ttl
])->expiresAfter($ttl)
);
}

/**
* Validates the requirement for the PHP function.
*
* @throws Exception The number of digits must be between 2 and PHP_INT_SIZE.
* @throws Exception The number of retries must be at least 0.
* @throws Exception Validity duration is invalid.
*/
private function validateRequirements(): void
{
match (true) {
$this->digitCount < 2 || $this->digitCount > PHP_INT_SIZE
=> throw new Exception('The number of digits must be between 2 and ' . PHP_INT_SIZE . '.'),
$this->retry < 0
=> throw new Exception('The number of retries must be atleast 0.'),
$this->validUpto < 1
=> throw new Exception('Validity duration is invalid.'),
default => null
};
}

/**
* Generate Secure random number of given length
*
Expand All @@ -96,8 +145,8 @@ public function flush(): bool
private function number(int $length): int
{
return random_int(
intval('1' . str_repeat('0', $length - 1)),
intval(str_repeat('9', $length))
(int)('1' . str_repeat('0', $length - 1)),
(int)str_repeat('9', $length)
);
}
}
15 changes: 10 additions & 5 deletions src/TOTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ public function getOTP(int $input = null): string
}

/**
* Verifies if the given OTP matches the OTP generated by the given timestamp (or Current Timestamp).
* Verifies the provided OTP against the generated OTP for the given timestamp (or Current Timestamp).
*
* @param string $otp The OTP to be verified.
* @param int|null $input The input used to generate the OTP. Defaults to Current Timestamp.
* @return bool Returns true if the OTP matches the generated one, otherwise false.
* @param int|null $timestamp The timestamp for which the OTP is to be generated. Defaults to Current Timestamp.
* @param bool $leeway Whether to allow a time leeway for OTP verification.
* @return bool Returns true if the provided OTP matches the generated OTP, false otherwise.
*/
public function verify(string $otp, int $input = null): bool
public function verify(string $otp, int $timestamp = null, bool $leeway = false): bool
{
return hash_equals($otp, $this->getOTP($input));
$isVerified = hash_equals($otp, $this->getOTP($timestamp));
if (!$isVerified && $leeway) {
return hash_equals($otp, $this->getOTP(($timestamp ?? time()) - $this->period));
}
return $isVerified;
}
}

0 comments on commit 7a9d150

Please sign in to comment.