Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/security-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ jobs:
phpstan_memory_limit: "1G"
psalm_threads: "1"
run_analysis: true
run_svg_report: true
artifact_retention_days: 61
2 changes: 1 addition & 1 deletion captainhook.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"options": []
},
{
"action": "composer ic:tests",
"action": "composer ic:ci",
"options": []
}
]
Expand Down
100 changes: 100 additions & 0 deletions src/AbstractOtpAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Infocyph\OTP;

use Infocyph\OTP\Support\ProvisioningUriBuilder;
use Infocyph\OTP\Support\ProvisioningUriParser;
use Infocyph\OTP\Support\SvgQrRenderer;
use Infocyph\OTP\ValueObjects\EnrollmentPayload;
use Infocyph\OTP\ValueObjects\ParsedOtpAuthUri;

abstract class AbstractOtpAuthenticator
{
public static function parseProvisioningUri(string $uri): ParsedOtpAuthUri
{
return ProvisioningUriParser::parse($uri);
}

final protected function assertOtp(string $otp, int $digitCount): void
{
if (!preg_match('/^\d+$/', $otp) || strlen($otp) !== $digitCount) {
throw new \InvalidArgumentException('OTP must be a numeric string matching the configured digit count.');
}
}

/**
* @param array<string> $include
* @param array<string, scalar|null> $additionalParameters
*/
final protected function buildEnrollmentPayload(
string $otpType,
string $secret,
string $label,
string $issuer,
array $include,
array $additionalParameters,
string $algorithm,
int $digitCount,
?int $period,
?int $counter,
bool $withQrSvg,
int $imageSize,
string $uri,
): EnrollmentPayload {
return ProvisioningUriBuilder::enrollmentPayload(
$otpType,
$secret,
$label,
$issuer,
$this->includeFlags($include),
$additionalParameters,
$algorithm,
$digitCount,
$period,
$counter,
null,
$withQrSvg ? SvgQrRenderer::render($uri, $imageSize) : null,
);
}

/**
* @param array<string> $include
* @param array<string, scalar|null> $additionalParameters
*/
final protected function buildProvisioningUri(
string $otpType,
string $secret,
string $label,
string $issuer,
array $include,
array $additionalParameters,
string $algorithm,
int $digitCount,
?int $period,
?int $counter,
): string {
return ProvisioningUriBuilder::build(
$otpType,
$secret,
$label,
$issuer,
$this->includeFlags($include),
$additionalParameters,
$algorithm,
$digitCount,
$period,
$counter,
);
}

/**
* @param array<string> $include
* @return array<string, bool>
*/
final protected function includeFlags(array $include): array
{
return array_fill_keys($include, true);
}
}
32 changes: 9 additions & 23 deletions src/HOTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@
use Infocyph\OTP\Result\VerificationResult;
use Infocyph\OTP\Support\AlgorithmValidator;
use Infocyph\OTP\Support\OtpMath;
use Infocyph\OTP\Support\ProvisioningUriBuilder;
use Infocyph\OTP\Support\ProvisioningUriParser;
use Infocyph\OTP\Support\SecretUtility;
use Infocyph\OTP\Support\SvgQrRenderer;
use Infocyph\OTP\ValueObjects\EnrollmentPayload;
use Infocyph\OTP\ValueObjects\ParsedOtpAuthUri;
use Infocyph\OTP\ValueObjects\SecretRotation;

final class HOTP
final class HOTP extends AbstractOtpAuthenticator
{
private readonly string $secret;

Expand All @@ -44,11 +41,6 @@ public static function generateSecret(int $bytes = 64): string
return SecretUtility::generate($bytes);
}

public static function parseProvisioningUri(string $uri): ParsedOtpAuthUri
{
return ProvisioningUriParser::parse($uri);
}

