Completed
Push — master ( 7ee1a3...efdf4a )
by Paul
02:33
created

u2f   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 17
Bugs 3 Features 4
Metric Value
wmc 50
c 17
b 3
f 4
lcom 1
cbo 15
dl 0
loc 506
rs 7.2559

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 20 2
A delete() 0 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   Complexity   

Complex Class

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
		$count = $this->update_session($sql_ary);
193
194
		if ($count != 1)
195
		{
196
			// Reset sessions table.
197
			$sql_ary['u2f_request'] = '';
198
			$this->update_session($sql_ary);
199
			throw new BadRequestHttpException('TFA_UNABLE_TO_UPDATE_SESSION');
200
		}
201
202
		$this->template->assign_vars(array(
203
			'U2F_REQ'				=> $registrations,
204
			'S_TFA_U2F'				=> true,
205
			'S_TFA_INCLUDE_HTML'	=> 'tfa_u2f_authenticate.html',
206
		));
207
	}
208
209
	/**
210
	 * Actual login procedure
211
	 * @param int $user_id
212
	 * @throws AccessDeniedHttpException
213
	 * @throws BadRequestHttpException
214
	 */
215
	public function login($user_id)
216
	{
217
		try
218
		{
219
			$sql = 'SELECT u2f_request 
220
						FROM ' . SESSIONS_TABLE . ' 
221
						WHERE
222
							session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
223
							session_user_id = ' . (int) $this->user->data['user_id'];
224
			$result = $this->db->sql_query($sql);
225
			$row = $this->db->sql_fetchrow($result);
226
			$this->db->sql_freeresult($result);
227
228
			if (!$row || empty($row['u2f_request']))
229
			{
230
				throw new AccessDeniedHttpException($this->user->lang('TFA_NO_ACCESS'));
231
			}
232
233
			$response = json_decode(htmlspecialchars_decode($this->request->variable('authenticate', '')));
234
235
			if (property_exists($response, 'errorCode'))
236
			{
237
				if ($response->errorCode == 4) // errorCode 4 means that this device wasn't registered
238
				{
239
					throw new AccessDeniedHttpException($this->user->lang('TFA_NOT_REGISTERED'));
240
				}
241
				throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG'));
242
			}
243
			$result = new AuthenticationResponse($response->signatureData, $response->clientData, $response->keyHandle); // Do not need to include errorCode, as we already handled it.
244
245
			/** @var \paul999\tfa\helper\registration_helper $reg */
246
			$reg = $this->u2f->doAuthenticate($this->convertRequests(json_decode($row['u2f_request'])), $this->getRegistrations($user_id), $result);
247
			$sql_ary = array(
248
				'counter' => $reg->getCounter(),
249
				'last_used' => time(),
250
			);
251
252
			$sql = 'UPDATE ' . $this->registration_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE registration_id = ' . (int) $reg->getId();
253
			$this->db->sql_query($sql);
254
		}
255
		catch (U2fError $error)
256
		{
257
			$this->createError($error);
258
		}
259
		catch (\InvalidArgumentException $invalid)
260
		{
261
			throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG') . '<br />' . $invalid->getMessage(), $invalid);
262
		}
263
	}
264
265
	/**
266
	 * @param array $requests
267
	 * @return array
268
	 */
269
	private function convertRequests($requests)
270
	{
271
		$result = array();
272
		foreach ($requests as $request)
273
		{
274
			$result[] = new SignRequest($request->challenge, $request->keyHandle, $request->appId);
275
		}
276
		return $result;
277
	}
278
279
	/**
280
	 * Start of registration
281
	 * @return string
282
	 */
283
	public function register_start()
