Completed
Pull Request — master (#27)
by Paul
02:39 queued 11s
created

u2f::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 8
Ratio 100 %

Importance

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