Failed Conditions
Pull Request — oauthcreation (#531)
by Simon
06:20
created

YubikeyOtpCredentialProvider   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 0
loc 153
rs 10
c 0
b 0
f 0
wmc 18
lcom 1
cbo 5

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A authenticate() 0 19 4
A setCredential() 0 22 3
A getYubikeyData() 0 10 2
A parseYubicoApiResult() 0 14 3
A getYubikeyId() 0 4 1
A verifyHmac() 0 16 2
A verifyToken() 0 16 2
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Security\CredentialProviders;
10
11
use Waca\DataObjects\User;
12
use Waca\Exceptions\ApplicationLogicException;
13
use Waca\Helpers\HttpHelper;
14
use Waca\PdoDatabase;
15
use Waca\SiteConfiguration;
16
17
class YubikeyOtpCredentialProvider extends CredentialProviderBase
18
{
19
    /** @var HttpHelper */
20
    private $httpHelper;
21
    /**
22
     * @var SiteConfiguration
23
     */
24
    private $configuration;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
25
26
    public function __construct(PdoDatabase $database, SiteConfiguration $configuration, HttpHelper $httpHelper)
27
    {
28
        parent::__construct($database, $configuration, 'yubikeyotp');
29
        $this->httpHelper = $httpHelper;
30
        $this->configuration = $configuration;
31
    }
32
33
    public function authenticate(User $user, $data)
34
    {
35
        if (is_array($data)) {
36
            return false;
37
        }
38
39
        $credentialData = $this->getCredentialData($user->getId());
40
41
        if ($credentialData === null) {
42
            return false;
43
        }
44
45
        if ($credentialData->getData() !== $this->getYubikeyId($data)) {
46
            // different device
47
            return false;
48
        }
49
50
        return $this->verifyToken($data);
51
    }
52
53
    public function setCredential(User $user, $factor, $data)
54
    {
55
        $keyId = $this->getYubikeyId($data);
56
        $valid = $this->verifyToken($data);
57
58
        if (!$valid) {
59
            throw new ApplicationLogicException("Provided token is not valid.");
60
        }
61
62
        $storedData = $this->getCredentialData($user->getId());
63
64
        if ($storedData === null) {
65
            $storedData = $this->createNewCredential($user);
66
        }
67
68
        $storedData->setData($keyId);
69
        $storedData->setFactor($factor);
70
        $storedData->setVersion(1);
71
        $storedData->setPriority(8);
72
73
        $storedData->save();
74
    }
75
76
    /**
77
     * Get the Yubikey ID.
78
     *
79
     * This looks like it's just dumping the "password" that's stored in the database, but it's actually fine.
80
     *
81
     * We only store the "serial number" of the Yubikey - if we get a validated (by webservice) token prefixed with the
82
     * serial number, that's a successful OTP authentication. Thus, retrieving the stored data is just retrieving the
83
     * yubikey's serial number (in modhex format), since the actual security credentials are stored on the device.
84
     *
85
     * Note that the serial number is actually the credential serial number - it's possible to regenerate the keys on
86
     * the device, and that will change the serial number too.
87
     *
88
     * More information about the structure of OTPs can be found here:
89
     * https://developers.yubico.com/OTP/OTPs_Explained.html
90
     *
91
     * @param int $userId
92
     *
93
     * @return null|string
94
     */
95
    public function getYubikeyData($userId)
96
    {
97
        $credential = $this->getCredentialData($userId);
98
99
        if ($credential === null) {
100
            return null;
101
        }
102
103
        return $credential->getData();
104
    }
105
106
    /**
107
     * @param $result
108
     *
109
     * @return array
110
     */
111
    private function parseYubicoApiResult($result)
112
    {
113
        $data = array();
114
        foreach (explode("\r\n", $result) as $line) {
115
            $pos = strpos($line, '=');
116
            if ($pos === false) {
117
                continue;
118
            }
119
120
            $data[substr($line, 0, $pos)] = substr($line, $pos + 1);
121
        }
122
123
        return $data;
124
    }
125
126
    private function getYubikeyId($data)
127
    {
128
        return substr($data, 0, -32);
129
    }
130
131
    private function verifyHmac($apiResponse, $apiKey)
132
    {
133
        ksort($apiResponse);
134
        $signature = $apiResponse['h'];
135
        unset($apiResponse['h']);
136
137
        $data = array();
138
        foreach ($apiResponse as $key => $value) {
139
            $data[] = $key . "=" . $value;
140
        }
141
        $dataString = implode('&', $data);
142
143
        $hmac = base64_encode(hash_hmac('sha1', $dataString, base64_decode($apiKey), true));
144
145
        return $hmac === $signature;
146
    }
147
148
    /**
149
     * @param $data
150
     *
151
     * @return bool
152
     */
153
    private function verifyToken($data)
154
    {
155
        $result = $this->httpHelper->get('https://api.yubico.com/wsapi/2.0/verify', array(
156
            'id'    => $this->configuration->getYubicoApiId(),
157
            'otp'   => $data,
158
            'nonce' => md5(openssl_random_pseudo_bytes(64)),
159
        ));
160
161
        $apiResponse = $this->parseYubicoApiResult($result);
162
163
        if (!$this->verifyHmac($apiResponse, $this->configuration->getYubicoApiKey())) {
164
            return false;
165
        }
166
167
        return $apiResponse['status'] == 'OK';
168
    }
169
}
170