u2f   B
last analyzed

Complexity

Total Complexity 52

Size/Duplication

Total Lines 492
Duplicated Lines 3.66 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 9
dl 18
loc 492
rs 7.44
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 10 10 1
A getU2f() 0 8 2
A is_enabled() 0 4 1
A is_usable() 0 8 2
A is_potentially_usable() 0 5 3
A is_ssl() 0 13 4
A key_registered() 0 4 1
A get_priority() 0 4 1
A login_start() 0 23 2
B login() 0 53 7
A convertRequests() 0 9 2
A register_start() 0 29 2
A register() 0 42 4
A show_ucp() 0 4 1
A delete() 8 8 1
A can_register() 0 4 1
A get_name() 0 4 1
A get_translatable_name() 0 4 1
A getRegistrations() 0 21 2
C createError() 0 56 12
A update_session() 0 10 1

How to fix   Duplicated Code    Complexity   

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:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like u2f often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use u2f, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 *
4
 * 2FA extension for the phpBB Forum Software package.
5
 *
6
 * @copyright (c) 2015 Paul Sohier
7
 * @license GNU General Public License, version 2 (GPL-2.0)
8
 *
9
 */
10
11
namespace paul999\tfa\modules;
12
13
use paul999\tfa\helper\registration_helper;
14
use paul999\u2f\AuthenticationResponse;
15
use paul999\u2f\Exceptions\U2fError;
16
use paul999\u2f\RegisterRequest;
17
use paul999\u2f\RegisterResponse;
18
use paul999\u2f\SignRequest;
19
use phpbb\db\driver\driver_interface;
20
use phpbb\exception\http_exception;
21
use phpbb\request\request_interface;
22
use phpbb\template\template;
23
use phpbb\user;
24
25
class u2f extends abstract_module
26
{
27
	/**
28
	 * @var request_interface
29
	 */
30
	private $request;
31
32
	/**
33
	 * @var string
34
	 */
35
	private $registration_table;
36
37
	/**
38
	 * @var string
39
	 */
40
	private $root_path;
41
42
	/**
43
	 * @var \paul999\u2f\U2F
44
	 */
45
	private $u2f;
46
47
	/**
48
	 * u2f constructor.
49
	 * @param driver_interface $db
50
	 * @param user $user
51
	 * @param request_interface $request
52
	 * @param template $template
53
	 * @param string $registration_table
54
	 * @param string $root_path
55
	 */
56 View Code Duplication
	public function __construct(driver_interface $db, user $user, request_interface $request, template $template, $registration_table, $root_path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
57
	{
58
		$this->db       = $db;
59
		$this->user     = $user;
60
		$this->request  = $request;
61
		$this->template = $template;
62
		$this->root_path= $root_path;
63
64
		$this->registration_table	= $registration_table;
65
	}
66
67
	private function getU2f()
68
	{
69
		if (empty($this->u2f))
70
		{
71
			$this->u2f = new \paul999\u2f\U2F('https://' . $this->request->server('HTTP_HOST'));
72
		}
73
		return $this->u2f;
74
	}
75
76
	/**
77
	 * Return if this module is enabled by the admin
78
	 * (And all server requirements are met).
79
	 *
80
	 * Do not return false in case a specific user disabeld this module,
81
	 * OR if the user is unable to use this specific module.
82
	 * @return boolean
83
	 */
84
	public function is_enabled()
85
	{
86
		return true;
87
	}
88
89
	/**
90
	 * Check if the current user is able to use this module.
91
	 *
92
	 * This means that the user enabled it in the UCP,
93
	 * And has it setup up correctly.
94
	 * This method will be called during login, not during registration/
95
	 *
96
	 * @param int $user_id
97
	 * @return bool
98
	 */
99
	public function is_usable($user_id)
100
	{
101
		if (!$this->is_potentially_usable($user_id))
102
		{
103
			return false;
104
		}
105
		return $this->check_table_for_user($this->registration_table, $user_id);
106
	}
107
108
	/**
109
	 * Check if the user can potentially use this.
110
	 * This method is called at registration page.
111
	 *
112
	 * You can, for example, check if the current browser is suitable.
113
	 *
114
	 * @param int|boolean $user_id Use false to ignore user
115
	 * @return bool
116
	 */
117
	public function is_potentially_usable($user_id = false)
118
	{
119
		$user_agent = strtolower($this->request->server('HTTP_USER_AGENT'));
120
		return strpos($user_agent, 'edge') === false && strpos($user_agent, 'chrome') !== false && $this->is_ssl();
121
	}
122
123
	/**
124
	 * Check if the current session is secure.
125
	 *
126
	 * @return bool
127
	 */
128
	private function is_ssl()
129
	{
130
		$secure = $this->request->server('HTTPS');
131
		if (!empty($secure))
132
		{
133
			return 'on' === strtolower($secure) || '1' == $secure;
134
		}
135
		else if ('443' == $this->request->server('SERVER_PORT'))
136
		{
137
			return true;
138
		}
139
		return false;
140
	}
141
142
	/**
143
	 * Check if the user has any key registered with this module.
144
	 * There should be no check done if the key is usable, it should
145
	 * only return if a key is registered.
146
	 *
147
	 * @param $user_id
148
	 * @return bool
149
	 */
150
	public function key_registered($user_id)
151
	{
152
		return $this->check_table_for_user($this->registration_table, $user_id);
153
	}
154
155
	/**
156
	 * Get the priority for this module.
157
	 * A lower priority means more chance it gets selected as default option
158
	 *
159
	 * There can be only one module with a specific priority!
160
	 * If there is already a module registered with this priority,
161
	 * a Exception might be thrown
162
	 *
163
	 * @return int
164
	 */
165
	public function get_priority()
166
	{
167
		return 10;
168
	}
169
170
	/**
171
	 * Start of the login procedure.
172
	 * @param int $user_id
173
	 * @return array
174
	 * @throws http_exception
175
	 * @throws U2fError
176
	 */
177
	public function login_start($user_id)
178
	{
179
		$registrations = json_encode($this->getU2f()->getAuthenticateData($this->getRegistrations($user_id)), JSON_UNESCAPED_SLASHES);
180
181
		$sql_ary = array(
182
			'u2f_request'	=> $registrations
183
		);
184
185
		$count = $this->update_session($sql_ary);
186
187
		if ($count != 1)
188
		{
189
			// Reset sessions table.
190
			$sql_ary['u2f_request'] = '';
191
			$this->update_session($sql_ary);
192
			throw new http_exception(400, 'TFA_UNABLE_TO_UPDATE_SESSION');
193
		}
194
		$this->template->assign_var('U2F_REQ', $registrations);
195
196
		return array(
197
			'S_TFA_INCLUDE_HTML'	=> '@paul999_tfa/tfa_u2f_authenticate.html',
198
		);
199
	}
200
201
	/**
202
	 * Actual login procedure
203
	 *
204
	 * @param int $user_id
205
	 *
206
	 * @return bool
207
	 * @throws http_exception
208
	 */
209
	public function login($user_id)
210
	{
211
		$this->user->add_lang_ext('paul999/tfa', 'common');
212
		try
213
		{
214
			$sql = 'SELECT u2f_request 
215
				FROM ' . SESSIONS_TABLE . " 
216
				WHERE
217
					session_id = '" . $this->db->sql_escape($this->user->data['session_id']) . "' AND
218
					session_user_id = " . (int) $this->user->data['user_id'];
219
			$result = $this->db->sql_query($sql);
220
			$row = $this->db->sql_fetchrow($result);
221
			$this->db->sql_freeresult($result);
222
223
			if (!$row || empty($row['u2f_request']))
224
			{
225
				throw new http_exception(403, 'TFA_NO_ACCESS');
226
			}
227
228
			$response = json_decode(htmlspecialchars_decode($this->request->variable('authenticate', '')));
229
230
			if (property_exists($response, 'errorCode'))
231
			{
232
				if ($response->errorCode == 4) // errorCode 4 means that this device wasn't registered
233
				{
234
					throw new http_exception(403, 'TFA_NOT_REGISTERED');
235
				}
236
				throw new http_exception(400, 'TFA_SOMETHING_WENT_WRONG');
237
			}
238
			$result = new AuthenticationResponse($response->signatureData, $response->clientData, $response->keyHandle); // Do not need to include errorCode, as we already handled it.
239
240
			/** @var \paul999\tfa\helper\registration_helper $reg */
241
			$reg = $this->getU2f()->doAuthenticate($this->convertRequests(json_decode($row['u2f_request'])), $this->getRegistrations($user_id), $result);
242
			$sql_ary = array(
243
				'counter' => $reg->getCounter(),
244
				'last_used' => time(),
245
			);
246
247
			$sql = 'UPDATE ' . $this->registration_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE registration_id = ' . (int) $reg->getId();
248
			$this->db->sql_query($sql);
249
250
			return true;
251
		}
252
		catch (U2fError $error)
253
		{
254
			$this->createError($error);
255
		}
256
		catch (\InvalidArgumentException $invalid)
257
		{
258
			throw new http_exception(400, 'TFA_SOMETHING_WENT_WRONG');
259
		}
260
		return false;
261
	}
262
263
	/**
264
	 * @param array $requests
265
	 * @return array
266
	 */
267
	private function convertRequests($requests)
268
	{
269
		$result = array();
270
		foreach ($requests as $request)
271
		{
272
			$result[] = new SignRequest($request->challenge, $request->keyHandle, $request->appId);
273
		}
274
		return $result;
275
	}
276
277
	/**
278
	 * Start of registration
279
	 * @return string
280
	 * @throws U2fError
281
	 */
282
	public function register_start()
283
	{
284
		$reg_data = $this->getRegistrations($this->user->data['user_id']);
285
286
		$data = $this->getU2f()->getRegisterData($reg_data);
287
288
		$sql_ary = array(
289
			'u2f_request' => json_encode($data[0], JSON_UNESCAPED_SLASHES),
290
		);
291
292
		$count = $this->update_session($sql_ary);
293
294
		if ($count != 1)
295
		{
296
			// Reset sessions table. We had multiple sessions with same ID!!!
297
			$sql_ary['u2f_request'] = '';
298
			$this->update_session($sql_ary);
299
300
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
301
		}
302
303
		$this->template->assign_vars(array(
304
			'U2F_REG'           => true,
305
			'U2F_SIGN_REQUEST'  => json_encode($data[0], JSON_UNESCAPED_SLASHES),
306
			'U2F_SIGN'          => json_encode($data[1], JSON_UNESCAPED_SLASHES),
307
		));
308
309
		return 'tfa_u2f_ucp_new';
310
	}
311
312
	/**
313
	 * Actual registration
314
	 * @throws http_exception
315
	 */
316
	public function register()
317
	{
318
		try
319
		{
320
			$register = json_decode($this->user->data['u2f_request']);
321
			$response = json_decode(htmlspecialchars_decode($this->request->variable('register', '')));
322
			$error = 0;
323
324
			if (property_exists($response, 'errorCode'))
325
			{
326
				$error = $response->errorCode;
327
			}
328
329
			$registerrequest = new RegisterRequest($register->challenge, $register->appId);
330
			$responserequest = new RegisterResponse($response->registrationData, $response->clientData, $error);
331
332
			$reg = $this->getU2f()->doRegister($registerrequest, $responserequest);
333
334
			$sql_ary = array(
335
				'user_id' => $this->user->data['user_id'],
336
				'key_handle' => $reg->getKeyHandle(),
337
				'public_key' => $reg->getPublicKey(),
338
				'certificate' => $reg->getCertificate(),
339
				'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
340
				'registered' => time(),
341
				'last_used' => time(),
342
			);
343
344
			$sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
345
			$this->db->sql_query($sql);
346
347
			$sql_ary = array(
348
				'u2f_request' => '',
349
			);
350
351
			$this->update_session($sql_ary);
352
		}
353
		catch (U2fError $err)
354
		{
355
			$this->createError($err);
356
		}
357
	}
358
359
	/**
360
	 * This method is called to show the UCP page.
361
	 * You can assign template variables to the template, or do anything else here.
362
	 */
363
	public function show_ucp()
364
	{
365
		$this->show_ucp_complete($this->registration_table);
366
	}
367
368
	/**
369
	 * Delete a specific row from the UCP.
370
	 * The data is based on the data provided in show_ucp.
371
	 * @param int $key
372
	 * @return void
373
	 */
374 View Code Duplication
	public function delete($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
375
	{
376
		$sql = 'DELETE FROM ' . $this->registration_table . '
377
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
378
			AND registration_id =' . (int) $key;
379
380
		$this->db->sql_query($sql);
381
	}
382
383
	/**
384
	 * If this module can add new keys (Or other things)
385
	 *
386
	 * @return boolean
387
	 */
388
	public function can_register()
389
	{
390
		return $this->is_potentially_usable(false);
391
	}
392
393
	/**
394
	 * Return the name of the current module
395
	 * This is for internal use only
396
	 * @return string
397
	 */
398
	public function get_name()
399
	{
400
		return 'u2f';
401
	}
402
403
	/**
404
	 * Get a language key for this specific module.
405
	 * @return string
406
	 */
407
	public function get_translatable_name()
408
	{
409
		return 'TFA_U2F';
410
	}
411
412
	/**
413
	 * Select all registration objects from the database
414
	 * @param integer $user_id
415
	 * @return array
416
	 */
417
	private function getRegistrations($user_id)
418
	{
419
		$sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
420
		$result = $this->db->sql_query($sql);
421
		$rows = array();
422
423
		while ($row = $this->db->sql_fetchrow($result))
424
		{
425
			$reg = new registration_helper();
426
			$reg->setCounter($row['counter']);
427
			$reg->setCertificate($row['certificate']);
428
			$reg->setKeyHandle($row['key_handle']);
429
			$reg->setPublicKey($row['public_key']);
430
			$reg->setId($row['registration_id']);
431
432
			$rows[] = $reg;
433
		}
434
435
		$this->db->sql_freeresult($result);
436
		return $rows;
437
	}
438
439
	/**
440
	 * @param U2fError $error
441
	 * @throws http_exception
442
	 */
443
	private function createError(U2fError $error)
444
	{
445
		switch ($error->getCode())
446
		{
447
			/** Error for the authentication message not matching any outstanding
448
			 * authentication request */
449
			case U2fError::ERR_NO_MATCHING_REQUEST:
450
				throw new http_exception(400, 'ERR_NO_MATCHING_REQUEST', array(), $error);
451
452
			/** Error for the authentication message not matching any registration */
453
			case U2fError::ERR_NO_MATCHING_REGISTRATION:
454
				throw new http_exception(400, 'ERR_NO_MATCHING_REGISTRATION', array(), $error);
455
456
			/** Error for the signature on the authentication message not verifying with
457
			 * the correct key */
458
			case U2fError::ERR_AUTHENTICATION_FAILURE:
459
				throw new http_exception(400, 'ERR_AUTHENTICATION_FAILURE', array(), $error);
460
461
			/** Error for the challenge in the registration message not matching the
462
			 * registration challenge */
463
			case U2fError::ERR_UNMATCHED_CHALLENGE:
464
				throw new http_exception(400, 'ERR_UNMATCHED_CHALLENGE', array(), $error);
465
466
			/** Error for the attestation signature on the registration message not
467
			 * verifying */
468
			case U2fError::ERR_ATTESTATION_SIGNATURE:
469
				throw new http_exception(400, 'ERR_ATTESTATION_SIGNATURE', array(), $error);
470
471
			/** Error for the attestation verification not verifying */
472
			case U2fError::ERR_ATTESTATION_VERIFICATION:
473
				throw new http_exception(400, 'ERR_ATTESTATION_VERIFICATION', array(), $error);
474
475
			/** Error for not getting good random from the system */
476
			case U2fError::ERR_BAD_RANDOM:
477
				throw new http_exception(400, 'ERR_BAD_RANDOM', array(), $error);
478
479
			/** Error when the counter is lower than expected */
480
			case U2fError::ERR_COUNTER_TOO_LOW:
481
				throw new http_exception(400, 'ERR_COUNTER_TOO_LOW', array(), $error);
482
483
			/** Error decoding public key */
484
			case U2fError::ERR_PUBKEY_DECODE:
485
				throw new http_exception(400, 'ERR_PUBKEY_DECODE', array(), $error);
486
487
			/** Error user-agent returned error */
488
			case U2fError::ERR_BAD_UA_RETURNING:
489
				throw new http_exception(400, 'ERR_BAD_UA_RETURNING', array(), $error);
490
491
			/** Error old OpenSSL version */
492
			case U2fError::ERR_OLD_OPENSSL:
493
				throw new http_exception(400, 'ERR_OLD_OPENSSL', array(OPENSSL_VERSION_TEXT), $error);
494
495
			default:
496
				throw new http_exception(400, 'TFA_UNKNOWN_ERROR', array(), $error);
497
		}
498
	}
499
500
	/**
501
	 * Update the session with new TFA data
502
	 * @param $sql_ary
503
	 * @return int
504
	 */
505
	private function update_session($sql_ary)
506
	{
507
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . "
508
			WHERE
509
				session_id = '" . $this->db->sql_escape($this->user->data['session_id']) . "' AND
510
				session_user_id = " . (int) $this->user->data['user_id'];
511
		$this->db->sql_query($sql);
512
513
		return $this->db->sql_affectedrows();
514
	}
515
516
}
517