/**
* @param array<string> $include
* @param array<string, scalar|null> $additionalParameters
Expand All @@ -63,19 +55,20 @@ public function getEnrollmentPayload(
): EnrollmentPayload {
$uri = $this->getProvisioningUri($label, $issuer, $include, $additionalParameters);

return ProvisioningUriBuilder::enrollmentPayload(
return $this->buildEnrollmentPayload(
'hotp',
$this->secret,
$label,
$issuer,
array_fill_keys($include, true),
$include,
$additionalParameters,
$this->algorithm,
$this->digitCount,
null,
$this->counter,
null,
$withQrSvg ? SvgQrRenderer::render($uri, $imageSize) : null,
$withQrSvg,
$imageSize,
$uri,
);
}

Expand All @@ -94,12 +87,12 @@ public function getProvisioningUri(
array $include = ['algorithm', 'digits', 'counter'],
array $additionalParameters = [],
): string {
return ProvisioningUriBuilder::build(
return $this->buildProvisioningUri(
'hotp',
$this->secret,
$label,
$issuer,
array_fill_keys($include, true),
$include,
$additionalParameters,
$this->algorithm,
$this->digitCount,
Expand Down Expand Up @@ -187,7 +180,7 @@ public function verifyWithResult(
?ReplayStoreInterface $replayStore = null,
?string $binding = null,
): VerificationResult {
$this->assertOtp($otp);
$this->assertOtp($otp, $this->digitCount);
if ($counter < 0 || $lookAhead < 0) {
throw new \InvalidArgumentException('Counter and look-ahead window must be non-negative.');
}
Expand Down Expand Up @@ -217,11 +210,4 @@ public function verifyWithResult(

return new VerificationResult(false, 'mismatch');
}

private function assertOtp(string $otp): void
{
if (!preg_match('/^\d+$/', $otp) || strlen($otp) !== $this->digitCount) {
throw new \InvalidArgumentException('OTP must be a numeric string matching the configured digit count.');
}
}
}
33 changes: 10 additions & 23 deletions src/TOTP.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@
use Infocyph\OTP\Result\VerificationResult;
use Infocyph\OTP\Support\AlgorithmValidator;
use Infocyph\OTP\Support\OtpMath;
use Infocyph\OTP\Support\ProvisioningUriBuilder;
use Infocyph\OTP\Support\ProvisioningUriParser;
use Infocyph\OTP\Support\SecretUtility;
use Infocyph\OTP\Support\SvgQrRenderer;
use Infocyph\OTP\ValueObjects\EnrollmentPayload;
use Infocyph\OTP\ValueObjects\ParsedOtpAuthUri;
use Infocyph\OTP\ValueObjects\SecretRotation;
use Infocyph\OTP\ValueObjects\VerificationWindow;

final class TOTP
final class TOTP extends AbstractOtpAuthenticator
{
private readonly string $secret;

Expand Down Expand Up @@ -47,11 +44,6 @@ public static function generateSecret(int $bytes = 64): string
return SecretUtility::generate($bytes);
}

public static function parseProvisioningUri(string $uri): ParsedOtpAuthUri
{
return ProvisioningUriParser::parse($uri);
}

public function getCurrentTimeStep(?int $timestamp = null): int
{
return $this->getTimeStepFromTimestamp($timestamp ?? time());
Expand All @@ -71,19 +63,20 @@ public function getEnrollmentPayload(
): EnrollmentPayload {
$uri = $this->getProvisioningUri($label, $issuer, $include, $additionalParameters);

return ProvisioningUriBuilder::enrollmentPayload(
return $this->buildEnrollmentPayload(
'totp',
$this->secret,
$label,
$issuer,
array_fill_keys($include, true),
$include,
$additionalParameters,
$this->algorithm,
$this->digitCount,
$this->period,
null,
null,
$withQrSvg ? SvgQrRenderer::render($uri, $imageSize) : null,
$withQrSvg,
$imageSize,
$uri,
);
}

Expand All @@ -107,16 +100,17 @@ public function getProvisioningUri(
array $include = ['algorithm', 'digits', 'period'],
array $additionalParameters = [],
): string {
return ProvisioningUriBuilder::build(
return $this->buildProvisioningUri(
'totp',
$this->secret,
$label,
$issuer,
array_fill_keys($include, true),
$include,
$additionalParameters,
$this->algorithm,
$this->digitCount,
$this->period,
null,
);
}

Expand Down Expand Up @@ -229,7 +223,7 @@ public function verifyWithWindow(
?string $binding = null,
bool $singleUse = true,
): VerificationResult {
$this->assertOtp($otp);
$this->assertOtp($otp, $this->digitCount);
$window ??= new VerificationWindow();
if ($window->past < 0 || $window->future < 0) {
throw new \InvalidArgumentException('Verification windows must be non-negative.');
Expand Down Expand Up @@ -267,11 +261,4 @@ public function verifyWithWindow(

return new VerificationResult(false, 'mismatch');
}

private function assertOtp(string $otp): void
{
if (!preg_match('/^\d+$/', $otp) || strlen($otp) !== $this->digitCount) {
throw new \InvalidArgumentException('OTP must be a numeric string matching the configured digit count.');
}
}
}