Completed
Push — master ( b5a957...040c1d )
by Paul
03:38
created

u2f::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.4286
cc 1
eloc 7
nc 1
nop 5
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
14
use paul999\tfa\helper\registration_helper;
15
use paul999\u2f\AuthenticationResponse;
16
use paul999\u2f\Exceptions\U2fError;
17
use paul999\u2f\SignRequest;
18
use phpbb\db\driver\driver_interface;
19
use phpbb\request\request_interface;
20
use phpbb\template\template;
21
use phpbb\user;
22
use phpbrowscap\Browscap;
23
use ReflectionObject;
24
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
25
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
26
27
class u2f implements module_interface
28
{
29
30
    /**
31
     * @var driver_interface
32
     */
33
    private $db;
34
35
    /**
36
     * @var user
37
     */
38
    private $user;
39
40
    /**
41
     * @var request_interface
42
     */
43
    private $request;
44
45
    /**
46
     * @var template
47
     */
48
    private $template;
49
50
    /**
51
     * @var string
52
     */
53
    private $registration_table;
54
55
    /**
56
     * @var \paul999\u2f\U2F
57
     */
58
    private $u2f;
59
60
    /**
61
     * @var array
62
     */
63
    private $reg_data;
64
65
    /**
66
     * u2f constructor.
67
     * @param driver_interface $db
68
     * @param user $user
69
     * @param request_interface $request
70
     * @param template $template
71
     * @param string $registration_table
72
     */
73
    public function __construct(driver_interface $db, user $user, request_interface $request, template $template, $registration_table)
74
    {
75
        $this->db       = $db;
76
        $this->user     = $user;
77
        $this->request  = $request;
78
        $this->template = $template;
79
80
        $this->registration_table	= $registration_table;
81
82
        $this->u2f = new \paul999\u2f\U2F('https://' . $this->request->server('HTTP_HOST'));
83
    }
84
85
    /**
86
     * Return if this module is enabled by the admin
87
     * (And all server requirements are met).
88
     *
89
     * Do not return false in case a specific user disabeld this module,
90
     * OR if the user is unable to use this specific module.
91
     * @return boolean
92
     */
93
    public function is_enabled()
94
    {
95
        // TODO: Implement is_enabled() method.
96
    }
97
98
    /**
99
     * Check if the current user is able to use this module.
100
     *
101
     * This means that the user enabled it in the UCP,
102
     * And has it setup up correctly.
103
     * This method will be called during login, not during registration/
104
     *
105
     * @param int $user_id
106
     * @return bool
107
     */
108
    public function is_usable($user_id)
109
    {
110
        $browscap = new Browscap();
111
        $info = $browscap->getBrowser();
112
        if ($info['Browser'] !== 'chrome')
113
        {
114
            return false; // u2f is currently only supported in chrome!
115
        }
116
        $sql = 'SELECT COUNT(registration_id) as reg_id FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
117
        $result = $this->db->sql_query($sql);
118
        $row = $this->db->sql_fetchrow($result);
119
        $this->db->sql_freeresult($result);
120
121
        return $row && $row['reg_id'] > 0;
122
    }
123
124
    /**
125
     * Check if the user can potentially use this.
126
     * This method is called at registration page.
127
     *
128
     * You can, for example, check if the current browser is suitable.
129
     *
130
     * @param int $user_id
131
     * @return bool
132
     */
133
    public function is_potentially_usable($user_id)
134
    {
135
        $browsercap = new Browscap();
136
        $info = $browsercap->getBrowser();
137
        return $info['Browser'] === 'chrome';
138
    }
139
140
    /**
141
     * Get the priority for this module.
142
     * A lower priority means more chance it gets selected as default option
143
     *
144
     * There can be only one module with a specific priority!
145
     * If there is already a module registered with this priority,
146
     * a Exception might be thrown
147
     *
148
     * @param int $user_id If set, the priority can depend on the current user
149
     * @return int
150
     */
151
    public function get_priority($user_id = 0)
152
    {
153
        return 10;
154
    }
155
156
    /**
157
     * Start of the login procedure.
158
     * @param int $user_id
159
     * @return void
160
     * @throws BadRequestHttpException
161
     */
162
    public function login_start($user_id)
163
    {
164
        $registrations = json_encode($this->u2f->getAuthenticateData($this->getRegistrations($user_id)), JSON_UNESCAPED_SLASHES);
165
166
        $sql_ary = array(
167
            'u2f_request'	=> $registrations
168
        );
169
170
        $sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
171
					WHERE
172
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
173
						session_user_id = ' . (int) $this->user->data['user_id'];
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
174
        $this->db->sql_query($sql);
175
        $count = $this->db->sql_affectedrows();
176
177
        if ($count != 1)
178
        {
179
            if ($count > 1)
180
            {
181
                // Reset sessions table. We had multiple sessions with same ID!!!
182
                $sql_ary['u2f_request'] = '';
183
                $sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
184
					WHERE
185
						session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
186
						session_user_id = ' . (int) $this->user->data['user_id'];
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
187
                $this->db->sql_query($sql);
188
            }
189
            throw new BadRequestHttpException('TFA_UNABLE_TO_UPDATE_SESSION');
190
        }
191
192
        $this->template->assign_var('U2F_REQ', $registrations);
193
    }
194
195
    /**
196
     * Actual login procedure
197
     * @param int $user_id
198
     * @throws AccessDeniedHttpException
199
     */
200
    public function login($user_id)
201
    {
202
        try {
203
            $sql = 'SELECT u2f_request FROM ' . SESSIONS_TABLE . ' WHERE
204
			    session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
205
			    session_user_id = ' . (int)$this->user->data['user_id'];
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
206
            $result = $this->db->sql_query($sql);
207
            $row = $this->db->sql_fetchrow($result);
208
            $this->db->sql_freeresult($result);
209
210
            if (!$row || empty($row['u2f_request'])) {
211
                throw new AccessDeniedHttpException($this->user->lang('TFA_NO_ACCESS'));
212
            }
213
214
            $response = json_decode(htmlspecialchars_decode($this->request->variable('authenticate', '')));
215
216
            if (property_exists($response, 'errorCode')) {
217
                if ($response->errorCode == 4) // errorCode 4 means that this device wasn't registered
218
                {
219
                    throw new AccessDeniedHttpException($this->user->lang('TFA_NOT_REGISTERED'));
220
                }
221
                throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG'));
222
            }
223
            $result = new AuthenticationResponse($response->signatureData, $response->clientData, $response->keyHandle, $response->errorCode);
224
225
            /** @var \paul999\tfa\helper\registration_helper $reg */
226
            $reg = $this->u2f->doAuthenticate($this->convertRequests(json_decode($row['u2f_request'])), $this->getRegistrations($user_id), $result);
227
            $sql_ary = array(
228
                'counter' => $reg->getCounter(),
229
                'last_used' => time(),
230
            );
231
232
            $sql = 'UPDATE ' . $this->registration_table . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . ' WHERE registration_id = ' . (int)$reg->id;
233
            $this->db->sql_query($sql);
234
        }
235
        catch (U2fError $error)
236
		{
237
            $this->createError($error);
238
        }
239
		catch (\InvalidArgumentException $invalid)
240
		{
241
            throw new BadRequestHttpException($this->user->lang('TFA_SOMETHING_WENT_WRONG') . '<br />' . $invalid->getMessage(), $invalid);
242
        }
243
    }
244
245
    /**
246
     * @param array $requests
247
     * @return array
248
     */
249
    private function convertRequests($requests)
250
    {
251
        $result = array();
252
        foreach($requests as $request)
253
        {
254
            $result[] = new SignRequest($request->challenge, $request->keyHandle, $request->appId);
255
        }
256
        return $result;
257
    }
258
259
    /**
260
     * Start of registration
261
     * @return void
262
     */
263
    public function register_start()
264
    {
265
        $data = $this->u2f->getRegisterData($this->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 == 0)
274
        {
275
            trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
276
        }
277
        else if ($count > 1)
278
        {
279
            // Reset sessions table. We had multiple sessions with same ID!!!
280
            $sql_ary['u2f_request'] = '';
281
            $this->update_session($sql_ary);
282
283
            trigger_error('TFA_UNABLE_TO_UPDATE_SESSION');
284
        }
285
286
        $this->template->assign_vars(array(
287
            'U2F_REG'           => true,
288
            'U2F_SIGN_REQUEST'  => json_encode($data[0], JSON_UNESCAPED_SLASHES),
289
            'U2F_SIGN'          => json_encode($data[1], JSON_UNESCAPED_SLASHES),
290
        ));
291
    }
292
293
    /**
294
     * Actual registration
295
     * @return void
296
     */
297
    public function register()
298
    {
299
        try
300
        {
301
            $reg = $this->u2f->doRegister(json_decode($this->user->data['u2f_request']), json_decode(htmlspecialchars_decode($this->request->variable('register', ''))));
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
302
303
            $sql_ary = array(
304
                'user_id' => $this->user->data['user_id'],
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
305
                'key_handle' => $reg->getKeyHandle(),
306
                'public_key' => $reg->getPublicKey(),
307
                'certificate' => $reg->getCertificate(),
308
                'counter' => ($reg->getCounter() > 0) ? $reg->getCounter() : 0,
309
                'registered' => time(),
310
                'last_used' => time(),
311
            );
312
313
            $sql = 'INSERT INTO ' . $this->registration_table . ' ' . $this->db->sql_build_array('INSERT', $sql_ary);
314
            $this->db->sql_query($sql);
315
316
            $sql_ary = array(
317
                'u2f_request' => '',
318
            );
319
320
            $this->update_session($sql_ary);
321
        }
322
        catch (U2fError $err)
323
        {
324
            $this->createError($err);
325
        }
326
    }
327
328
    /**
329
     * This method is called to show the UCP page.
330
     * You can assign template variables to the template, or do anything else here.
331
     */
332
    public function show_ucp()
333
    {
334
        $sql = 'SELECT *
335
			FROM ' . $this->registration_table . '
336
			WHERE user_id = ' . (int) $this->user->data['user_id'] . '
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
337
			ORDER BY registration_id ASC';
338
339
        $result = $this->db->sql_query($sql);
340
        $this->reg_data = array();
341
342
        while ($row = $this->db->sql_fetchrow($result))
343
        {
344
            $this->template->assign_block_vars('keys', array(
345
                'ID'            => $row['registration_id'],
346
                'REGISTERED'    => $this->user->format_date($row['registered']),
347
                'LAST_USED'     => $this->user->format_date($row['last_used']),
348
            ));
349
350
            $reg				= new registration_helper();
351
            $reg->setCounter($row['counter']);
352
            $reg->setCertificate($row['certificate']);
353
            $reg->setKeyHandle($row['key_handle']);
354
            $reg->setPublicKey($row['public_key']);
355
            $reg->id			= $row['registration_id'];
356
            $this->reg_data		= $reg;
0 ignored issues
show
Documentation Bug introduced by
It seems like $reg of type object<paul999\tfa\helper\registration_helper> is incompatible with the declared type array of property $reg_data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
357
        }
358
        $this->db->sql_freeresult($result);
359
    }
360
361
    /**
362
     * Delete a specific row from the UCP.
363
     * The data is based on the data provided in show_ucp.
364
     * @param array $data
365
     * @return mixed
366
     */
367
    public function delete($data)
368
    {
369
        if (isset($data['keys']))
370
        {
371
            $sql_where = $this->db->sql_in_set('registration_id', $data['keys']);
372
            $sql = 'DELETE FROM ' . $this->registration_table . '
373
                                                WHERE user_id = ' . (int)$this->user->data['user_id'] . '
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
374
                                                AND ' . $sql_where;
375
376
            $this->db->sql_query($sql);
377
        }
378
    }
379
380
    /**
381
     * Select all registration objects from the database
382
     * @param integer $user_id
383
     * @return array
384
     */
385
    private function getRegistrations($user_id)
386
    {
387
        $sql = 'SELECT * FROM ' . $this->registration_table . ' WHERE user_id = ' . (int) $user_id;
388
        $result = $this->db->sql_query($sql);
389
        $rows = array();
390
391
        while ($row = $this->db->sql_fetchrow($result))
392
        {
393
            $reg 				= new registration_helper();
394
            $reg->setCounter($row['counter']);
395
            $reg->setCertificate($row['certificate']);
396
            $reg->setKeyHandle($row['key_handle']);
397
            $reg->setPublicKey($row['public_key']);
398
            $reg->id 			= $row['registration_id'];
399
            $rows[] 			= $reg;
400
        }
401
402
        $this->db->sql_freeresult($result);
403
        return $rows;
404
    }
405
406
    /**
407
     * @param U2fError $error
408
     * @throws BadRequestHttpException
409
     */
410
    private function createError(U2fError $error)
411
    {
412
        switch ($error->getCode())
413
        {
414
            /** Error for the authentication message not matching any outstanding
415
             * authentication request */
416
            case U2fError::ERR_NO_MATCHING_REQUEST:
417
                throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REQUEST'), $error);
418
419
            /** Error for the authentication message not matching any registration */
420
            case U2fError::ERR_NO_MATCHING_REGISTRATION:
421
                throw new BadRequestHttpException($this->user->lang('ERR_NO_MATCHING_REGISTRATION'), $error);
422
423
            /** Error for the signature on the authentication message not verifying with
424
             * the correct key */
425
            case U2fError::ERR_AUTHENTICATION_FAILURE:
426
                throw new BadRequestHttpException($this->user->lang('ERR_AUTHENTICATION_FAILURE'), $error);
427
428
            /** Error for the challenge in the registration message not matching the
429
             * registration challenge */
430
            case U2fError::ERR_UNMATCHED_CHALLENGE:
431
                throw new BadRequestHttpException($this->user->lang('ERR_UNMATCHED_CHALLENGE'), $error);
432
433
            /** Error for the attestation signature on the registration message not
434
             * verifying */
435
            case U2fError::ERR_ATTESTATION_SIGNATURE:
436
                throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_SIGNATURE'), $error);
437
438
            /** Error for the attestation verification not verifying */
439
            case U2fError::ERR_ATTESTATION_VERIFICATION:
440
                throw new BadRequestHttpException($this->user->lang('ERR_ATTESTATION_VERIFICATION'), $error);
441
442
            /** Error for not getting good random from the system */
443
            case U2fError::ERR_BAD_RANDOM:
444
                throw new BadRequestHttpException($this->user->lang('ERR_BAD_RANDOM'), $error);
445
446
            /** Error when the counter is lower than expected */
447
            case U2fError::ERR_COUNTER_TOO_LOW:
448
                throw new BadRequestHttpException($this->user->lang('ERR_COUNTER_TOO_LOW'), $error);
449
450
            /** Error decoding public key */
451
            case U2fError::ERR_PUBKEY_DECODE:
452
                throw new BadRequestHttpException($this->user->lang('ERR_PUBKEY_DECODE'), $error);
453
454
            /** Error user-agent returned error */
455
            case U2fError::ERR_BAD_UA_RETURNING:
456
                throw new BadRequestHttpException($this->user->lang('ERR_BAD_UA_RETURNING'), $error);
457
458
            /** Error old OpenSSL version */
459
            case U2fError::ERR_OLD_OPENSSL:
460
                throw new BadRequestHttpException(sprintf($this->user->lang('ERR_OLD_OPENSSL'), OPENSSL_VERSION_TEXT), $error);
461
462
            default:
463
                throw new BadRequestHttpException($this->user->lang('TFA_UNKNOWN_ERROR'), $error);
464
        }
465
    }
466
467
    /**
468
     * Update the session with new TFA data
469
     * @param $sql_ary
470
     * @return int
471
     */
472
    private function update_session($sql_ary)
473
    {
474
        $sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $this->db->sql_build_array('UPDATE', $sql_ary) . '
475
							WHERE
476
								session_id = \'' . $this->db->sql_escape($this->user->data['session_id']) . '\' AND
477
								session_user_id = ' . (int) $this->user->data['user_id'];
0 ignored issues
show
Bug introduced by
The property data cannot be accessed from this context as it is declared private in class phpbb\session.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
478
        $this->db->sql_query($sql);
479
480
        return $this->db->sql_affectedrows();
481
    }
482
}