Skip to content

Commit 335d375

Browse files
authored
skip secret length check in OTP client setting (#5)
Some providers don't follow the RFCs to the letter and only use 10 bytes for the shared secret. Therefore, the generate() method now skips the secret length validation. The verify() method continues to throw if the secret is too short. This is to enforce the security recommendations from the RFCs when you're implementing an OTP provider.
1 parent 1c1cd65 commit 335d375

File tree

3 files changed

+18
-6
lines changed

3 files changed

+18
-6
lines changed

src/Algorithm.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ enum Algorithm: string
1414
case Sha256 = 'sha256';
1515
case Sha512 = 'sha512';
1616

17-
public function hash(string $value, Secret $secret): string
17+
public function hash(string $value, Secret $secret, bool $skipLengthCheck): string
1818
{
19-
if (strlen($secret->asBinary()) < $this->minSecretLength()) {
19+
if (!$skipLengthCheck && strlen($secret->asBinary()) < $this->minSecretLength()) {
2020
throw new InvalidArgumentException(sprintf(
2121
"Secret is too short (%d bytes), at least %d bytes should be provided for algorithm %s.",
2222
strlen($secret->asBinary()),

src/OTP.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ public function generate(
8282
$value = $generator->current();
8383
$generator->send(true);
8484

85-
return $this->generateOneTimePassword($value, $account->getSecret(), $digits);
85+
// skip length check because an OTP client does not have control over the secret length and some providers only use 10 bytes
86+
return $this->generateOneTimePassword($value, $account->getSecret(), $digits, skipLengthCheck: true);
8687
}
8788

8889
/**
@@ -131,10 +132,11 @@ private function generateOneTimePassword(
131132
int $value,
132133
Secret $secret,
133134
int $digits,
135+
bool $skipLengthCheck = false,
134136
): string
135137
{
136138
$packedValue = pack('J*', $value);
137-
$hash = $this->algorithm->hash($packedValue, $secret);
139+
$hash = $this->algorithm->hash($packedValue, $secret, $skipLengthCheck);
138140
$offset = ord($hash[strlen($hash) - 1]) & 0xf;
139141

140142
$code = (

tests/TOTPIntegrationTest.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ public function getTime(): float
3030
);
3131

3232
$account = new SimpleAccountDescriptor('AccountName', Secret::fromBinary(str_repeat("\x42", 20)));
33-
3433
Assert::same('930232', $authenticator->generate($account));
34+
35+
// short secret is ok here
36+
$account = new SimpleAccountDescriptor('AccountName', Secret::fromBinary(str_repeat("\x42", 10)));
37+
Assert::noError(fn() => $authenticator->generate($account));
3538
}
3639

3740
public function testVerify(): void
@@ -47,12 +50,19 @@ public function getTime(): float
4750
);
4851

4952
$account = new SimpleAccountDescriptor('AccountName', Secret::fromBinary(str_repeat("\x42", 20)));
50-
5153
Assert::false($authenticator->verify($account, '027585'));
5254
Assert::true($authenticator->verify($account, '714222'));
5355
Assert::true($authenticator->verify($account, '930232'));
5456
Assert::true($authenticator->verify($account, '605509'));
5557
Assert::false($authenticator->verify($account, '570599'));
58+
59+
// short secret means error
60+
$account = new SimpleAccountDescriptor('AccountName', Secret::fromBinary(str_repeat("\x42", 10)));
61+
Assert::throws(
62+
fn() => $authenticator->verify($account, '111111'),
63+
\InvalidArgumentException::class,
64+
'Secret is too short (10 bytes), at least 20 bytes should be provided for algorithm sha1.',
65+
);
5666
}
5767
}
5868

0 commit comments

Comments
 (0)