284
	{
285
		$reg_data = $this->getRegistrations($this->user->data['user_id']);
286
287
		$data = $this->u2f->getRegisterData($reg_data);
288
289
		$sql_ary = array(
290
			'u2f_request' => json_encode($data[0], JSON_UNESCAPED_SLASHES),
291
		);
292
293
		$count = $this->update_session($sql_ary);
294
295
		if ($count != 0)
296
		{
297
			// Reset sessions table. We had multiple sessions with same ID!!!
298
			$sql_ary['u2f_request'] = '';
299
			$this->update_session($sql_ary);
300
301
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
302
		}
303
304
		$this->template->assign_vars(array(
305
			'U2F_REG'           => true,
306
			'U2F_SIGN_REQUEST'  => json_encode($data[0], JSON_UNESCAPED_SLASHES),
307
			'U2F_SIGN'          => json_encode($data[1], JSON_UNESCAPED_SLASHES),
308
		));
309
310
		return 'tfa_u2f_ucp_new';
311
	}
312
313
	/**
314
	 * Actual registration
315
	 * @return void
316
	 * @throws BadRequestHttpException
317
	 */
318
	public function register()
319
	{
320
		try
321
		{
322
			$register = json_decode($this->user->data['u2f_request']);
323
			$response = json_decode(htmlspecialchars_decode($this->request->variable('register', '')));
324
			$error = 0;
325
326
			if (property_exists($response, 'errorCode'))
327
			{
328
				$error = $response->errorCode;
329
			}
330
331
			$registerrequest = new RegisterRequest($register->challenge, $register->appId);
332
			$responserequest = new RegisterResponse($response->registrationData, $response->clientData, $error);
333
334
			$reg = $this->u2f->doRegister($registerrequest, $responserequest);
335
336
			$sql_ary = array(
337
				'user_id' => $this->user->data['user_id'],
338
				'key_handle' => $reg->getKeyHandle(),
339
				'public_key' => $reg->getPublicKey(),
340
				'certificate' => $reg->getCertificate(),
341
				'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
342
				'registered' => time(),
343
				'last_used' => time(),
344
			);
345
346
			$sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
347
			$this->db->sql_query($sql);
348
349
			$sql_ary = array(
350
				'u2f_request' => '',
351
			);
352
353
			$this->update_session($sql_ary);
354
		}
355
		catch (U2fError $err)
356
		{
357
			$this->createError($err);
358
		}
359
	}
360
361
	/**
362
	 * This method is called to show the UCP page.
363
	 * You can assign template variables to the template, or do anything else here.
364
	 */
365
	public function show_ucp()
366
	{
367
		$sql = 'SELECT *
368
			FROM ' . $this->registration_table . '
369
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
370
			ORDER BY registration_id ASC';
371
372
		$result = $this->db->sql_query($sql);
373
374
		while ($row = $this->db->sql_fetchrow($result))
375
		{
376
			$this->template->assign_block_vars('keys', array(
377
				'CLASS'         => $this->get_name(),
378
				'ID'            => $row['registration_id'],
379
				'REGISTERED'    => $this->user->format_date($row['registered']),
380
				'LAST_USED'     => $this->user->format_date($row['last_used']),
381
			));
382
		}
383
		$this->db->sql_freeresult($result);
384
	}
385
386
	/**
387
	 * Delete a specific row from the UCP.
388
	 * The data is based on the data provided in show_ucp.
389
	 * @param int $key
390
	 * @return void
391
	 */
392
	public function delete($key)
393
	{
394
			$sql = 'DELETE FROM ' . $this->registration_table . '
395
						WHERE user_id = ' . (int) $this->user->data['user_id'] . '
396
						AND registration_id =' . (int) $key;
397
398
			$this->db->sql_query($sql);
399
	}
400
401
	/**
402
	 * If this module can add new keys (Or other things)
403
	 *
404
	 * @return boolean
405
	 */
406
	public function can_register()
407
	{
408
		return $this->is_potentially_usable(false);
409
	}
410
411
	/**
412
	 * Return the name of the current module
413
	 * This is for internal use only
414
	 * @return string
415
	 */
416
	public function get_name()
417
	{
418
		return 'u2f';
419
	}
420
421
	/**
422
	 * Get a language key for this specific module.
423
	 * @return string
424
	 */
425
	public function get_translatable_name()
