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

Auth   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 324
Duplicated Lines 1.85 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 6
loc 324
rs 8.8
c 0
b 0
f 0
wmc 36
lcom 1
cbo 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A buildLookup() 0 8 1
A getRange() 0 4 1
A setRange() 0 8 2
A setInitKey() 3 8 2
A getInitKey() 0 4 1
A setLookup() 0 8 2
A getLookup() 0 4 1
A getRefresh() 0 4 1
A setRefresh() 0 8 2
A getCodeLength() 0 4 1
A setCodeLength() 0 5 1
B validateCode() 0 19 7
A generateOneTime() 0 14 3
A generateCode() 0 11 2
A generateTimestamp() 0 4 1
A truncateHash() 0 11 1
B base32_decode() 3 26 4
A getQrCodeUrl() 0 5 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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
0 ignored issues
show
Documentation introduced by
Should the type for parameter $initKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $range can also be of type double or string. However, the property $range is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $seconds can also be of type double or string. However, the property $refreshSeconds is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the type for parameter $initKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
213
	 * @param string $timestamp Timestamp for calculation
0 ignored issues
show
Documentation introduced by
Should the type for parameter $timestamp not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
214
	 * @param integer $range Seconds before/after to validate hash against
0 ignored issues
show
Documentation introduced by
Should the type for parameter $range not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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
Bug introduced by
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
Bug introduced by
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]
0 ignored issues
show
Documentation introduced by
Should the type for parameter $initKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
242
	 * @param string $timestamp Timestamp for calculation [optional]
0 ignored issues
show
Documentation introduced by
Should the type for parameter $timestamp not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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
Bug introduced by
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
Bug introduced by
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
0 ignored issues
show
Documentation introduced by
Should the return type not be double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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
?>