Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace OTPHP;
6
 
1441 ariadna 7
use Exception;
8
use InvalidArgumentException;
1 efrain 9
use ParagonIE\ConstantTime\Base32;
1441 ariadna 10
use RuntimeException;
11
use function assert;
12
use function chr;
13
use function count;
14
use function is_string;
15
use const STR_PAD_LEFT;
1 efrain 16
 
17
abstract class OTP implements OTPInterface
18
{
19
    use ParameterTrait;
20
 
1441 ariadna 21
    private const DEFAULT_SECRET_SIZE = 64;
22
 
1 efrain 23
    /**
1441 ariadna 24
     * @param non-empty-string $secret
1 efrain 25
     */
1441 ariadna 26
    protected function __construct(string $secret)
1 efrain 27
    {
28
        $this->setSecret($secret);
29
    }
30
 
1441 ariadna 31
    public function getQrCodeUri(string $uri, string $placeholder): string
1 efrain 32
    {
33
        $provisioning_uri = urlencode($this->getProvisioningUri());
34
 
35
        return str_replace($placeholder, $provisioning_uri, $uri);
36
    }
37
 
38
    /**
1441 ariadna 39
     * @param 0|positive-int $input
1 efrain 40
     */
1441 ariadna 41
    public function at(int $input): string
1 efrain 42
    {
1441 ariadna 43
        return $this->generateOTP($input);
44
    }
1 efrain 45
 
1441 ariadna 46
    /**
47
     * @return non-empty-string
48
     */
49
    final protected static function generateSecret(): string
50
    {
51
        return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
1 efrain 52
    }
53
 
54
    /**
1441 ariadna 55
     * The OTP at the specified input.
56
     *
57
     * @param 0|positive-int $input
58
     *
59
     * @return non-empty-string
1 efrain 60
     */
1441 ariadna 61
    protected function generateOTP(int $input): string
1 efrain 62
    {
1441 ariadna 63
        $hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
64
        $unpacked = unpack('C*', $hash);
65
        $unpacked !== false || throw new InvalidArgumentException('Invalid data.');
66
        $hmac = array_values($unpacked);
67
 
68
        $offset = ($hmac[count($hmac) - 1] & 0xF);
69
        $code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
70
        $otp = $code % (10 ** $this->getDigits());
71
 
72
        return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);
1 efrain 73
    }
74
 
75
    /**
1441 ariadna 76
     * @param array<non-empty-string, mixed> $options
1 efrain 77
     */
1441 ariadna 78
    protected function filterOptions(array &$options): void
1 efrain 79
    {
1441 ariadna 80
        foreach ([
81
            'algorithm' => 'sha1',
82
            'period' => 30,
83
            'digits' => 6,
84
        ] as $key => $default) {
1 efrain 85
            if (isset($options[$key]) && $default === $options[$key]) {
86
                unset($options[$key]);
87
            }
88
        }
89
 
90
        ksort($options);
91
    }
92
 
93
    /**
1441 ariadna 94
     * @param non-empty-string $type
95
     * @param array<non-empty-string, mixed> $options
1 efrain 96
     *
1441 ariadna 97
     * @return non-empty-string
1 efrain 98
     */
99
    protected function generateURI(string $type, array $options): string
100
    {
101
        $label = $this->getLabel();
1441 ariadna 102
        is_string($label) || throw new InvalidArgumentException('The label is not set.');
103
        $this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');
104
        $options = [...$options, ...$this->getParameters()];
1 efrain 105
        $this->filterOptions($options);
106
        $params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
107
 
1441 ariadna 108
        return sprintf(
109
            'otpauth://%s/%s?%s',
110
            $type,
111
            rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),
112
            $params
113
        );
1 efrain 114
    }
115
 
116
    /**
1441 ariadna 117
     * @param non-empty-string $safe
118
     * @param non-empty-string $user
1 efrain 119
     */
1441 ariadna 120
    protected function compareOTP(string $safe, string $user): bool
121
    {
122
        return hash_equals($safe, $user);
123
    }
124
 
125
    /**
126
     * @return non-empty-string
127
     */
1 efrain 128
    private function getDecodedSecret(): string
129
    {
130
        try {
1441 ariadna 131
            $decoded = Base32::decodeUpper($this->getSecret());
132
        } catch (Exception) {
133
            throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
1 efrain 134
        }
1441 ariadna 135
        assert($decoded !== '');
1 efrain 136
 
1441 ariadna 137
        return $decoded;
1 efrain 138
    }
139
 
140
    private function intToByteString(int $int): string
141
    {
142
        $result = [];
1441 ariadna 143
        while ($int !== 0) {
1 efrain 144
            $result[] = chr($int & 0xFF);
145
            $int >>= 8;
146
        }
147
 
1441 ariadna 148
        return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT);
1 efrain 149
    }
150
}