Completed
Push — master ( 8bdeb6...ac614c )
by Paul
03:21
created

u2f::get_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
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\SignRequest;
17
use phpbb\db\driver\driver_interface;
18
use phpbb\request\request_interface;
19
use phpbb\template\template;
20
use phpbb\user;
21
use phpbrowscap\Browscap;
22
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
23
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
24
25
class u2f implements module_interface
26
{
27
28
	/**
29
	 * @var driver_interface
30
	 */
31
	private $db;
32
33
	/**
34
	 * @var user
35
	 */
36
	private $user;
37
38
	/**
39
	 * @var request_interface
40
	 */
41
	private $request;
42
43
	/**
44
	 * @var template
45
	 */
46
	private $template;
47
48
	/**
49
	 * @var string
50
	 */
51
	private $registration_table;
52
53
	/**
54
	 * @var string
55
	 */
56
	private $root_path;
57
58
	/**
59
	 * @var \paul999\u2f\U2F
60
	 */
61
	private $u2f;
62
63
	/**
64
	 * @var array
65
	 */
66
	private $reg_data;
67
68
	/**
69
	 * u2f constructor.
70
	 * @param driver_interface $db
71
	 * @param user $user
72
	 * @param request_interface $request
73
	 * @param template $template
74
	 * @param string $registration_table
75
	 * @param string $root_path
76
	 */
77
	public function __construct(driver_interface $db, user $user, request_interface $request, template $template, $registration_table, $root_path)
78
	{
79
		$this->db       = $db;
80
		$this->user     = $user;
81
		$this->request  = $request;
82
		$this->template = $template;
83
		$this->root_path= $root_path;
84
85
		$this->registration_table	= $registration_table;
86
87
		$this->u2f = new \paul999\u2f\U2F('https://' . $this->request->server('HTTP_HOST'));
88
	}
89
90
	/**
91
	 * Return if this module is enabled by the admin
92
	 * (And all server requirements are met).
93
	 *
94
	 * Do not return false in case a specific user disabeld this module,
95
	 * OR if the user is unable to use this specific module.
96
	 * @return boolean
97
	 */
98
	public function is_enabled()
99
	{
100
		return true;
101
	}
102
103
	/**
104
	 * Check if the current user is able to use this module.
105
	 *
106
	 * This means that the user enabled it in the UCP,
107
	 * And has it setup up correctly.
108
	 * This method will be called during login, not during registration/
109
	 *
110
	 * @param int $user_id
111
	 * @return bool
112
	 */
113
	public function is_usable($user_id)
114
	{
115
		if (!$this->is_potentially_usable($user_id))
116
		{
117
			return false;
118
		}
119
		$sql = 'SELECT COUNT(registration_id) as reg_id 
120
					FROM ' . $this->registration_table . ' 
121
					WHERE 
122
						user_id = ' . (int) $user_id;
123
		$result = $this->db->sql_query($sql);
124
		$row = $this->db->sql_fetchrow($result);
125
		$this->db->sql_freeresult($result);
126
127
		return $row && $row['reg_id'] > 0;
128
	}
129
130
	/**
131
	 * Check if the user can potentially use this.
132
	 * This method is called at registration page.
133
	 *
134
	 * You can, for example, check if the current browser is suitable.
135
	 *
136
	 * @param int|boolean $user_id Use false to ignore user
137
	 * @return bool
138
	 */
139
	public function is_potentially_usable($user_id = false)
140
	{
141
		$browsercap = new Browscap($this->root_path . 'cache/');
142
		$info = $browsercap->getBrowser($this->request->server('HTTP_USER_AGENT'));
143
		return strtolower($info->Browser) === 'chrome' && $this->is_ssl();
144
	}
145
146
	/**
147
	 * Check if the current session is secure.
148
	 *
149
	 * @return bool
150
	 */
151
	private function is_ssl()
152
	{
153
		$secure = $this->request->server('HTTPS');
154
		if (!empty($secure))
155
		{
156
			return 'on' == strtolower($secure) || '1' == $secure;
157
		}
158
		else if ('443' == $this->request->server('SERVER_PORT'))
159
		{
160
			return true;
161
		}
162
		return false;
163
	}
164
165
	/**
166
	 * Get the priority for this module.
167
	 * A lower priority means more chance it gets selected as default option
168
	 *
169
	 * There can be only one module with a specific priority!
170
	 * If there is already a module registered with this priority,
171
	 * a Exception might be thrown
172
	 *
173
	 * @return int
174
	 */
175
	public function get_priority()
176
	{
177
		return 10;
178
	}
179
180
	/**
181
	 * Start of the login procedure.
182
	 * @param int $user_id
183
	 * @return void
184
	 * @throws BadRequestHttpException
185
	 */
186
	public function login_start($user_id)
187
	{
188
		$registrations = json_encode($this->u2f->getAuthenticateData($this->getRegistrations($user_id)), JSON_UNESCAPED_SLASHES);
189
190
		$sql_ary = array(
191
			'u2f_request'	=> $registrations
192
		);
193
194
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
195
					WHERE
196
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
197
						session_user_id = ' . (int) $this->user->data['user_id'];
198
		$this->db->sql_query($sql);
199
		$count = $this->db->sql_affectedrows();
200
201
		if ($count != 1)
202
		{
203
			if ($count > 1)
204
			{
205
				// Reset sessions table. We had multiple sessions with same ID!!!
206
				$sql_ary['u2f_request'] = '';
207
				$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
208
					WHERE
209
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
210
						session_user_id = ' . (int) $this->user->data['user_id'];
211
				$this->db->sql_query($sql);
212
			}
213
			throw new BadRequestHttpException('TFA_UNABLE_TO_UPDATE_SESSION');
214
		}
215
216
		$this->template->assign_var('U2F_REQ', $registrations);
217
	}
218
219
	/**
220
	 * Actual login procedure
221
	 * @param int $user_id
222
	 * @throws AccessDeniedHttpException
223
	 */
224
	public function login($user_id)
225
	{
226
		try
227
		{
228
			$sql = 'SELECT u2f_request 
229
						FROM ' . SESSIONS_TABLE . ' 
230
						WHERE
231
							session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
232
							session_user_id = ' . (int) $this->user->data['user_id'];
233
			$result = $this->db->sql_query($sql);
234
			$row = $this->db->sql_fetchrow($result);
235
			$this->db->sql_freeresult($result);
236
237
			if (!$row || empty($row['u2f_request']))
238
			{
239
				throw new AccessDeniedHttpException($this->user->lang('TFA_NO_ACCESS'));
240
			}
241
242
			$response = json_decode(htmlspecialchars_decode($this->request->variable('authenticate', '')));
243
244
			if (property_exists($response, 'errorCode'))
245
			{
246
				if ($response->errorCode == 4) // errorCode 4 means that this device wasn't registered
247
				{
248
					throw new AccessDeniedHttpException($this->user->lang('TFA_NOT_REGISTERED'));
249
				}
250
				throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG'));
251
			}
252
			$result = new AuthenticationResponse($response->signatureData, $response->clientData, $response->keyHandle, $response->errorCode);
253
254
			/** @var \paul999\tfa\helper\registration_helper $reg */
255
			$reg = $this->u2f->doAuthenticate($this->convertRequests(json_decode($row['u2f_request'])), $this->getRegistrations($user_id), $result);
256
			$sql_ary = array(
257
				'counter' => $reg->getCounter(),
258
				'last_used' => time(),
259
			);
260
261
			$sql = 'UPDATE ' . $this->registration_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE registration_id = ' . (int) $reg->getId();
262
			$this->db->sql_query($sql);
263
		}
264
		catch (U2fError $error)
265
		{
266
			$this->createError($error);
267
		}
268
		catch (\InvalidArgumentException $invalid)
269
		{
270
			throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG') . '<br />' . $invalid->getMessage(), $invalid);
271
		}
272
	}
273
274
	/**
275
	 * @param array $requests
276
	 * @return array
277
	 */
278
	private function convertRequests($requests)
279
	{
280
		$result = array();
281
		foreach ($requests as $request)
282
		{
283
			$result[] = new SignRequest($request->challenge, $request->keyHandle, $request->appId);
284
		}
285
		return $result;
286
	}
287
288
	/**
289
	 * Start of registration
290
	 * @return string
291
	 */
292
	public function register_start()
293
	{
294
		$data = $this->u2f->getRegisterData($this->reg_data);
295
296
		$sql_ary = array(
297
			'u2f_request' => json_encode($data[0], JSON_UNESCAPED_SLASHES),
298
		);
299
300
		$count = $this->update_session($sql_ary);
301
302
		if ($count == 0)
303
		{
304
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
305
		}
306
		else if ($count > 1)
307
		{
308
			// Reset sessions table. We had multiple sessions with same ID!!!
309
			$sql_ary['u2f_request'] = '';
310
			$this->update_session($sql_ary);
311
312
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
313
		}
314
315
		$this->template->assign_vars(array(
316
			'U2F_REG'           => true,
317
			'U2F_SIGN_REQUEST'  => json_encode($data[0], JSON_UNESCAPED_SLASHES),
318
			'U2F_SIGN'          => json_encode($data[1], JSON_UNESCAPED_SLASHES),
319
		));
320
321
		return 'tfa_u2f_ucp_new';
322
	}
323
324
	/**
325
	 * Actual registration
326
	 * @return void
327
	 * @throws BadRequestHttpException
328
	 */
329
	public function register()
330
	{
331
		try
332
		{
333
			$reg = $this->u2f->doRegister(json_decode($this->user->data['u2f_request']), json_decode(htmlspecialchars_decode($this->request->variable('register', ''))));
334
335
			$sql_ary = array(
336
				'user_id' => $this->user->data['user_id'],
337
				'key_handle' => $reg->getKeyHandle(),
338
				'public_key' => $reg->getPublicKey(),
339
				'certificate' => $reg->getCertificate(),
340
				'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
341
				'registered' => time(),
342
				'last_used' => time(),
343
			);
344
345
			$sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
346
			$this->db->sql_query($sql);
347
348
			$sql_ary = array(
349
				'u2f_request' => '',
350
			);
351
352
			$this->update_session($sql_ary);
353
		}
354
		catch (U2fError $err)
355
		{
356
			$this->createError($err);
357
		}
358
	}
359
360
	/**
361
	 * This method is called to show the UCP page.
362
	 * You can assign template variables to the template, or do anything else here.
363
	 */
364
	public function show_ucp()
365
	{
366
		$sql = 'SELECT *
367
			FROM ' . $this->registration_table . '
368
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
369
			ORDER BY registration_id ASC';
370
371
		$result = $this->db->sql_query($sql);
372
		//$this->reg_data = array();
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
383
			$reg = new registration_helper();
384
			$reg->setCounter($row['counter']);
385
			$reg->setCertificate($row['certificate']);
386
			$reg->setKeyHandle($row['key_handle']);
387
			$reg->setPublicKey($row['public_key']);
388
			$reg->setId($row['registration_id']);
389
390
			$this->reg_data[] = $reg; */
391
		}
392
		$this->db->sql_freeresult($result);
393
	}
