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