Completed
Push — master ( d14778...e59fd5 )
by Paul
03:22
created

u2f   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 533
Duplicated Lines 4.13 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 11
Bugs 3 Features 3
Metric Value
wmc 53
c 11
b 3
f 3
lcom 1
cbo 13
dl 22
loc 533
rs 7.4757

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