Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Sources/Class-TOTP.php (7 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
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^[' . implo...kup())) . ']+$/', $key) of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
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;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $range of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
225
		$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp;
0 ignored issues
show
It seems like you are loosely comparing $timestamp of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
226
		$initKey = ($initKey == null) ? $this->getInitKey() : $initKey;
0 ignored issues
show
It seems like you are loosely comparing $initKey of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
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;
0 ignored issues
show
It seems like you are loosely comparing $initKey of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
248
		$timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp;
0 ignored issues
show
It seems like you are loosely comparing $timestamp of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
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
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^[' . implo...okup)) . ']+$/', $hash) of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
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
?>