Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.59% covered (success)
92.59%
25 / 27
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
HOTPSecret
92.59% covered (success)
92.59%
25 / 27
80.00% covered (warning)
80.00%
8 / 10
13.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createRandom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSecret
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCounter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUrl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getQrCodeUri
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 validateState
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 createOTP
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 nextPassword
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 verify
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\OtpValueObjects;
3
4use Apie\Core\Attributes\FakeMethod;
5use Apie\Core\Attributes\ProvideIndex;
6use Apie\Core\Lists\StringHashmap;
7use Apie\Core\ValueObjects\CompositeValueObject;
8use Apie\Core\ValueObjects\Interfaces\ValueObjectInterface;
9use Apie\OtpValueObjects\Concerns\NoIndexing;
10use Apie\Serializer\Exceptions\ValidationException;
11use chillerlan\QRCode\QRCode;
12use OTPHP\HOTP;
13
14#[FakeMethod('createRandom')]
15#[ProvideIndex('noIndexing')]
16class HOTPSecret implements ValueObjectInterface
17{
18    use CompositeValueObject;
19    use NoIndexing;
20
21    private string $secret;
22
23    private int $counter;
24
25    public function __construct(HOTP $hotp)
26    {
27        $this->secret = $hotp->getSecret();
28        $this->counter = $hotp->getCounter();
29    }
30
31    public static function createRandom(): self
32    {
33        return new self(HOTP::create());
34    }
35
36    public function getSecret(): string
37    {
38        return $this->secret;
39    }
40
41    public function getCounter(): string
42    {
43        return $this->counter;
44    }
45
46    public function getUrl(string $label): string
47    {
48        $tmp = HOTP::create($this->secret, $this->counter);
49        $tmp->setLabel($label);
50        return (new QRCode)->render($tmp->getProvisioningUri());
51    }
52
53    public function getQrCodeUri(string $label): string
54    {
55        $tmp = HOTP::create($this->secret, $this->counter);
56        $tmp->setLabel($label);
57        return $tmp->getQrCodeUri(
58            'https://api.qrserver.com/v1/create-qr-code/?data=[DATA]&size=300x300&ecc=M',
59            '[DATA]'
60        );
61    }
62
63    private function validateState(): void
64    {
65        $errors = [];
66        if ($this->counter < 0) {
67            $errors['counter'] = 'Counter should higher than or equal to 0';
68        }
69        if (!preg_match('/^[A-Z0-9]{103}$/', $this->secret)) {
70            $errors['secret'] = 'Secret is not in valid format';
71        }
72
73        if (!empty($errors)) {
74            throw new ValidationException(new StringHashmap($errors));
75        }
76    }
77
78    public function createOTP(): OTP
79    {
80        return new OTP(HOTP::create($this->secret, $this->counter)->at($this->counter));
81    }
82
83    public function nextPassword(): self
84    {
85        $res = clone $this;
86        $res->counter++;
87        return $res;
88    }
89
90    public function verify(OTP $otp): bool
91    {
92        $hotp = HOTP::create($this->secret, $this->counter);
93        return $hotp->verify($otp->toNative());
94    }
95}