Completed
Push — master ( 0bdc28...70ec53 )
by Paul
9s
created

u2f::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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