394
395
	/**
396
	 * Delete a specific row from the UCP.
397
	 * The data is based on the data provided in show_ucp.
398
	 * @param int $key
399
	 * @return void
400
	 */
401
	public function delete($key)
402
	{
403
			$sql = 'DELETE FROM ' . $this->registration_table . '
404
						WHERE user_id = ' . (int) $this->user->data['user_id'] . '
405
						AND registration_id =' . (int) $key;
406
407
			$this->db->sql_query($sql);
408
409
	}
410
411
	/**
412
	 * If this module can add new keys (Or other things)
413
	 *
414
	 * @return boolean
415
	 */
416
	public function can_register()
417
	{
418
		return $this->is_potentially_usable(false);
419
	}
420
421
	/**
422
	 * Return the name of the current module
423
	 * This is for internal use only
424
	 * @return string
425
	 */
426
	public function get_name()
427
	{
428
		return 'u2f';
429
	}
430
431
	/**
432
	 * Get a language key for this specific module.
433
	 * @return string
434
	 */
435
	public function get_translatable_name()
436
	{
437
		return 'MODULE_U2F';
438
	}
439
440
	/**
441
	 * Select all registration objects from the database
442
	 * @param integer $user_id
443
	 * @return array
444
	 */
445
	private function getRegistrations($user_id)
