Completed
Pull Request — master (#27)
by Paul
02:15
created

u2f::is_usable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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