426
	{
427
		return 'MODULE_U2F';
428
	}
429
430
	/**
431
	 * Select all registration objects from the database
432
	 * @param integer $user_id
433
	 * @return array
434
	 */
435
	private function getRegistrations($user_id)
436
	{
437
		$sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
438
		$result = $this->db->sql_query($sql);
439
		$rows = array();
440
441
		while ($row = $this->db->sql_fetchrow($result))
442
		{
443
			$reg = new registration_helper();
444
			$reg->setCounter($row['counter']);
445
			$reg->setCertificate($row['certificate']);
446
			$reg->setKeyHandle($row['key_handle']);
447
			$reg->setPublicKey($row['public_key']);
448
			$reg->setId($row['registration_id']);
449
450
			$rows[] = $reg;
451
		}
452
453
		$this->db->sql_freeresult($result);
454
		return $rows;
455
	}
456
457
	/**
458
	 * @param U2fError $error
459
	 * @throws BadRequestHttpException
460
	 */
461
	private function createError(U2fError $error)
462
	{
463
		switch ($error->getCode())
464
		{
465
			/** Error for the authentication message not matching any outstanding
466
			 * authentication request */
467
			case U2fError::ERR_NO_MATCHING_REQUEST:
468
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REQUEST'), $error);
469
470
			/** Error for the authentication message not matching any registration */
471
			case U2fError::ERR_NO_MATCHING_REGISTRATION:
472
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REGISTRATION'), $error);
473
474
			/** Error for the signature on the authentication message not verifying with
475
			 * the correct key */
476
			case U2fError::ERR_AUTHENTICATION_FAILURE:
477
				throw new BadRequestHttpException($this->user->lang('ERR_AUTHENTICATION_FAILURE'), $error);
478
479
			/** Error for the challenge in the registration message not matching the
480
			 * registration challenge */
481
			case U2fError::ERR_UNMATCHED_CHALLENGE:
482
				throw new BadRequestHttpException($this->user->lang('ERR_UNMATCHED_CHALLENGE'), $error);
483
484
			/** Error for the attestation signature on the registration message not
485
			 * verifying */
486
			case U2fError::ERR_ATTESTATION_SIGNATURE:
487
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_SIGNATURE'), $error);
488
489
			/** Error for the attestation verification not verifying */
490
			case U2fError::ERR_ATTESTATION_VERIFICATION:
491
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_VERIFICATION'), $error);
492
493
			/** Error for not getting good random from the system */
494
			case U2fError::ERR_BAD_RANDOM:
495
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_RANDOM'), $error);
496
497
			/** Error when the counter is lower than expected */
498
			case U2fError::ERR_COUNTER_TOO_LOW:
499
				throw new BadRequestHttpException($this->user->lang('ERR_COUNTER_TOO_LOW'), $error);
500
501
			/** Error decoding public key */
502
			case U2fError::ERR_PUBKEY_DECODE:
503
				throw new BadRequestHttpException($this->user->lang('ERR_PUBKEY_DECODE'), $error);
504
505
			/** Error user-agent returned error */
506
			case U2fError::ERR_BAD_UA_RETURNING:
507
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_UA_RETURNING'), $error);
508
509
			/** Error old OpenSSL version */
510
			case U2fError::ERR_OLD_OPENSSL:
511
				throw new BadRequestHttpException(sprintf($this->user->lang('ERR_OLD_OPENSSL'), OPENSSL_VERSION_TEXT), $error);
512
513
			default:
514
				throw new BadRequestHttpException($this->user->lang('TFA_UNKNOWN_ERROR'), $error);
515
		}
516
	}
517
518
	/**
519
	 * Update the session with new TFA data
520
	 * @param $sql_ary
521
	 * @return int
522
	 */
523
	private function update_session($sql_ary)
524
	{
525
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
526
							WHERE
527
								session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
528
								session_user_id = ' . (int) $this->user->data['user_id'];
529
		$this->db->sql_query($sql);
530
531
		return $this->db->sql_affectedrows();
532
	}
533
}
534