Completed
Push — master ( e6a1c6...980cfc )
by Paul
03:00
created

u2f   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 538
Duplicated Lines 4.09 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 14
Bugs 3 Features 4
Metric Value
wmc 53
c 14
b 3
f 4
lcom 1
cbo 15
dl 22
loc 538
rs 6.8582

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A is_enabled() 0 4 1
A is_usable() 0 16 3
A is_potentially_usable() 0 6 2
A is_ssl() 0 13 4
A get_priority() 0 4 1
B login_start() 0 32 3
C login() 0 49 7
A convertRequests() 0 9 2
B register_start() 11 52 4
B register() 0 42 4
A show_ucp() 0 21 2
A delete() 0 9 1
A can_register() 0 4 1
A get_name() 0 4 1
A get_translatable_name() 0 4 1
A getRegistrations() 11 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\RegisterRequestInterface;
18
use paul999\u2f\RegisterResponse;
19
use paul999\u2f\SignRequest;
20
use phpbb\db\driver\driver_interface;
21
use phpbb\request\request_interface;
22
use phpbb\template\template;
23
use phpbb\user;
24
use phpbrowscap\Browscap;
25
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
26
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
27
28
class u2f implements module_interface
29
{
30
31
	/**
32
	 * @var driver_interface
33
	 */
34
	private $db;
35
36
	/**
37
	 * @var user
38
	 */
39
	private $user;
40
41
	/**
42
	 * @var request_interface
43
	 */
44
	private $request;
45
46
	/**
47
	 * @var template
48
	 */
49
	private $template;
50
51
	/**
52
	 * @var string
53
	 */
54
	private $registration_table;
55
56
	/**
57
	 * @var string
58
	 */
59
	private $root_path;
60
61
	/**
62
	 * @var \paul999\u2f\U2F
63
	 */
64
	private $u2f;
65
66
	/**
67
	 * u2f constructor.
68
	 * @param driver_interface $db
69
	 * @param user $user
70
	 * @param request_interface $request
71
	 * @param template $template
72
	 * @param string $registration_table
73
	 * @param string $root_path
74
	 */
75
	public function __construct(driver_interface $db, user $user, request_interface $request, template $template, $registration_table, $root_path)
76
	{
77
		$this->db       = $db;
78
		$this->user     = $user;
79
		$this->request  = $request;
80
		$this->template = $template;
81
		$this->root_path= $root_path;
82
83
		$this->registration_table	= $registration_table;
84
85
		$this->u2f = new \paul999\u2f\U2F('https://' . $this->request->server('HTTP_HOST'));
86
	}
87
88
	/**
89
	 * Return if this module is enabled by the admin
90
	 * (And all server requirements are met).
91
	 *
92
	 * Do not return false in case a specific user disabeld this module,
93
	 * OR if the user is unable to use this specific module.
94
	 * @return boolean
95
	 */
96
	public function is_enabled()
97
	{
98
		return true;
99
	}
100
101
	/**
102
	 * Check if the current user is able to use this module.
103
	 *
104
	 * This means that the user enabled it in the UCP,
105
	 * And has it setup up correctly.
106
	 * This method will be called during login, not during registration/
107
	 *
108
	 * @param int $user_id
109
	 * @return bool
110
	 */
111
	public function is_usable($user_id)
112
	{
113
		if (!$this->is_potentially_usable($user_id))
114
		{
115
			return false;
116
		}
117
		$sql = 'SELECT COUNT(registration_id) as reg_id 
118
					FROM ' . $this->registration_table . ' 
119
					WHERE 
120
						user_id = ' . (int) $user_id;
121
		$result = $this->db->sql_query($sql);
122
		$row = $this->db->sql_fetchrow($result);
123
		$this->db->sql_freeresult($result);
124
125
		return $row && $row['reg_id'] > 0;
126
	}
127
128
	/**
129
	 * Check if the user can potentially use this.
130
	 * This method is called at registration page.
131
	 *
132
	 * You can, for example, check if the current browser is suitable.
133
	 *
134
	 * @param int|boolean $user_id Use false to ignore user
135
	 * @return bool
136
	 */
137
	public function is_potentially_usable($user_id = false)
138
	{
139
		$browsercap = new Browscap($this->root_path . 'cache/');
140
		$info = $browsercap->getBrowser($this->request->server('HTTP_USER_AGENT'));
141
		return strtolower($info->Browser) === 'chrome' && $this->is_ssl();
142
	}
143
144
	/**
145
	 * Check if the current session is secure.
146
	 *
147
	 * @return bool
148
	 */
149
	private function is_ssl()
150
	{
151
		$secure = $this->request->server('HTTPS');
152
		if (!empty($secure))
153
		{
154
			return 'on' == strtolower($secure) || '1' == $secure;
155
		}
156
		else if ('443' == $this->request->server('SERVER_PORT'))
157
		{
158
			return true;
159
		}
160
		return false;
161
	}
162
163
	/**
164
	 * Get the priority for this module.
165
	 * A lower priority means more chance it gets selected as default option
166
	 *
167
	 * There can be only one module with a specific priority!
168
	 * If there is already a module registered with this priority,
169
	 * a Exception might be thrown
170
	 *
171
	 * @return int
172
	 */
173
	public function get_priority()
174
	{
175
		return 10;
176
	}
177
178
	/**
179
	 * Start of the login procedure.
180
	 * @param int $user_id
181
	 * @return void
182
	 * @throws BadRequestHttpException
183
	 */
184
	public function login_start($user_id)
185
	{
186
		$registrations = json_encode($this->u2f->getAuthenticateData($this->getRegistrations($user_id)), JSON_UNESCAPED_SLASHES);
187
188
		$sql_ary = array(
189
			'u2f_request'	=> $registrations
190
		);
191
192
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
193
					WHERE
194
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
195
						session_user_id = ' . (int) $this->user->data['user_id'];
196
		$this->db->sql_query($sql);
197
		$count = $this->db->sql_affectedrows();
198
199
		if ($count != 1)
200
		{
201
			if ($count > 1)
202
			{
203
				// Reset sessions table. We had multiple sessions with same ID!!!
204
				$sql_ary['u2f_request'] = '';
205
				$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
206
					WHERE
207
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
208
						session_user_id = ' . (int) $this->user->data['user_id'];
209
				$this->db->sql_query($sql);
210
			}
211
			throw new BadRequestHttpException('TFA_UNABLE_TO_UPDATE_SESSION');
212
		}
213
214
		$this->template->assign_var('U2F_REQ', $registrations);
215
	}
216
217
	/**
218
	 * Actual login procedure
219
	 * @param int $user_id
220
	 * @throws AccessDeniedHttpException
221
	 */
222
	public function login($user_id)
223
	{
224
		try
225
		{
226
			$sql = 'SELECT u2f_request 
227
						FROM ' . SESSIONS_TABLE . ' 
228
						WHERE
229
							session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
230
							session_user_id = ' . (int) $this->user->data['user_id'];
231
			$result = $this->db->sql_query($sql);
232
			$row = $this->db->sql_fetchrow($result);
233
			$this->db->sql_freeresult($result);
234
235
			if (!$row || empty($row['u2f_request']))
236
			{
237
				throw new AccessDeniedHttpException($this->user->lang('TFA_NO_ACCESS'));
238
			}
239
240
			$response = json_decode(htmlspecialchars_decode($this->request->variable('authenticate', '')));
241
242
			if (property_exists($response, 'errorCode'))
243
			{
244
				if ($response->errorCode == 4) // errorCode 4 means that this device wasn't registered
245
				{
246
					throw new AccessDeniedHttpException($this->user->lang('TFA_NOT_REGISTERED'));
247
				}
248
				throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG'));
249
			}
250
			$result = new AuthenticationResponse($response->signatureData, $response->clientData, $response->keyHandle, $response->errorCode);
251
252
			/** @var \paul999\tfa\helper\registration_helper $reg */
253
			$reg = $this->u2f->doAuthenticate($this->convertRequests(json_decode($row['u2f_request'])), $this->getRegistrations($user_id), $result);
254
			$sql_ary = array(
255
				'counter' => $reg->getCounter(),
256
				'last_used' => time(),
257
			);
258
259
			$sql = 'UPDATE ' . $this->registration_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE registration_id = ' . (int) $reg->getId();
260
			$this->db->sql_query($sql);
261
		}
262
		catch (U2fError $error)
263
		{
264
			$this->createError($error);
265
		}
266
		catch (\InvalidArgumentException $invalid)
267
		{
268
			throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG') . '<br />' . $invalid->getMessage(), $invalid);
269
		}
270
	}
271
272
	/**
273
	 * @param array $requests
274
	 * @return array
275
	 */
276
	private function convertRequests($requests)
277
	{
278
		$result = array();
279
		foreach ($requests as $request)
280
		{
281
			$result[] = new SignRequest($request->challenge, $request->keyHandle, $request->appId);
282
		}
283
		return $result;
284
	}
285
286
	/**
287
	 * Start of registration
288
	 * @return string
289
	 */
290
	public function register_start()
291
	{
292
		$sql = 'SELECT *
293
			FROM ' . $this->registration_table . '
294
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
295
			ORDER BY registration_id ASC';
296
297
		$result = $this->db->sql_query($sql);
298
		$reg_data = array();
299
300 View Code Duplication
		while ($row = $this->db->sql_fetchrow($result))
0 ignored issues
show
Duplication introduced by
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...
301
		{
302
			$reg = new registration_helper();
303
			$reg->setCounter($row['counter']);
304
			$reg->setCertificate($row['certificate']);
305
			$reg->setKeyHandle($row['key_handle']);
306
			$reg->setPublicKey($row['public_key']);
307
			$reg->setId($row['registration_id']);
308
309
			$reg_data[] = $reg;
310
		}
311
		$this->db->sql_freeresult($result);
312
313
		$data = $this->u2f->getRegisterData($reg_data);
314
315
		$sql_ary = array(
316
			'u2f_request' => json_encode($data[0], JSON_UNESCAPED_SLASHES),
317
		);
318
319
		$count = $this->update_session($sql_ary);
320
321
		if ($count == 0)
322
		{
323
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
324
		}
325
		else if ($count > 1)
326
		{
327
			// Reset sessions table. We had multiple sessions with same ID!!!
328
			$sql_ary['u2f_request'] = '';
329
			$this->update_session($sql_ary);
330
331
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
332
		}
333
334
		$this->template->assign_vars(array(
335
			'U2F_REG'           => true,
336
			'U2F_SIGN_REQUEST'  => json_encode($data[0], JSON_UNESCAPED_SLASHES),
337
			'U2F_SIGN'          => json_encode($data[1], JSON_UNESCAPED_SLASHES),
338
		));
339
340
		return 'tfa_u2f_ucp_new';
341
	}
342
343
	/**
344
	 * Actual registration
345
	 * @return void
346
	 * @throws BadRequestHttpException
347
	 */
348
	public function register()
349
	{
350
		try
351
		{
352
			$register = json_decode($this->user->data['u2f_request']);
353
			$response = json_decode(htmlspecialchars_decode($this->request->variable('register', '')));
354
			$error = 0;
355
356
			if (property_exists($response, 'errorCode'))
357
			{
358
				$error = $response->errorCode;
359
			}
360
361
			$registerrequest = new RegisterRequest($register->challenge, $register->appId);
362
			$responserequest = new RegisterResponse($response->registrationData, $response->clientData, $error);
363
364
			$reg = $this->u2f->doRegister($registerrequest, $responserequest);
365
366
			$sql_ary = array(
367
				'user_id' => $this->user->data['user_id'],
368
				'key_handle' => $reg->getKeyHandle(),
369
				'public_key' => $reg->getPublicKey(),
370
				'certificate' => $reg->getCertificate(),
371
				'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
372
				'registered' => time(),
373
				'last_used' => time(),
374
			);
375
376
			$sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
377
			$this->db->sql_query($sql);
378
379
			$sql_ary = array(
380
				'u2f_request' => '',
381
			);
382
383
			$this->update_session($sql_ary);
384
		}
385
		catch (U2fError $err)
386
		{
387
			$this->createError($err);
388
		}
389
	}
390
391
	/**
392
	 * This method is called to show the UCP page.
393
	 * You can assign template variables to the template, or do anything else here.
394
	 */
395
	public function show_ucp()
396
	{
397
		$sql = 'SELECT *
398
			FROM ' . $this->registration_table . '
399
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
400
			ORDER BY registration_id ASC';
401
402
		$result = $this->db->sql_query($sql);
403
		//$this->reg_data = array();
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
404
405
		while ($row = $this->db->sql_fetchrow($result))
406
		{
407
			$this->template->assign_block_vars('keys', array(
408
				'CLASS'         => $this->get_name(),
409
				'ID'            => $row['registration_id'],
410
				'REGISTERED'    => $this->user->format_date($row['registered']),
411
				'LAST_USED'     => $this->user->format_date($row['last_used']),
412
			));
413
		}
414
		$this->db->sql_freeresult($result);
415
	}
416
417
	/**
418
	 * Delete a specific row from the UCP.
419
	 * The data is based on the data provided in show_ucp.
420
	 * @param int $key
421
	 * @return void
422
	 */
423
	public function delete($key)
424
	{
425
			$sql = 'DELETE FROM ' . $this->registration_table . '
426
						WHERE user_id = ' . (int) $this->user->data['user_id'] . '
427
						AND registration_id =' . (int) $key;
428
429
			$this->db->sql_query($sql);
430
431
	}
432
433
	/**
434
	 * If this module can add new keys (Or other things)
435
	 *
436
	 * @return boolean
437
	 */
438
	public function can_register()
439
	{
440
		return $this->is_potentially_usable(false);
441
	}
442
443
	/**
444
	 * Return the name of the current module
445
	 * This is for internal use only
446
	 * @return string
447
	 */
448
	public function get_name()
449
	{
450
		return 'u2f';
451
	}
452
453
	/**
454
	 * Get a language key for this specific module.
455
	 * @return string
456
	 */
457
	public function get_translatable_name()
458
	{
459
		return 'MODULE_U2F';
460
	}
461
462
	/**
463
	 * Select all registration objects from the database
464
	 * @param integer $user_id
465
	 * @return array
466
	 */
467
	private function getRegistrations($user_id)
468
	{
469
		$sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
470
		$result = $this->db->sql_query($sql);
471
		$rows = array();
472
473 View Code Duplication
		while ($row = $this->db->sql_fetchrow($result))
0 ignored issues
show
Duplication introduced by
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...
474
		{
475
			$reg = new registration_helper();
476
			$reg->setCounter($row['counter']);
477
			$reg->setCertificate($row['certificate']);
478
			$reg->setKeyHandle($row['key_handle']);
479
			$reg->setPublicKey($row['public_key']);
480
			$reg->setId($row['registration_id']);
481
482
			$rows[] = $reg;
483
		}
484
485
		$this->db->sql_freeresult($result);
486
		return $rows;
487
	}
488
489
	/**
490
	 * @param U2fError $error
491
	 * @throws BadRequestHttpException
492
	 */
493
	private function createError(U2fError $error)
494
	{
495
		switch ($error->getCode())
496
		{
497
			/** Error for the authentication message not matching any outstanding
498
			 * authentication request */
499
			case U2fError::ERR_NO_MATCHING_REQUEST:
500
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REQUEST'), $error);
501
502
			/** Error for the authentication message not matching any registration */
503
			case U2fError::ERR_NO_MATCHING_REGISTRATION:
504
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REGISTRATION'), $error);
505
506
			/** Error for the signature on the authentication message not verifying with
507
			 * the correct key */
508
			case U2fError::ERR_AUTHENTICATION_FAILURE:
509
				throw new BadRequestHttpException($this->user->lang('ERR_AUTHENTICATION_FAILURE'), $error);
510
511
			/** Error for the challenge in the registration message not matching the
512
			 * registration challenge */
513
			case U2fError::ERR_UNMATCHED_CHALLENGE:
514
				throw new BadRequestHttpException($this->user->lang('ERR_UNMATCHED_CHALLENGE'), $error);
515
516
			/** Error for the attestation signature on the registration message not
517
			 * verifying */
518
			case U2fError::ERR_ATTESTATION_SIGNATURE:
519
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_SIGNATURE'), $error);
520
521
			/** Error for the attestation verification not verifying */
522
			case U2fError::ERR_ATTESTATION_VERIFICATION:
523
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_VERIFICATION'), $error);
524
525
			/** Error for not getting good random from the system */
526
			case U2fError::ERR_BAD_RANDOM:
527
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_RANDOM'), $error);
528
529
			/** Error when the counter is lower than expected */
530
			case U2fError::ERR_COUNTER_TOO_LOW:
531
				throw new BadRequestHttpException($this->user->lang('ERR_COUNTER_TOO_LOW'), $error);
532
533
			/** Error decoding public key */
534
			case U2fError::ERR_PUBKEY_DECODE:
535
				throw new BadRequestHttpException($this->user->lang('ERR_PUBKEY_DECODE'), $error);
536
537
			/** Error user-agent returned error */
538
			case U2fError::ERR_BAD_UA_RETURNING:
539
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_UA_RETURNING'), $error);
540
541
			/** Error old OpenSSL version */
542
			case U2fError::ERR_OLD_OPENSSL:
543
				throw new BadRequestHttpException(sprintf($this->user->lang('ERR_OLD_OPENSSL'), OPENSSL_VERSION_TEXT), $error);
544
545
			default:
546
				throw new BadRequestHttpException($this->user->lang('TFA_UNKNOWN_ERROR'), $error);
547
		}
548
	}
549
550
	/**
551
	 * Update the session with new TFA data
552
	 * @param $sql_ary
553
	 * @return int
554
	 */
555
	private function update_session($sql_ary)
556
	{
557
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
558
							WHERE
559
								session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
560
								session_user_id = ' . (int) $this->user->data['user_id'];
561
		$this->db->sql_query($sql);
562
563
		return $this->db->sql_affectedrows();
564
	}
565
}
566