Commit 0d3f92d6 authored by Filippo Tessarotto's avatar Filippo Tessarotto Committed by Florent Morselli
Browse files

Prefer explicit setters over optional parameters

Showing with 161 additions and 124 deletions
+161 -124
......@@ -12,33 +12,36 @@ use function is_int;
*/
final class HOTP extends OTP implements HOTPInterface
{
protected function __construct(null|string $secret, int $counter, string $digest, int $digits)
{
parent::__construct($secret, $digest, $digits);
$this->setCounter($counter);
}
public static function create(
null|string $secret = null,
int $counter = 0,
string $digest = 'sha1',
int $digits = 6
int $counter = self::DEFAULT_COUNTER,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
): self {
return new self($secret, $counter, $digest, $digits);
$htop = $secret !== null
? self::createFromSecret($secret)
: self::generate()
;
$htop->setCounter($counter);
$htop->setDigest($digest);
$htop->setDigits($digits);
return $htop;
}
public static function createFromSecret(
string $secret,
int $counter = 0,
string $digest = 'sha1',
int $digits = 6
): self {
return new self($secret, $counter, $digest, $digits);
public static function createFromSecret(string $secret): self
{
$htop = new self($secret);
$htop->setCounter(self::DEFAULT_COUNTER);
$htop->setDigest(self::DEFAULT_DIGEST);
$htop->setDigits(self::DEFAULT_DIGITS);
return $htop;
}
public static function generate(int $counter = 0, string $digest = 'sha1', int $digits = 6): self
public static function generate(): self
{
return new self(self::generateSecret(), $counter, $digest, $digits);
return self::createFromSecret(self::generateSecret());
}
public function getCounter(): int
......@@ -72,7 +75,7 @@ final class HOTP extends OTP implements HOTPInterface
return $this->verifyOtpWithWindow($otp, $counter, $window);
}
protected function setCounter(int $counter): void
public function setCounter(int $counter): void
{
$this->setParameter('counter', $counter);
}
......
......@@ -6,6 +6,8 @@ namespace OTPHP;
interface HOTPInterface extends OTPInterface
{
public const DEFAULT_COUNTER = 0;
/**
* The initial counter (a positive integer).
*/
......@@ -16,6 +18,9 @@ interface HOTPInterface extends OTPInterface
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param non-empty-string $digest
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
......@@ -25,20 +30,5 @@ interface HOTPInterface extends OTPInterface
int $digits = 6
): self;
/**
* Create a TOTP object from an existing secret.
*
* @param non-empty-string $secret
*/
public static function createFromSecret(
string $secret,
int $counter = 0,
string $digest = 'sha1',
int $digits = 6
): self;
/**
* Create a new HOTP object. A random 64 bytes secret will be generated.
*/
public static function generate(int $counter = 0, string $digest = 'sha1', int $digits = 6): self;
public function setCounter(int $counter): void;
}
......@@ -17,11 +17,12 @@ abstract class OTP implements OTPInterface
{
use ParameterTrait;
protected function __construct(null|string $secret, string $digest, int $digits)
/**
* @param non-empty-string $secret
*/
protected function __construct(string $secret)
{
$this->setSecret($secret);
$this->setDigest($digest);
$this->setDigits($digits);
}
public function getQrCodeUri(string $uri, string $placeholder): string
......
......@@ -6,6 +6,34 @@ namespace OTPHP;
interface OTPInterface
{
public const DEFAULT_DIGITS = 6;
public const DEFAULT_DIGEST = 'sha1';
/**
* Create a OTP object from an existing secret.
*
* @param non-empty-string $secret
*/
public static function createFromSecret(string $secret): self;
/**
* Create a new OTP object. A random 64 bytes secret will be generated.
*/
public static function generate(): self;
/**
* @param non-empty-string $secret
*/
public function setSecret(string $secret): void;
public function setDigits(int $digits): void;
/**
* @param non-empty-string $digest
*/
public function setDigest(string $digest): void;
/**
* @return string Return the OTP at the specified timestamp
*/
......
......@@ -121,6 +121,21 @@ trait ParameterTrait
}
}
public function setSecret(string $secret): void
{
$this->setParameter('secret', $secret);
}
public function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
public function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
/**
* @return array<string, callable>
*/
......@@ -161,21 +176,6 @@ trait ParameterTrait
];
}
private function setSecret(null|string $secret): void
{
$this->setParameter('secret', $secret);
}
private function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
private function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
......
......@@ -12,40 +12,39 @@ use function is_int;
*/
final class TOTP extends OTP implements TOTPInterface
{
protected function __construct(null|string $secret, int $period, string $digest, int $digits, int $epoch = 0)
{
parent::__construct($secret, $digest, $digits);
$this->setPeriod($period);
$this->setEpoch($epoch);
}
public static function create(
null|string $secret = null,
int $period = 30,
string $digest = 'sha1',
int $digits = 6,
int $epoch = 0
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH
): self {
return new self($secret, $period, $digest, $digits, $epoch);
$totp = $secret !== null
? self::createFromSecret($secret)
: self::generate()
;
$totp->setPeriod($period);
$totp->setDigest($digest);
$totp->setDigits($digits);
$totp->setEpoch($epoch);
return $totp;
}
public static function createFromSecret(
string $secret,
int $period = 30,
string $digest = 'sha1',
int $digits = 6,
int $epoch = 0
): self {
return new self($secret, $period, $digest, $digits, $epoch);
public static function createFromSecret(string $secret): self
{
$totp = new self($secret);
$totp->setPeriod(self::DEFAULT_PERIOD);
$totp->setDigest(self::DEFAULT_DIGEST);
$totp->setDigits(self::DEFAULT_DIGITS);
$totp->setEpoch(self::DEFAULT_EPOCH);
return $totp;
}
public static function generate(
int $period = 30,
string $digest = 'sha1',
int $digits = 6,
int $epoch = 0
): self {
return new self(self::generateSecret(), $period, $digest, $digits, $epoch);
public static function generate(): self
{
return self::createFromSecret(self::generateSecret());
}
public function getPeriod(): int
......@@ -118,11 +117,16 @@ final class TOTP extends OTP implements TOTPInterface
return $this->generateURI('totp', $params);
}
protected function setPeriod(int $period): void
public function setPeriod(int $period): void
{
$this->setParameter('period', $period);
}
public function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
/**
* @return array<string, callable>
*/
......@@ -161,11 +165,6 @@ final class TOTP extends OTP implements TOTPInterface
ksort($options);
}
private function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
private function timecode(int $timestamp): int
{
return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
......
......@@ -6,36 +6,30 @@ namespace OTPHP;
interface TOTPInterface extends OTPInterface
{
public const DEFAULT_PERIOD = 30;
public const DEFAULT_EPOCH = 0;
/**
* Create a new TOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param non-empty-string $digest
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
null|string $secret = null,
int $period = 30,
string $digest = 'sha1',
int $digits = 6
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
): self;
/**
* Create a TOTP object from an existing secret.
*
* @param non-empty-string $secret
*/
public static function createFromSecret(
string $secret,
int $period = 30,
string $digest = 'sha1',
int $digits = 6
): self;
public function setPeriod(int $period): void;
/**
* Create a new TOTP object. A random 64 bytes secret will be generated.
*/
public static function generate(int $period = 30, string $digest = 'sha1', int $digits = 6): self;
public function setEpoch(int $epoch): void;
/**
* Return the TOTP at the current time.
......
......@@ -30,10 +30,10 @@ final class HOTPTest extends TestCase
*/
public function issuerHasColon(): void
{
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Issuer must not contain a colon.');
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
$otp->setLabel('alice');
$otp->setIssuer('foo%3Abar');
}
......@@ -42,10 +42,10 @@ final class HOTPTest extends TestCase
*/
public function issuerHasColon2(): void
{
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Issuer must not contain a colon.');
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
$otp->setLabel('alice');
$otp->setIssuer('foo%3abar');
}
......@@ -54,11 +54,11 @@ final class HOTPTest extends TestCase
*/
public function labelHasColon(): void
{
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Label must not contain a colon.');
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
$otp->setLabel('foo%3Abar');
$otp->getProvisioningUri();
}
/**
......@@ -66,11 +66,11 @@ final class HOTPTest extends TestCase
*/
public function labelHasColon2(): void
{
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Label must not contain a colon.');
$otp = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 8);
$otp->setLabel('foo:bar');
$otp->getProvisioningUri();
}
/**
......@@ -78,9 +78,11 @@ final class HOTPTest extends TestCase
*/
public function digitsIsNot1OrMore(): void
{
$htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Digits must be at least 1.');
HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'sha512', 0);
$htop->setDigits(0);
}
/**
......@@ -88,9 +90,11 @@ final class HOTPTest extends TestCase
*/
public function counterIsNot1OrMore(): void
{
$htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Counter must be at least 0.');
HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', -500);
$htop->setCounter(-500);
}
/**
......@@ -98,9 +102,11 @@ final class HOTPTest extends TestCase
*/
public function digestIsNotSupported(): void
{
$htop = HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The "foo" digest is not supported.');
HOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 0, 'foo');
$htop->setDigest('foo');
}
/**
......@@ -110,11 +116,10 @@ final class HOTPTest extends TestCase
*/
public function secretShouldBeBase32Encoded(): void
{
$otp = HOTP::createFromSecret(random_bytes(32));
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unable to decode the secret. Is it correctly base32 encoded?');
$secret = random_bytes(32);
$otp = HOTP::createFromSecret($secret);
$otp->at(0);
}
......@@ -185,8 +190,12 @@ final class HOTPTest extends TestCase
string $issuer = 'My Project'
): HOTP {
static::assertNotSame('', $secret);
static::assertNotSame('', $digest);
$otp = HOTP::createFromSecret($secret, $counter, $digest, $digits);
$otp = HOTP::createFromSecret($secret);
$otp->setCounter($counter);
$otp->setDigest($digest);
$otp->setDigits($digits);
$otp->setLabel($label);
$otp->setIssuer($issuer);
......
......@@ -33,7 +33,11 @@ final class TOTPTest extends TestCase
*/
public function customParameter(): void
{
$otp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 20, 'sha512', 8, 100);
$otp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$otp->setPeriod(20);
$otp->setDigest('sha512');
$otp->setDigits(8);
$otp->setEpoch(100);
$otp->setLabel('alice@foo.bar');
$otp->setIssuer('My Project');
$otp->setParameter('foo', 'bar.baz');
......@@ -59,9 +63,11 @@ final class TOTPTest extends TestCase
*/
public function periodIsNot1OrMore(): void
{
$totp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Period must be at least 1.');
TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', -20, 'sha512', 8);
$totp->setPeriod(-20);
}
/**
......@@ -69,9 +75,11 @@ final class TOTPTest extends TestCase
*/
public function epochIsNot0OrMore(): void
{
$totp = TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y');
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Epoch must be greater than or equal to 0.');
TOTP::createFromSecret('JDDK4U6G3BJLEZ7Y', 30, 'sha512', 8, -1);
$totp->setEpoch(-1);
}
/**
......@@ -403,8 +411,13 @@ final class TOTPTest extends TestCase
int $epoch = 0
): TOTP {
static::assertNotSame('', $secret);
static::assertNotSame('', $digest);
$otp = TOTP::createFromSecret($secret, $period, $digest, $digits, $epoch);
$otp = TOTP::createFromSecret($secret);
$otp->setPeriod($period);
$otp->setDigest($digest);
$otp->setDigits($digits);
$otp->setEpoch($epoch);
$otp->setLabel($label);
$otp->setIssuer($issuer);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment