Completed
Push — master ( 93f351...ab0a6e )
by Paul
03:41
created

u2f::get_translatable_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 \paul999\u2f\U2F
55
	 */
56
	private $u2f;
57
58
	/**
59
	 * @var array
60
	 */
61
	private $reg_data;
62
63
	/**
64
	 * u2f constructor.
65
	 * @param driver_interface $db
66
	 * @param user $user
67
	 * @param request_interface $request
68
	 * @param template $template
69
	 * @param string $registration_table
70
	 */
71
	public function __construct(driver_interface $db, user $user, request_interface $request, template $template, $registration_table)
72
	{
73
		$this->db       = $db;
74
		$this->user     = $user;
75
		$this->request  = $request;
76
		$this->template = $template;
77
78
		$this->registration_table	= $registration_table;
79
80
		$this->u2f = new \paul999\u2f\U2F('https://' . $this->request->server('HTTP_HOST'));
81
	}
82
83
	/**
84
	 * Return if this module is enabled by the admin
85
	 * (And all server requirements are met).
86
	 *
87
	 * Do not return false in case a specific user disabeld this module,
88
	 * OR if the user is unable to use this specific module.
89
	 * @return boolean
90
	 */
91
	public function is_enabled()
92
	{
93
		return true;
94
	}
95
96
	/**
97
	 * Check if the current user is able to use this module.
98
	 *
99
	 * This means that the user enabled it in the UCP,
100
	 * And has it setup up correctly.
101
	 * This method will be called during login, not during registration/
102
	 *
103
	 * @param int $user_id
104
	 * @return bool
105
	 */
106
	public function is_usable($user_id)
107
	{
108
		if (!$this->is_potentially_usable($user_id))
109
		{
110
			return false;
111
		}
112
		$sql = 'SELECT COUNT(registration_id) as reg_id 
113
					FROM ' . $this->registration_table . ' 
114
					WHERE 
115
						user_id = ' . (int) $user_id;
116
		$result = $this->db->sql_query($sql);
117
		$row = $this->db->sql_fetchrow($result);
118
		$this->db->sql_freeresult($result);
119
120
		return $row && $row['reg_id'] > 0;
121
	}
122
123
	/**
124
	 * Check if the user can potentially use this.
125
	 * This method is called at registration page.
126
	 *
127
	 * You can, for example, check if the current browser is suitable.
128
	 *
129
	 * @param int $user_id
130
	 * @return bool
131
	 */
132
	public function is_potentially_usable($user_id)
133
	{
134
		$browsercap = new Browscap();
135
		$info = $browsercap->getBrowser();
136
		return $info['Browser'] === 'chrome' && $this->is_ssl();
137
	}
138
139
	/**
140
	 * Check if the current session is secure.
141
	 *
142
	 * @return bool
143
	 */
144
	private function is_ssl()
145
	{
146
		$secure = $this->request->server('HTTPS');
147
		if (!empty($secure))
148
		{
149
			return 'on' == strtolower($secure) || '1' == $secure;
150
		}
151
		else if ('443' == $this->request->server('SERVER_PORT'))
152
		{
153
			return true;
154
		}
155
		return false;
156
	}
157
158
	/**
159
	 * Get the priority for this module.
160
	 * A lower priority means more chance it gets selected as default option
161
	 *
162
	 * There can be only one module with a specific priority!
163
	 * If there is already a module registered with this priority,
164
	 * a Exception might be thrown
165
	 *
166
	 * @return int
167
	 */
168
	public function get_priority()
169
	{
170
		return 10;
171
	}
172
173
	/**
174
	 * Start of the login procedure.
175
	 * @param int $user_id
176
	 * @return void
177
	 * @throws BadRequestHttpException
178
	 */
179
	public function login_start($user_id)
180
	{
181
		$registrations = json_encode($this->u2f->getAuthenticateData($this->getRegistrations($user_id)), JSON_UNESCAPED_SLASHES);
182
183
		$sql_ary = array(
184
			'u2f_request'	=> $registrations
185
		);
186
187
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
188
					WHERE
189
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
190
						session_user_id = ' . (int) $this->user->data['user_id'];
191
		$this->db->sql_query($sql);
192
		$count = $this->db->sql_affectedrows();
193
194
		if ($count != 1)
195
		{
196
			if ($count > 1)
197
			{
198
				// Reset sessions table. We had multiple sessions with same ID!!!
199
				$sql_ary['u2f_request'] = '';
200
				$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
201
					WHERE
202
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
203
						session_user_id = ' . (int) $this->user->data['user_id'];
204
				$this->db->sql_query($sql);
205
			}
206
			throw new BadRequestHttpException('TFA_UNABLE_TO_UPDATE_SESSION');
207
		}
208
209
		$this->template->assign_var('U2F_REQ', $registrations);
210
	}
211
212
	/**
213
	 * Actual login procedure
214
	 * @param int $user_id
215
	 * @throws AccessDeniedHttpException
216
	 */
217
	public function login($user_id)