446
	{
447
		$sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
448
		$result = $this->db->sql_query($sql);
449
		$rows = array();
450
451
		while ($row = $this->db->sql_fetchrow($result))
452
		{
453
			$reg = new registration_helper();
454
			$reg->setCounter($row['counter']);
455
			$reg->setCertificate($row['certificate']);
456
			$reg->setKeyHandle($row['key_handle']);
457
			$reg->setPublicKey($row['public_key']);
458
			$reg->setId($row['registration_id']);
459
460
			$rows[] = $reg;
461
		}
462
463
		$this->db->sql_freeresult($result);
464
		return $rows;
465
	}
466
467
	/**
468
	 * @param U2fError $error
469
	 * @throws BadRequestHttpException
470
	 */
471
	private function createError(U2fError $error)
472
	{
473
		switch ($error->getCode())
474
		{
475
			/** Error for the authentication message not matching any outstanding
476
			 * authentication request */
477
			case U2fError::ERR_NO_MATCHING_REQUEST:
478
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REQUEST'), $error);
479
480
			/** Error for the authentication message not matching any registration */
481
			case U2fError::ERR_NO_MATCHING_REGISTRATION:
482
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REGISTRATION'), $error);
483
484
			/** Error for the signature on the authentication message not verifying with
485
			 * the correct key */
486
			case U2fError::ERR_AUTHENTICATION_FAILURE:
487
				throw new BadRequestHttpException($this->user->lang('ERR_AUTHENTICATION_FAILURE'), $error);
488
489
			/** Error for the challenge in the registration message not matching the
490
			 * registration challenge */
491
			case U2fError::ERR_UNMATCHED_CHALLENGE:
492
				throw new BadRequestHttpException($this->user->lang('ERR_UNMATCHED_CHALLENGE'), $error);
493
494
			/** Error for the attestation signature on the registration message not
495
			 * verifying */
496
			case U2fError::ERR_ATTESTATION_SIGNATURE:
497
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_SIGNATURE'), $error);
498
499
			/** Error for the attestation verification not verifying */
500
			case U2fError::ERR_ATTESTATION_VERIFICATION:
501
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_VERIFICATION'), $error);
502
503
			/** Error for not getting good random from the system */
504
			case U2fError::ERR_BAD_RANDOM:
505
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_RANDOM'), $error);
506
507
			/** Error when the counter is lower than expected */
508
			case U2fError::ERR_COUNTER_TOO_LOW:
509
				throw new BadRequestHttpException($this->user->lang('ERR_COUNTER_TOO_LOW'), $error);
510
511
			/** Error decoding public key */
512
			case U2fError::ERR_PUBKEY_DECODE:
513
				throw new BadRequestHttpException($this->user->lang('ERR_PUBKEY_DECODE'), $error);
514
515
			/** Error user-agent returned error */
516
			case U2fError::ERR_BAD_UA_RETURNING:
517
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_UA_RETURNING'), $error);
518
519
			/** Error old OpenSSL version */
520
			case U2fError::ERR_OLD_OPENSSL:
521
				throw new BadRequestHttpException(sprintf($this->user->lang('ERR_OLD_OPENSSL'), OPENSSL_VERSION_TEXT), $error);
522
523
			default:
524
				throw new BadRequestHttpException($this->user->lang('TFA_UNKNOWN_ERROR'), $error);
525
		}
526
	}
527
528
	/**
529
	 * Update the session with new TFA data
530
	 * @param $sql_ary
531
	 * @return int
532
	 */
533
	private function update_session($sql_ary)
534
	{
535
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
536
							WHERE
537
								session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
538
								session_user_id = ' . (int) $this->user->data['user_id'];
539
		$this->db->sql_query($sql);
540
541
		return $this->db->sql_affectedrows();
542
	}
543
}
544