Completed
Push — master ( 2c97ea...cf2c3c )
by Florent
02:00
created

SignatureResponse::retrieveClientData()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 1
nop 1
dl 0
loc 5
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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
19
class SignatureResponse
20
{
21
    /**
22
     * @var ClientData
23
     */
24
    private $clientData;
25
26
    /**
27
     * @var KeyHandler
28
     */
29
    private $keyHandle;
30
31
    /**
32
     * @var bool
33
     */
34
    private $userPresence;
35
36
    /**
37
     * @var string
38
     */
39
    private $userPresenceByte;
40
41
    /**
42
     * @var int
43
     */
44
    private $counter;
45
46
    /**
47
     * @var string
48
     */
49
    private $counterBytes;
50
51
    /**
52
     * @var string
53
     */
54
    private $signature;
55
56
    public function __construct(array $data)
57
    {
58
        Assertion::false(array_key_exists('errorCode', $data) && 0 !== $data['errorCode'], 'Invalid response.');
59
60
        $this->keyHandle = $this->retrieveKeyHandle($data);
61
        $this->clientData = $this->retrieveClientData($data);
62
        Assertion::eq('navigator.id.getAssertion', $this->clientData->getType(), 'Invalid response.');
63
        list($this->userPresence, $this->userPresenceByte, $this->counter, $this->counterBytes, $this->signature) = $this->extractSignatureData($data);
64
    }
65
66
    public function getClientData(): ClientData
67
    {
68
        return $this->clientData;
69
    }
70
71
    public function getKeyHandle(): KeyHandler
72
    {
73
        return $this->keyHandle;
74
    }
75
76
    public function isUserPresent(): bool
77
    {
78
        return $this->userPresence;
79
    }
80
81
    public function getCounter(): int
82
    {
83
        return $this->counter;
84
    }
85
86
    public function getSignature(): string
87
    {
88
        return $this->signature;
89
    }
90
91
    private function retrieveKeyHandle(array $data): KeyHandler
92
    {
93
        Assertion::false(!array_key_exists('keyHandle', $data) || !\is_string($data['keyHandle']), 'Invalid response.');
94
95
        return new KeyHandler(Base64Url::decode($data['keyHandle']));
96
    }
97
98
    private function retrieveClientData(array $data): ClientData
99
    {
100
        Assertion::false(!array_key_exists('clientData', $data) || !\is_string($data['clientData']), 'Invalid response.');
101
102
        return new ClientData($data['clientData']);
103
    }
104
105
    private function extractSignatureData(array $data): array
106
    {
107
        Assertion::false(!array_key_exists('signatureData', $data) || !\is_string($data['signatureData']), 'Invalid response.');
108
109
        $stream = \Safe\fopen('php://memory', 'r+');
110
        $signatureData = Base64Url::decode($data['signatureData']);
111
        \Safe\fwrite($stream, $signatureData);
112
        \Safe\rewind($stream);
113
114
        try {
115
            $userPresenceByte = \Safe\fread($stream, 1);
116
            Assertion::eq(1, mb_strlen($userPresenceByte, '8bit'), 'Invalid response.');
117
            $userPresence = (bool) \ord($userPresenceByte);
118
119
            $counterBytes = \Safe\fread($stream, 4);
120
            Assertion::eq(4, mb_strlen($counterBytes, '8bit'), 'Invalid response.');
121
            $counter = unpack('Nctr', $counterBytes)['ctr'];
122
            $signature = '';
123
            while (!feof($stream)) {
124
                $signature .= \Safe\fread($stream, 1024);
125
            }
126
            \Safe\fclose($stream);
127
        } catch (\Throwable $throwable) {
128
            \Safe\fclose($stream);
129
            throw $throwable;
130
        }
131
132
        return [
133
            $userPresence,
134
            $userPresenceByte,
135
            $counter,
136
            $counterBytes,
137
            $signature,
138
        ];
139
    }
140
141
    public function isValid(SignatureRequest $request, ?int $currentCounter = null): bool
142
    {
143
        if (!hash_equals($request->getChallenge(), $this->clientData->getChallenge())) {
144
            return false;
145
        }
146
        if (!hash_equals($request->getApplicationId(), $this->clientData->getOrigin())) {
147
            return false;
148
        }
149
150
        if (null !== $currentCounter && $currentCounter >= $this->counter) {
151
            return false;
152
        }
153
154
        $dataToVerify = hash('sha256', $this->clientData->getOrigin(), true);
155
        $dataToVerify .= $this->userPresenceByte;
156
        $dataToVerify .= $this->counterBytes;
157
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
158
159
        $registeredKey = $request->getRegisteredKey($this->keyHandle);
160
161
        return 1 === openssl_verify($dataToVerify, $this->signature, $registeredKey->getPublicKeyAsPem(), OPENSSL_ALGO_SHA256);
162
    }
163
}
164