218
	{
219
		try
220
		{
221
			$sql = 'SELECT u2f_request 
222
						FROM ' . SESSIONS_TABLE . ' 
223
						WHERE
224
							session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
225
							session_user_id = ' . (int) $this->user->data['user_id'];
226
			$result = $this->db->sql_query($sql);
227
			$row = $this->db->sql_fetchrow($result);
228
			$this->db->sql_freeresult($result);
229
230
			if (!$row || empty($row['u2f_request']))
231
			{
232
				throw new AccessDeniedHttpException($this->user->lang('TFA_NO_ACCESS'));
233
			}
234
235
			$response = json_decode(htmlspecialchars_decode($this->request->variable('authenticate', '')));
236
237
			if (property_exists($response, 'errorCode'))
238
			{
239
				if ($response->errorCode == 4) // errorCode 4 means that this device wasn't registered
240
				{
241
					throw new AccessDeniedHttpException($this->user->lang('TFA_NOT_REGISTERED'));
242
				}
243
				throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG'));
244
			}
245
			$result = new AuthenticationResponse($response->signatureData, $response->clientData, $response->keyHandle, $response->errorCode);
246
247
			/** @var \paul999\tfa\helper\registration_helper $reg */
248
			$reg = $this->u2f->doAuthenticate($this->convertRequests(json_decode($row['u2f_request'])), $this->getRegistrations($user_id), $result);
249
			$sql_ary = array(
250
				'counter' => $reg->getCounter(),
251
				'last_used' => time(),
252
			);
253
254
			$sql = 'UPDATE ' . $this->registration_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE registration_id = ' . (int) $reg->getId();
255
			$this->db->sql_query($sql);
256
		}
257
		catch (U2fError $error)
258
		{
259
			$this->createError($error);
260
		}
261
		catch (\InvalidArgumentException $invalid)
262
		{
263
			throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG') . '<br />' . $invalid->getMessage(), $invalid);
264
		}
265
	}
266
267
	/**
268
	 * @param array $requests
269
	 * @return array
270
	 */
271
	private function convertRequests($requests)
272
	{
273
		$result = array();
274
		foreach ($requests as $request)
275
		{
276
			$result[] = new SignRequest($request->challenge, $request->keyHandle, $request->appId);
277
		}
278
		return $result;
279
	}
280
281
	/**
282
	 * Start of registration
283
	 * @return string
284
     */
285
	public function register_start()
286
	{
287
		$data = $this->u2f->getRegisterData($this->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
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
298
		}
299
		else if ($count > 1)
300
		{
301
			// Reset sessions table. We had multiple sessions with same ID!!!
302
			$sql_ary['u2f_request'] = '';
303
			$this->update_session($sql_ary);
304
305
			trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
306
		}
307
308
		$this->template->assign_vars(array(
309
			'U2F_REG'           => true,
310
			'U2F_SIGN_REQUEST'  => json_encode($data[0], JSON_UNESCAPED_SLASHES),
311
			'U2F_SIGN'          => json_encode($data[1], JSON_UNESCAPED_SLASHES),
312
		));
313
314
        return 'tfa_u2f_ucp_new';
315
	}
316
317
	/**
318
	 * Actual registration
319
	 * @return void
320
     * @throws BadRequestHttpException
321
	 */
322
	public function register()
323
	{
324
		try
325
		{
326
			$reg = $this->u2f->doRegister(json_decode($this->user->data['u2f_request']), json_decode(htmlspecialchars_decode($this->request->variable('register', ''))));
327
328
			$sql_ary = array(
329
				'user_id' => $this->user->data['user_id'],
330
				'key_handle' => $reg->getKeyHandle(),
331
				'public_key' => $reg->getPublicKey(),
332
				'certificate' => $reg->getCertificate(),
333
				'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
334
				'registered' => time(),
335
				'last_used' => time(),
336
			);
337
338
			$sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
339
			$this->db->sql_query($sql);
340
341
			$sql_ary = array(
342
				'u2f_request' => '',
343
			);
344
345
			$this->update_session($sql_ary);
346
		}
347
		catch (U2fError $err)
348
		{
349
			$this->createError($err);
350
		}
351
	}
352
353
	/**
354
	 * This method is called to show the UCP page.
355
	 * You can assign template variables to the template, or do anything else here.
356
	 */
357
	public function show_ucp()
358
	{
359
		$sql = 'SELECT *
360
			FROM ' . $this->registration_table . '
361
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
362
			ORDER BY registration_id ASC';
363
364
		$result = $this->db->sql_query($sql);
365
		//$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...
366
367
		while ($row = $this->db->sql_fetchrow($result))
368
		{
369
			$this->template->assign_block_vars('keys', array(
370
			    'CLASS'         => 'u2f',
371
				'ID'            => $row['registration_id'],
372
				'REGISTERED'    => $this->user->format_date($row['registered']),
373
				'LAST_USED'     => $this->user->format_date($row['last_used']),
374
			));
375
/*
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...
376
			$reg = new registration_helper();
377
			$reg->setCounter($row['counter']);
378
			$reg->setCertificate($row['certificate']);
379
			$reg->setKeyHandle($row['key_handle']);
380
			$reg->setPublicKey($row['public_key']);
381
			$reg->setId($row['registration_id']);
382
383
			$this->reg_data[] = $reg; */
384
		}
385
		$this->db->sql_freeresult($result);
386
	}
