summaryrefslogtreecommitdiff
path: root/vendor/christian-riesen/otp/src/Otp/GoogleAuthenticator.php
blob: 23e67ff051e35351eaa093714b9ca25da41293a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<?php
namespace Otp;

/**
 * Google Authenticator
 *
 * Last update: 2014-08-19
 *
 * Can be easy used with Google Authenticator
 * @link https://code.google.com/p/google-authenticator/
 *
 * @author Christian Riesen <chris.riesen@gmail.com>
 * @link http://christianriesen.com
 * @license MIT License see LICENSE file
 */

class GoogleAuthenticator
{
    protected static $allowedTypes = array('hotp', 'totp');

    protected static $height = 200;
    protected static $width = 200;

    /**
     * Returns the Key URI
     *
     * Format of encoded url is here:
     * https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
     * Should be done in a better fashion
     *
     * @param string $type totp or hotp
     * @param string $label Label to display this as to the user
     * @param string $secret Base32 encoded secret
     * @param integer $counter Required by hotp, otherwise ignored
     * @param array $options Optional fields that will be set if present
     *
     * @return string Key URI
     */
    public static function getKeyUri($type, $label, $secret, $counter = null, $options = array())
    {
        // two types only..
        if (!in_array($type, self::$allowedTypes)) {
            throw new \InvalidArgumentException('Type has to be of allowed types list');
        }

        // Label can't be empty
        $label = trim($label);

        if (strlen($label) < 1) {
            throw new \InvalidArgumentException('Label has to be one or more printable characters');
        }

        if (substr_count($label, ':') > 2) {
        	throw new \InvalidArgumentException('Account name contains illegal colon characters');
        }

        // Secret needs to be here
        if (strlen($secret) < 1) {
            throw new \InvalidArgumentException('No secret present');
        }

        // check for counter on hotp
        if ($type == 'hotp' && is_null($counter)) {
            throw new \InvalidArgumentException('Counter required for hotp');
        }

        // This is the base, these are at least required
        $otpauth = 'otpauth://' . $type . '/' . str_replace(array(':', ' '), array('%3A', '%20'), $label) . '?secret=' . rawurlencode($secret);

        if ($type == 'hotp' && !is_null($counter)) {
            $otpauth .= '&counter=' . intval($counter);
        }

        // Now check the options array

        // algorithm (currently ignored by Authenticator)
        // Defaults to SHA1
        if (array_key_exists('algorithm', $options)) {
            $otpauth .= '&algorithm=' . rawurlencode($options['algorithm']);
        }

        // digits (currently ignored by Authenticator)
        // Defaults to 6
        if (array_key_exists('digits', $options) && intval($options['digits']) !== 6 && intval($options['digits']) !== 8) {
        	throw new \InvalidArgumentException('Digits can only have the values 6 or 8, ' . $options['digits'] . ' given');
        } elseif (array_key_exists('digits', $options)) {
            $otpauth .= '&digits=' . intval($options['digits']);
        }

        // period, only for totp (currently ignored by Authenticator)
        // Defaults to 30
        if ($type == 'totp' && array_key_exists('period', $options)) {
            $otpauth .= '&period=' . rawurlencode($options['period']);
        }

        // issuer
        // Defaults to none
        if (array_key_exists('issuer', $options)) {
            $otpauth .= '&issuer=' . rawurlencode($options['issuer']);
        }

        return $otpauth;
    }


    /**
     * Returns the QR code url
     *
     * Format of encoded url is here:
     * https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
     * Should be done in a better fashion
     *
     * @param string $type totp or hotp
     * @param string $label Label to display this as to the user
     * @param string $secret Base32 encoded secret
     * @param integer $counter Required by hotp, otherwise ignored
     * @param array $options Optional fields that will be set if present
     *
     * @return string URL to the QR code
     */
    public static function getQrCodeUrl($type, $label, $secret, $counter = null, $options = array())
    {
        // Width and height can be overwritten
        $width = self::$width;

        if (array_key_exists('width', $options) && is_numeric($options['width'])) {
            $width = $options['width'];
        }

        $height = self::$height;

        if (array_key_exists('height', $options) && is_numeric($options['height'])) {
            $height = $options['height'];
        }

        $otpauth = self::getKeyUri($type, $label, $secret, $counter, $options);

        $url = 'https://chart.googleapis.com/chart?chs=' . $width . 'x'
             . $height . '&cht=qr&chld=M|0&chl=' . urlencode($otpauth);

        return $url;
    }

    /**
     * Creates a pseudo random Base32 string
     *
     * This could decode into anything. It's located here as a small helper
     * where code that might need base32 usually also needs something like this.
     *
     * @param integer $length Exact length of output string
     * @return string Base32 encoded random
     */
    public static function generateRandom($length = 16)
    {
        $keys = array_merge(range('A','Z'), range(2,7)); // No padding char

        $string = '';

        for ($i = 0; $i < $length; $i++) {
            $string .= $keys[self::getRand()];
        }

        return $string;
    }

    /**
     * Get random number
     *
     * @return int Random number between 0 and 31 (including)
     */
    private static function getRand()
    {
        if (function_exists('random_int')) {
            // Uses either the PHP7 internal function or the polyfill if present
            return random_int(0, 31);
        } elseif (function_exists('openssl_random_pseudo_bytes')) {
            $bytes = openssl_random_pseudo_bytes(2);
            $number = hexdec(bin2hex($bytes));

            if ($number > 31) {
                $number = $number % 32;
            }

            return $number;
        } else {
            return mt_rand(0, 31);
        }
    }
}