Completed
Push — release-2.1 ( 6f6d35...abeae7 )
by Mathias
08:46
created

Sources/Class-TOTP.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace TOTP;
4
5
/**
6
 * A class for generating the codes compatible with the Google Authenticator and similar TOTP
7
 * clients.
8
 *
9
 * NOTE: A lot of the logic from this class has been borrowed from this class:
10
 * https://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt
11
 *
12
 * @author Chris Cornutt <[email protected]>
13
 * @package GAuth
14
 * @license MIT
15
 *
16
 * Simple Machines Forum (SMF)
17
 *
18
 * @package SMF
19
 * @author Simple Machines http://www.simplemachines.org
20
 * @copyright 2017 Simple Machines and individual contributors
21
 * @license http://www.simplemachines.org/about/smf/license.php BSD
22
 *
23
 * @version 2.1 Beta 4
24
 */
25
26
/**
27
 * Class Auth
28
 * @package TOTP
29
 */
30
class Auth
31
{
32
	/**
33
	 * @var array Internal lookup table
34
	 */
35
	private $lookup = array();
36
37
	/**
38
	 * @var string Initialization key
39
	 */
40
	private $initKey = null;
41
42
	/**
43
	 * @var integer Seconds between key refreshes
44
	 */
45
	private $refreshSeconds = 30;
46
47
	/**
48
	 * @var integer The length of codes to generate
49
	 */
50
	private $codeLength = 6;
51
52
	/**
53
	 * @var integer Range plus/minus for "window of opportunity" on allowed codes
54
	 */
55
	private $range = 2;
56
57
	/**
58
	 * Initialize the object and set up the lookup table
59
	 *     Optionally the Initialization key
60
	 *
61
	 * @param string $initKey Initialization key
62
	 */
63
	public function __construct($initKey = null)
64
	{
65
		$this->buildLookup();
66
67
		if ($initKey !== null) {
68
			$this->setInitKey($initKey);
69
		}
70
	}
71
72
	/**
73
	 * Build the base32 lookup table
74
	 */
75
	public function buildLookup()
76
	{
77
		$lookup = array_combine(
78
			array_merge(range('A', 'Z'), range(2, 7)),
79
			range(0, 31)
80
		);
81
		$this->setLookup($lookup);
82
	}
83
84
	/**
85
	 * Get the current "range" value
86
	 * @return integer Range value
87
	 */
88
	public function getRange()
89
	{
90
		return $this->range;
91
	}
92
93
	/**
94
	 * Set the "range" value
95
	 *
96
	 * @param integer $range Range value
97
	 * @return \TOTP\Auth instance
98
	 */
99
	public function setRange($range)
100
	{
101
		if (!is_numeric($range)) {
102
			throw new \InvalidArgumentException('Invalid window range');
103
		}
104
		$this->range = $range;
105
		return $this;
106
	}
107
108
	/**
109
	 * Set the initialization key for the object
110
	 *
111
	 * @param string $key Initialization key
112
	 * @throws \InvalidArgumentException If hash is not valid base32
113
	 * @return \TOTP\Auth instance
114
	 */
115
	public function setInitKey($key)
116
	{
117 View Code Duplication
		if (preg_match('/^[' . implode('', array_keys($this->getLookup())) . ']+$/', $key) == false) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
118
			throw new \InvalidArgumentException('Invalid base32 hash!');
119
		}
120
		$this->initKey = $key;
121
		return $this;
122
	}
123
124
	/**
125
	 * Get the current Initialization key
126
	 *
127
	 * @return string Initialization key
128
	 */
129
	public function getInitKey()
130
	{
131
		return $this->initKey;
132
	}
133
134
	/**
135
	 * Set the contents of the internal lookup table
136
	 *
137
	 * @param array $lookup Lookup data set
138
	 * @throws \InvalidArgumentException If lookup given is not an array
139
	 * @return \TOTP\Auth instance
140
	 */
141
	public function setLookup($lookup)
142
	{
143
		if (!is_array($lookup)) {
144
			throw new \InvalidArgumentException('Lookup value must be an array');
145
		}
146
		$this->lookup = $lookup;
147
		return $this;
148
	}
149
150
	/**
151
	 * Get the current lookup data set
152
	 *
153
	 * @return array Lookup data
154
	 */
155
	public function getLookup()
156
	{
157
		return $this->lookup;
158
	}
159
160
	/**
161
	 * Get the number of seconds for code refresh currently set
162
	 *
163
	 * @return integer Refresh in seconds
164
	 */
165
	public function getRefresh()
166
	{
167
		return $this->refreshSeconds;
168
	}
169
170
	/**
171
	 * Set the number of seconds to refresh codes
172
	 *
173
	 * @param integer $seconds Seconds to refresh
174
	 * @throws \InvalidArgumentException If seconds value is not numeric
175
	 * @return \TOTP\Auth instance
176
	 */
177
	public function setRefresh($seconds)
178
	{
179
		if (!is_numeric($seconds)) {
180
			throw new \InvalidArgumentException('Seconds must be numeric');
181
		}
182
		$this->refreshSeconds = $seconds;
183
		return $this;
184
	}