387
388
	/**
389
	 * Delete a specific row from the UCP.
390
	 * The data is based on the data provided in show_ucp.
391
	 * @param array $data
392
	 * @return mixed
393
	 */
394
	public function delete($data)
395
	{
396
		if (isset($data['keys']))
397
		{
398
			$sql_where = $this->db->sql_in_set('registration_id', $data['keys']);
399
			$sql = 'DELETE FROM ' . $this->registration_table . '
400
												WHERE user_id = ' . (int) $this->user->data['user_id'] . '
401
												AND ' . $sql_where;
402
403
			$this->db->sql_query($sql);
404
		}
405
	}
406
407
    /**
408
     * If this module can add new keys (Or other things)
409
     *
410
     * @return boolean
411
     */
412
    public function can_register()
413
    {
414
        return $this->is_potentially_usable(false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
415
    }
416
417
    /**
418
     * Get a language key for this specific module.
419
     * @return string
420
     */
421
    public function get_translatable_name()
422
    {
423
        return 'MODULE_U2F';
424
    }
425
426
	/**
427
	 * Select all registration objects from the database
428
	 * @param integer $user_id
429
	 * @return array
430
	 */
431
	private function getRegistrations($user_id)
432
	{
433
		$sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
434
		$result = $this->db->sql_query($sql);
435
		$rows = array();
436
437
		while ($row = $this->db->sql_fetchrow($result))
438
		{
439
			$reg = new registration_helper();
440
			$reg->setCounter($row['counter']);
441
			$reg->setCertificate($row['certificate']);
442
			$reg->setKeyHandle($row['key_handle']);
443
			$reg->setPublicKey($row['public_key']);
444
			$reg->setId($row['registration_id']);
445
446
			$rows[] = $reg;
447
		}
448
449
		$this->db->sql_freeresult($result);
450
		return $rows;
451
	}
452
453
	/**
454
	 * @param U2fError $error
455
	 * @throws BadRequestHttpException
456
	 */
457
	private function createError(U2fError $error)
458
	{
459
		switch ($error->getCode())
460
		{
461
			/** Error for the authentication message not matching any outstanding
462
			 * authentication request */
463
			case U2fError::ERR_NO_MATCHING_REQUEST:
464
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REQUEST'), $error);
465
466
			/** Error for the authentication message not matching any registration */
467
			case U2fError::ERR_NO_MATCHING_REGISTRATION:
468
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REGISTRATION'), $error);
469
470
			/** Error for the signature on the authentication message not verifying with
471
			 * the correct key */
472
			case U2fError::ERR_AUTHENTICATION_FAILURE:
473
				throw new BadRequestHttpException($this->user->lang('ERR_AUTHENTICATION_FAILURE'), $error);
474
475
			/** Error for the challenge in the registration message not matching the
476
			 * registration challenge */
477
			case U2fError::ERR_UNMATCHED_CHALLENGE:
478
				throw new BadRequestHttpException($this->user->lang('ERR_UNMATCHED_CHALLENGE'), $error);
479
480
			/** Error for the attestation signature on the registration message not
481
			 * verifying */
482
			case U2fError::ERR_ATTESTATION_SIGNATURE:
483
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_SIGNATURE'), $error);
484
485
			/** Error for the attestation verification not verifying */
486
			case U2fError::ERR_ATTESTATION_VERIFICATION:
487
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_VERIFICATION'), $error);
488
489
			/** Error for not getting good random from the system */
490
			case U2fError::ERR_BAD_RANDOM:
491
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_RANDOM'), $error);
492
493
			/** Error when the counter is lower than expected */
494
			case U2fError::ERR_COUNTER_TOO_LOW:
495
				throw new BadRequestHttpException($this->user->lang('ERR_COUNTER_TOO_LOW'), $error);
496
497
			/** Error decoding public key */
498
			case U2fError::ERR_PUBKEY_DECODE:
499
				throw new BadRequestHttpException($this->user->lang('ERR_PUBKEY_DECODE'), $error);
500
501
			/** Error user-agent returned error */
502
			case U2fError::ERR_BAD_UA_RETURNING:
503
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_UA_RETURNING'), $error);
504
505
			/** Error old OpenSSL version */
506
			case U2fError::ERR_OLD_OPENSSL:
507
				throw new BadRequestHttpException(sprintf($this->user->lang('ERR_OLD_OPENSSL'), OPENSSL_VERSION_TEXT), $error);
508
509
			default:
510
				throw new BadRequestHttpException($this->user->lang('TFA_UNKNOWN_ERROR'), $error);
511
		}
512
	}
513
514
	/**
515
	 * Update the session with new TFA data
516
	 * @param $sql_ary
517
	 * @return int
518
	 */
519
	private function update_session($sql_ary)
520
	{
521
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
522
							WHERE
523
								session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
524
								session_user_id = ' . (int) $this->user->data['user_id'];
525
		$this->db->sql_query($sql);
526
527
		return $this->db->sql_affectedrows();
528
	}
529
}
530