Failed Conditions
Push — master ( 104856...f1eb38 )
by Florent
02:37
created

SignatureResponse   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 216
rs 10
c 0
b 0
f 0
wmc 28

11 Methods

Rating   Name   Duplication   Size   Complexity  
C extractSignatureData() 0 41 7
B isValid() 0 21 5
A getKeyHandle() 0 3 1
A create() 0 3 1
A isUserPresence() 0 3 1
A getCounter() 0 3 1
A retrieveKeyHandle() 0 7 3
A __construct() 0 12 4
A getSignature() 0 3 1
A getClientData() 0 3 1
A retrieveClientData() 0 7 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace U2FAuthentication\Fido;
15
16
use Base64Url\Base64Url;
17
18
class SignatureResponse
19
{
20
    /**
21
     * @var ClientData
22
     */
23
    private $clientData;
24
25
    /**
26
     * @var KeyHandle
27
     */
28
    private $keyHandle;
29
30
    /**
31
     * @var bool
32
     */
33
    private $userPresence;
34
35
    /**
36
     * @var string
37
     */
38
    private $userPresenceByte;
39
40
    /**
41
     * @var int
42
     */
43
    private $counter;
44
45
    /**
46
     * @var string
47
     */
48
    private $counterBytes;
49
50
    /**
51
     * @var string
52
     */
53
    private $signature;
54
55
    /**
56
     * RegistrationChallengeMiddleware constructor.
57
     *
58
     * @param array $data
59
     */
60
    private function __construct(array $data)
61
    {
62
        if (array_key_exists('errorCode', $data) && 0 !== $data['errorCode']) {
63
            throw new \InvalidArgumentException('Invalid response.');
64
        }
65
66
        $this->keyHandle = $this->retrieveKeyHandle($data);
67
        $this->clientData = $this->retrieveClientData($data);
68
        if ('navigator.id.getAssertion' !== $this->clientData->getType()) {
69
            throw new \InvalidArgumentException('Invalid response.');
70
        }
71
        list($this->userPresence, $this->userPresenceByte, $this->counter, $this->counterBytes, $this->signature) = $this->extractSignatureData($data);
72
    }
73
74
    /**
75
     * @param array $data
76
     *
77
     * @return SignatureResponse
78
     */
79
    public static function create(array $data): self
80
    {
81
        return new self($data);
82
    }
83
84
    /**
85
     * @return ClientData
86
     */
87
    public function getClientData(): ClientData
88
    {
89
        return $this->clientData;
90
    }
91
92
    /**
93
     * @return KeyHandle
94
     */
95
    public function getKeyHandle(): KeyHandle
96
    {
97
        return $this->keyHandle;
98
    }
99
100
    /**
101
     * @return bool
102
     */
103
    public function isUserPresence(): bool
104
    {
105
        return $this->userPresence;
106
    }
107
108
    /**
109
     * @return int
110
     */
111
    public function getCounter(): int
112
    {
113
        return $this->counter;
114
    }
115
116
    /**
117
     * @return string
118
     */
119
    public function getSignature(): string
120
    {
121
        return $this->signature;
122
    }
123
124
    /**
125
     * @param array $data
126
     *
127
     * @throws \InvalidArgumentException
128
     *
129
     * @return KeyHandle
130
     */
131
    private function retrieveKeyHandle(array $data): KeyHandle
132
    {
133
        if (!array_key_exists('keyHandle', $data) || !is_string($data['keyHandle'])) {
134
            throw new \InvalidArgumentException('Invalid response.');
135
        }
136
137
        return KeyHandle::create(Base64Url::decode($data['keyHandle']));
138
    }
139
140
    /**
141
     * @param array $data
142
     *
143
     * @throws \InvalidArgumentException
144
     *
145
     * @return ClientData
146
     */
147
    private function retrieveClientData(array $data): ClientData
148
    {
149
        if (!array_key_exists('clientData', $data) || !is_string($data['clientData'])) {
150
            throw new \InvalidArgumentException('Invalid response.');
151
        }
152
153
        return ClientData::create($data['clientData']);
154
    }
155
156
    /**
157
     * @param array $data
158
     *
159
     * @throws \InvalidArgumentException
160
     *
161
     * @return array
162
     */
163
    private function extractSignatureData(array $data): array
164
    {
165
        if (!array_key_exists('signatureData', $data) || !is_string($data['signatureData'])) {
166
            throw new \InvalidArgumentException('Invalid response.');
167
        }
168
169
        $stream = fopen('php://memory', 'r+');
170
        if (false === $stream) {
171
            throw new \InvalidArgumentException('Unable to load the registration data.');
172
        }
173
        $signatureData = Base64Url::decode($data['signatureData']);
174
        fwrite($stream, $signatureData);
175
        rewind($stream);
176
177
        $userPresenceByte = fread($stream, 1);
178
        if (mb_strlen($userPresenceByte, '8bit') !== 1) {
179
            fclose($stream);
180
181
            throw new \InvalidArgumentException('Invalid response.');
182
        }
183
        $userPresence = (bool) ord($userPresenceByte);
184
185
        $counterBytes = fread($stream, 4);
186
        if (mb_strlen($counterBytes, '8bit') !== 4) {
187
            fclose($stream);
188
189
            throw new \InvalidArgumentException('Invalid response.');
190
        }
191
        $counter = unpack('Nctr', $counterBytes)['ctr'];
192
        $signature = '';
193
        while (!feof($stream)) {
194
            $signature .= fread($stream, 1024);
195
        }
196
        fclose($stream);
197
198
        return [
199
            $userPresence,
200
            $userPresenceByte,
201
            $counter,
202
            $counterBytes,
203
            $signature,
204
        ];
205
    }
206
207
    /**
208
     * @param SignatureRequest $request
209
     * @param int|null         $currentCounter
210
     *
211
     * @return bool
212
     */
213
    public function isValid(SignatureRequest $request, ?int $currentCounter = null): bool
214
    {
215
        if (!hash_equals($request->getChallenge(), $this->clientData->getChallenge())) {
216
            return false;
217
        }
218
        if (!hash_equals($request->getApplicationId(), $this->clientData->getOrigin())) {
219
            return false;
220
        }
221
222
        if ($currentCounter !== null && $currentCounter >= $this->counter) {
223
            return false;
224
        }
225
226
        $dataToVerify = hash('sha256', $this->clientData->getOrigin(), true);
227
        $dataToVerify .= $this->userPresenceByte;
228
        $dataToVerify .= $this->counterBytes;
229
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
230
231
        $registeredKey = $request->getRegisteredKey($this->keyHandle);
232
233
        return openssl_verify($dataToVerify, $this->signature, $registeredKey->getPublicKeyAsPem(), OPENSSL_ALGO_SHA256) === 1;
234
    }
235
}
236