Completed
Push — master ( dda927...c82647 )
by Paul
10:33
created

u2f::is_ssl()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 13
rs 9.2
cc 4
eloc 7
nc 4
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
		// TODO: Implement is_enabled() method.
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
		elseif ('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 void
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
315
	/**
316
	 * Actual registration
317
	 * @return void
318
	 */
319
	public function register()
320
	{
321
		try
322
		{
323
			$reg = $this->u2f->doRegister(json_decode($this->user->data['u2f_request']), json_decode(htmlspecialchars_decode($this->request->variable('register', ''))));
324
325
			$sql_ary = array(
326
				'user_id' => $this->user->data['user_id'],
327
				'key_handle' => $reg->getKeyHandle(),
328
				'public_key' => $reg->getPublicKey(),
329
				'certificate' => $reg->getCertificate(),
330
				'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
331
				'registered' => time(),
332
				'last_used' => time(),
333
			);
334
335
			$sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
336
			$this->db->sql_query($sql);
337
338
			$sql_ary = array(
339
				'u2f_request' => '',
340
			);
341
342
			$this->update_session($sql_ary);
343
		}
344
		catch (U2fError $err)
345
		{
346
			$this->createError($err);
347
		}
348
	}
349
350
	/**
351
	 * This method is called to show the UCP page.
352
	 * You can assign template variables to the template, or do anything else here.
353
	 */
354
	public function show_ucp()
355
	{
356
		$sql = 'SELECT *
357
			FROM ' . $this->registration_table . '
358
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
359
			ORDER BY registration_id ASC';
360
361
		$result = $this->db->sql_query($sql);
362
		$this->reg_data = array();
363
364
		while ($row = $this->db->sql_fetchrow($result))
365
		{
366
			$this->template->assign_block_vars('keys', array(
367
				'ID'            => $row['registration_id'],
368
				'REGISTERED'    => $this->user->format_date($row['registered']),
369
				'LAST_USED'     => $this->user->format_date($row['last_used']),
370
			));
371
372
			$reg = new registration_helper();
373
			$reg->setCounter($row['counter']);
374
			$reg->setCertificate($row['certificate']);
375
			$reg->setKeyHandle($row['key_handle']);
376
			$reg->setPublicKey($row['public_key']);
377
			$reg->setId($row['registration_id']);
378
379
			$this->reg_data[] = $reg;
380
		}
381
		$this->db->sql_freeresult($result);
382
	}
383
384
	/**
385
	 * Delete a specific row from the UCP.
386
	 * The data is based on the data provided in show_ucp.
387
	 * @param array $data
388
	 * @return mixed
389
	 */
390
	public function delete($data)
391
	{
392
		if (isset($data['keys']))
393
		{
394
			$sql_where = $this->db->sql_in_set('registration_id', $data['keys']);
395
			$sql = 'DELETE FROM ' . $this->registration_table . '
396
												WHERE user_id = ' . (int) $this->user->data['user_id'] . '
397
												AND ' . $sql_where;
398
399
			$this->db->sql_query($sql);
400
		}
401
	}
402
403
	/**
404
	 * Select all registration objects from the database
405
	 * @param integer $user_id
406
	 * @return array
407
	 */
408
	private function getRegistrations($user_id)
409
	{
410
		$sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
411
		$result = $this->db->sql_query($sql);
412
		$rows = array();
413
414
		while ($row = $this->db->sql_fetchrow($result))
415
		{
416
			$reg = new registration_helper();
417
			$reg->setCounter($row['counter']);
418
			$reg->setCertificate($row['certificate']);
419
			$reg->setKeyHandle($row['key_handle']);
420
			$reg->setPublicKey($row['public_key']);
421
			$reg->setId($row['registration_id']);
422
423
			$rows[] = $reg;
424
		}
425
426
		$this->db->sql_freeresult($result);
427
		return $rows;
428
	}
429
430
	/**
431
	 * @param U2fError $error
432
	 * @throws BadRequestHttpException
433
	 */
434
	private function createError(U2fError $error)
435
	{
436
		switch ($error->getCode())
437
		{
438
			/** Error for the authentication message not matching any outstanding
439
			 * authentication request */
440
			case U2fError::ERR_NO_MATCHING_REQUEST:
441
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REQUEST'), $error);
442
443
			/** Error for the authentication message not matching any registration */
444
			case U2fError::ERR_NO_MATCHING_REGISTRATION:
445
				throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REGISTRATION'), $error);
446
447
			/** Error for the signature on the authentication message not verifying with
448
			 * the correct key */
449
			case U2fError::ERR_AUTHENTICATION_FAILURE:
450
				throw new BadRequestHttpException($this->user->lang('ERR_AUTHENTICATION_FAILURE'), $error);
451
452
			/** Error for the challenge in the registration message not matching the
453
			 * registration challenge */
454
			case U2fError::ERR_UNMATCHED_CHALLENGE:
455
				throw new BadRequestHttpException($this->user->lang('ERR_UNMATCHED_CHALLENGE'), $error);
456
457
			/** Error for the attestation signature on the registration message not
458
			 * verifying */
459
			case U2fError::ERR_ATTESTATION_SIGNATURE:
460
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_SIGNATURE'), $error);
461
462
			/** Error for the attestation verification not verifying */
463
			case U2fError::ERR_ATTESTATION_VERIFICATION:
464
				throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_VERIFICATION'), $error);
465
466
			/** Error for not getting good random from the system */
467
			case U2fError::ERR_BAD_RANDOM:
468
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_RANDOM'), $error);
469
470
			/** Error when the counter is lower than expected */
471
			case U2fError::ERR_COUNTER_TOO_LOW:
472
				throw new BadRequestHttpException($this->user->lang('ERR_COUNTER_TOO_LOW'), $error);
473
474
			/** Error decoding public key */
475
			case U2fError::ERR_PUBKEY_DECODE:
476
				throw new BadRequestHttpException($this->user->lang('ERR_PUBKEY_DECODE'), $error);
477
478
			/** Error user-agent returned error */
479
			case U2fError::ERR_BAD_UA_RETURNING:
480
				throw new BadRequestHttpException($this->user->lang('ERR_BAD_UA_RETURNING'), $error);
481
482
			/** Error old OpenSSL version */
483
			case U2fError::ERR_OLD_OPENSSL:
484
				throw new BadRequestHttpException(sprintf($this->user->lang('ERR_OLD_OPENSSL'), OPENSSL_VERSION_TEXT), $error);
485
486
			default:
487
				throw new BadRequestHttpException($this->user->lang('TFA_UNKNOWN_ERROR'), $error);
488
		}
489
	}
490
491
	/**
492
	 * Update the session with new TFA data
493
	 * @param $sql_ary
494
	 * @return int
495
	 */
496
	private function update_session($sql_ary)
497
	{
498
		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
499
							WHERE
500
								session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
501
								session_user_id = ' . (int) $this->user->data['user_id'];
502
		$this->db->sql_query($sql);
503
504
		return $this->db->sql_affectedrows();
505
	}
506
}
507