185
186
	/**
187
	 * Get the current length for generated codes
188
	 *
189
	 * @return integer Code length
190
	 */
191
	public function getCodeLength()
192
	{
193
		return $this->codeLength;
194
	}
195
196
	/**
197
	 * Set the length of the generated codes
198
	 *
199
	 * @param integer $length Code length
200
	 * @return \TOTP\Auth instance
201
	 */
202
	public function setCodeLength($length)
203
	{
204
		$this->codeLength = $length;
205
		return $this;
206
	}
207
208
	/**
209
	 * Validate the given code
210
	 *
211
	 * @param string $code Code entered by user
212
	 * @param string $initKey Initialization key
213
	 * @param string $timestamp Timestamp for calculation
214
	 * @param integer $range Seconds before/after to validate hash against
215
	 * @throws \InvalidArgumentException If incorrect code length
216
	 * @return boolean Pass/fail of validation
217
	 */
218
	public function validateCode($code, $initKey = null, $timestamp = null, $range = null)
219
	{
220
		if (strlen($code) !== $this->getCodeLength()) {
221
			throw new \InvalidArgumentException('Incorrect code length');
222
		}
223
224
		$range = ($range == null) ? $this->getRange() : $range;
225
		$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp;
226
		$initKey = ($initKey == null) ? $this->getInitKey() : $initKey;
227
228
		$binary = $this->base32_decode($initKey);
229
230
		for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) {
231
			if ($this->generateOneTime($binary, $time) == $code) {
232
				return true;
233
			}
234
		}
235
		return false;
236
	}
237
238
	/**
239
	 * Generate a one-time code
240
	 *
241
	 * @param string $initKey Initialization key [optional]
242
	 * @param string $timestamp Timestamp for calculation [optional]
243
	 * @return string Generated code/hash
244
	 */
245
	public function generateOneTime($initKey = null, $timestamp = null)
246
	{
247
		$initKey = ($initKey == null) ? $this->getInitKey() : $initKey;
248
		$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp;
249
250
		$hash = hash_hmac(
251
			'sha1',
252
			pack('N*', 0) . pack('N*', $timestamp),
253
			$initKey,
254
			true
255
		);
256
257
		return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT);
258
	}
259
260
	/**
261
	 * Generate a code/hash
262
	 *     Useful for making Initialization codes
263
	 *
264
	 * @param integer $length Length for the generated code
265
	 * @return string Generated code
266
	 */
267
	public function generateCode($length = 16)
268
	{
269
		$lookup = implode('', array_keys($this->getLookup()));
270
		$code = '';
271
272
		for ($i = 0; $i < $length; $i++) {
273
			$code .= $lookup[mt_rand(0, strlen($lookup) - 1)];
274
		}
275
276
		return $code;
277
	}
278
279
	/**
280
	 * Generate the timestamp for the calculation
281
	 *
282
	 * @return integer Timestamp
283
	 */
284
	public function generateTimestamp()
285
	{
286
		return floor(microtime(true) / $this->getRefresh());
287
	}
288
289
	/**
290
	 * Truncate the given hash down to just what we need
291
	 *
292
	 * @param string $hash Hash to truncate
293
	 * @return string Truncated hash value
294
	 */
295
	public function truncateHash($hash)
296
	{
297
		$offset = ord($hash[19]) & 0xf;
298
299
		return (
300
			((ord($hash[$offset + 0]) & 0x7f) << 24) |
301
			((ord($hash[$offset + 1]) & 0xff) << 16) |
302
			((ord($hash[$offset + 2]) & 0xff) << 8) |
303
			(ord($hash[$offset + 3]) & 0xff)
304
		) % pow(10, $this->getCodeLength());
305
	}
306
307
	/**
308
	 * Base32 decoding function
309
	 *
310
	 * @param string $hash The base32-encoded hash
311
	 * @throws \InvalidArgumentException When hash is not valid
312
	 * @return string Binary value of hash
313
	 */
314
	public function base32_decode($hash)
315
	{
316
		$lookup = $this->getLookup();
317
318 View Code Duplication
		if (preg_match('/^[' . implode('', array_keys($lookup)) . ']+$/', $hash) == false) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319
			throw new \InvalidArgumentException('Invalid base32 hash!');
320
		}
321
322
		$hash = strtoupper($hash);
323
		$buffer = 0;
324
		$length = 0;
325
		$binary = '';
326
327
		for ($i = 0; $i < strlen($hash); $i++) {
328
			$buffer = $buffer << 5;
329
			$buffer += $lookup[$hash[$i]];
330
			$length += 5;
331
332
			if ($length >= 8) {
333
				$length -= 8;
334
				$binary .= chr(($buffer & (0xFF << $length)) >> $length);
335
			}
336
		}
337
338
		return $binary;
339
	}
340
341
	/**
342
	 * Returns a URL to QR code for embedding the QR code
343
	 *
344
	 * @param string $name The name
345
	 * @param string $code The generated code
346
	 * @return string The URL to the QR code
347
	 */
348
	public function getQrCodeUrl($name, $code)
349
	{
350
		$urlencoded = urlencode('otpauth://totp/' . urlencode($name) . '?secret=' . $code);
351
		return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $urlencoded;
352
	}
353
}
354
355
?>