Completed
Pull Request — master (#19)
by Florent
01:56 queued 28s
created

SignatureResponse::isUserPresent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-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 KeyHandler
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
    private function __construct(array $data)
59
    {
60
        if (array_key_exists('errorCode', $data) && 0 !== $data['errorCode']) {
61
            throw new \InvalidArgumentException('Invalid response.');
62
        }
63
64
        $this->keyHandle = $this->retrieveKeyHandle($data);
65
        $this->clientData = $this->retrieveClientData($data);
66
        if ('navigator.id.getAssertion' !== $this->clientData->getType()) {
67
            throw new \InvalidArgumentException('Invalid response.');
68
        }
69
        list($this->userPresence, $this->userPresenceByte, $this->counter, $this->counterBytes, $this->signature) = $this->extractSignatureData($data);
70
    }
71
72
    /**
73
     * @return SignatureResponse
74
     */
75
    public static function create(array $data): self
76
    {
77
        return new self($data);
78
    }
79
80
    public function getClientData(): ClientData
81
    {
82
        return $this->clientData;
83
    }
84
85
    public function getKeyHandle(): KeyHandler
86
    {
87
        return $this->keyHandle;
88
    }
89
90
    public function isUserPresent(): bool
91
    {
92
        return $this->userPresence;
93
    }
94
95
    public function getCounter(): int
96
    {
97
        return $this->counter;
98
    }
99
100
    public function getSignature(): string
101
    {
102
        return $this->signature;
103
    }
104
105
    /**
106
     * @throws \InvalidArgumentException
107
     */
108
    private function retrieveKeyHandle(array $data): KeyHandler
109
    {
110
        if (!array_key_exists('keyHandle', $data) || !\is_string($data['keyHandle'])) {
111
            throw new \InvalidArgumentException('Invalid response.');
112
        }
113
114
        return KeyHandler::create(Base64Url::decode($data['keyHandle']));
115
    }
116
117
    /**
118
     * @throws \InvalidArgumentException
119
     */
120
    private function retrieveClientData(array $data): ClientData
121
    {
122
        if (!array_key_exists('clientData', $data) || !\is_string($data['clientData'])) {
123
            throw new \InvalidArgumentException('Invalid response.');
124
        }
125
126
        return ClientData::create($data['clientData']);
127
    }
128
129
    /**
130
     * @throws \InvalidArgumentException
131
     */
132
    private function extractSignatureData(array $data): array
133
    {
134
        if (!array_key_exists('signatureData', $data) || !\is_string($data['signatureData'])) {
135
            throw new \InvalidArgumentException('Invalid response.');
136
        }
137
138
        $stream = fopen('php://memory', 'r+');
139
        if (false === $stream) {
140
            throw new \InvalidArgumentException('Unable to load the registration data.');
141
        }
142
        $signatureData = Base64Url::decode($data['signatureData']);
143
        fwrite($stream, $signatureData);
144
        rewind($stream);
145
146
        $userPresenceByte = fread($stream, 1);
147
        if (1 !== mb_strlen($userPresenceByte, '8bit')) {
148
            fclose($stream);
149
150
            throw new \InvalidArgumentException('Invalid response.');
151
        }
152
        $userPresence = (bool) \ord($userPresenceByte);
153
154
        $counterBytes = fread($stream, 4);
155
        if (4 !== mb_strlen($counterBytes, '8bit')) {
156
            fclose($stream);
157
158
            throw new \InvalidArgumentException('Invalid response.');
159
        }
160
        $counter = unpack('Nctr', $counterBytes)['ctr'];
161
        $signature = '';
162
        while (!feof($stream)) {
163
            $signature .= fread($stream, 1024);
164
        }
165
        fclose($stream);
166
167
        return [
168
            $userPresence,
169
            $userPresenceByte,
170
            $counter,
171
            $counterBytes,
172
            $signature,
173
        ];
174
    }
175
176
    public function isValid(SignatureRequest $request, ?int $currentCounter = null): bool
177
    {
178
        if (!hash_equals($request->getChallenge(), $this->clientData->getChallenge())) {
179
            return false;
180
        }
181
        if (!hash_equals($request->getApplicationId(), $this->clientData->getOrigin())) {
182
            return false;
183
        }
184
185
        if (null !== $currentCounter && $currentCounter >= $this->counter) {
186
            return false;
187
        }
188
189
        $dataToVerify = hash('sha256', $this->clientData->getOrigin(), true);
190
        $dataToVerify .= $this->userPresenceByte;
191
        $dataToVerify .= $this->counterBytes;
192
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
193
194
        $registeredKey = $request->getRegisteredKey($this->keyHandle);
195
196
        return 1 === openssl_verify($dataToVerify, $this->signature, $registeredKey->getPublicKeyAsPem(), OPENSSL_ALGO_SHA256);
197
    }
198
}
199