Completed
Push — master ( d043ad...19080b )
by Paul
03:04
created

u2f   C

Complexity

Total Complexity 49

Size/Duplication

Total Lines 475
Duplicated Lines 1.68 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 19
Bugs 3 Features 4
Metric Value
wmc 49
c 19
b 3
f 4
lcom 1
cbo 16
dl 8
loc 475
rs 6.4223

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