Completed
Push — master ( 6f0ba5...dd01b0 )
by Florent
03:51
created

SignatureResponse::isUserPresence()   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
    public function __construct(array $data)
56
    {
57
        if (array_key_exists('errorCode', $data) && 0 !== $data['errorCode']) {
58
            throw new \InvalidArgumentException('Invalid response.');
59
        }
60
61
        $this->keyHandle = $this->retrieveKeyHandle($data);
62
        $this->clientData = $this->retrieveClientData($data);
63
        if ('navigator.id.getAssertion' !== $this->clientData->getType()) {
64
            throw new \InvalidArgumentException('Invalid response.');
65
        }
66
        list($this->userPresence, $this->userPresenceByte, $this->counter, $this->counterBytes, $this->signature) = $this->extractSignatureData($data);
67
    }
68
69
    public function getClientData(): ClientData
70
    {
71
        return $this->clientData;
72
    }
73
74
    public function getKeyHandle(): KeyHandler
75
    {
76
        return $this->keyHandle;
77
    }
78
79
    public function isUserPresent(): bool
80
    {
81
        return $this->userPresence;
82
    }
83
84
    public function getCounter(): int
85
    {
86
        return $this->counter;
87
    }
88
89
    public function getSignature(): string
90
    {
91
        return $this->signature;
92
    }
93
94
    private function retrieveKeyHandle(array $data): KeyHandler
95
    {
96
        if (!array_key_exists('keyHandle', $data) || !\is_string($data['keyHandle'])) {
97
            throw new \InvalidArgumentException('Invalid response.');
98
        }
99
100
        return new KeyHandler(Base64Url::decode($data['keyHandle']));
101
    }
102
103
    private function retrieveClientData(array $data): ClientData
104
    {
105
        if (!array_key_exists('clientData', $data) || !\is_string($data['clientData'])) {
106
            throw new \InvalidArgumentException('Invalid response.');
107
        }
108
109
        return new ClientData($data['clientData']);
110
    }
111
112
    private function extractSignatureData(array $data): array
113
    {
114
        if (!array_key_exists('signatureData', $data) || !\is_string($data['signatureData'])) {
115
            throw new \InvalidArgumentException('Invalid response.');
116
        }
117
118
        $stream = \Safe\fopen('php://memory', 'r+');
119
        $signatureData = Base64Url::decode($data['signatureData']);
120
        \Safe\fwrite($stream, $signatureData);
121
        \Safe\rewind($stream);
122
123
        $userPresenceByte = \Safe\fread($stream, 1);
124
        if (1 !== mb_strlen($userPresenceByte, '8bit')) {
125
            \Safe\fclose($stream);
126
127
            throw new \InvalidArgumentException('Invalid response.');
128
        }
129
        $userPresence = (bool) \ord($userPresenceByte);
130
131
        $counterBytes = \Safe\fread($stream, 4);
132
        if (4 !== mb_strlen($counterBytes, '8bit')) {
133
            \Safe\fclose($stream);
134
135
            throw new \InvalidArgumentException('Invalid response.');
136
        }
137
        $counter = unpack('Nctr', $counterBytes)['ctr'];
138
        $signature = '';
139
        while (!feof($stream)) {
140
            $signature .= \Safe\fread($stream, 1024);
141
        }
142
        \Safe\fclose($stream);
143
144
        return [
145
            $userPresence,
146
            $userPresenceByte,
147
            $counter,
148
            $counterBytes,
149
            $signature,
150
        ];
151
    }
152
153
    public function isValid(SignatureRequest $request, ?int $currentCounter = null): bool
154
    {
155
        if (!hash_equals($request->getChallenge(), $this->clientData->getChallenge())) {
156
            return false;
157
        }
158
        if (!hash_equals($request->getApplicationId(), $this->clientData->getOrigin())) {
159
            return false;
160
        }
161
162
        if (null !== $currentCounter && $currentCounter >= $this->counter) {
163
            return false;
164
        }
165
166
        $dataToVerify = hash('sha256', $this->clientData->getOrigin(), true);
167
        $dataToVerify .= $this->userPresenceByte;
168
        $dataToVerify .= $this->counterBytes;
169
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
170
171
        $registeredKey = $request->getRegisteredKey($this->keyHandle);
172
173
        return 1 === openssl_verify($dataToVerify, $this->signature, $registeredKey->getPublicKeyAsPem(), OPENSSL_ALGO_SHA256);
174
    }
175
}
176