Handshake::hello()   B
last analyzed

Complexity

Conditions 7
Paths 13

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 32
rs 8.8333
ccs 14
cts 14
cp 1
cc 7
nc 13
nop 1
crap 7
1
<?php
2
declare(strict_types = 1);
3
4
/**
5
 * @license http://www.apache.org/licenses/ Apache License 2.0
6
 * @license https://github.com/danielmewes/php-rql Apache License 2.0
7
 * @Author Daniel Mewes https://github.com/danielmewes
8
 * @Author Timon Bolier https://github.com/tbolier
9
 *
10
 * The Handshake class contains parts of code copied from the original PHP-RQL library under the Apache License 2.0:
11
 * @see https://github.com/danielmewes/php-rql/blob/master/rdb/Handshake.php
12
 *
13
 * Stating the following changes have been done to the parts of the code copied in the Handshake Class:
14
 * - Amendments to code styles and control structures.
15
 * - Abstraction of code to new methods to improve readability.
16
 * - Removed obsolete code.
17
 */
18
19
namespace TBolier\RethinkQL\Connection\Socket;
20
21
use Psr\Http\Message\StreamInterface;
22
23
class Handshake implements HandshakeInterface
24
{
25
    /**
26
     * @var string
27
     */
28
    private $username;
29
30
    /**
31
     * @var string
32
     */
33
    private $password;
34
35
    /**
36
     * @var string
37
     */
38
    private $protocolVersion = 0;
39
40
    /**
41
     * @var int
42
     */
43
    private $state;
44
45
    /**
46
     * @var string
47
     */
48
    private $myR;
49
50
    /**
51
     * @var string
52
     */
53
    private $clientFirstMessage;
54
55
    /**
56
     * @var string
57
     */
58
    private $serverSignature;
59
60
    /**
61
     * @var int
62
     */
63
    private $version;
64
65
    public function __construct(string $username, string $password, int $version)
66
    {
67
        $this->username = $username;
68
        $this->password = $password;
69
        $this->state = 0;
70 23
        $this->version = $version;
71
    }
72 23
73 23
    /**
74 23
     * @throws \RuntimeException
75 23
     * @throws Exception
76 23
     */
77
    public function hello(StreamInterface $stream): void
78
    {
79
        try {
80
            $handshakeResponse = null;
81
            while (true) {
82
                if (!$stream->isWritable()) {
83 21
                    throw new Exception('Not connected');
84
                }
85
86 21
                $this->checkResponse($handshakeResponse);
87 21
88 21
                try {
89 1
                    $msg = $this->nextMessage($handshakeResponse);
90
                } catch (Exception $e) {
91
                    $stream->close();
92 20
                    throw $e;
93
                }
94
95 20
                if ($msg === 'successful') {
96 2
                    break;
97 2
                }
98 2
99
                if ($msg !== '') {
100
                    $stream->write($msg);
101 20
                }
102 16
103
                // Read null-terminated response
104
                $handshakeResponse = $stream->getContents();
105 20
            }
106 20
        } catch (Exception $e) {
107
            $stream->close();
108
            throw $e;
109
        }
110 20
    }
111
112 5
    private function nextMessage(string $response = null): ?string
113 5
    {
114 5
        if ($response === null) {
115
            return $this->createHandshakeMessage();
116
        }
117 16
118
        switch ($this->state) {
119
            case 1:
120
                return $this->verifyProtocol($response);
121
            case 2:
122
                return $this->createAuthenticationMessage($response);
123
            case 3:
124 20
                return $this->verifyAuthentication($response);
125
            default:
126 20
                throw new Exception('Illegal handshake state');
127 20
        }
128 20
    }
129 18
130 18
    private function pkbdf2Hmac(string $password, string $salt, int $iterations): string
131 16
    {
132 16
        $t = hash_hmac('sha256', $salt."\x00\x00\x00\x01", $password, true);
133 16
        $u = $t;
134 16
        for ($i = 0; $i < $iterations - 1; ++$i) {
135
            $t = hash_hmac('sha256', $t, $password, true);
136
            $u ^= $t;
137
        }
138
139
        return $u;
140
    }
141
142
    private function createHandshakeMessage(): string
143
    {
144
        $this->myR = base64_encode(openssl_random_pseudo_bytes(18));
145
        $this->clientFirstMessage = 'n='.$this->username.',r='.$this->myR;
146 16
147
        $binaryVersion = pack('V', $this->version);
148 16
149 16
        $this->state = 1;
150 16
151
        return
152
            $binaryVersion
153
            . json_encode(
154
                [
155 16
                    'protocol_version' => $this->protocolVersion,
156
                    'authentication_method' => 'SCRAM-SHA-256',
157
                    'authentication' => 'n,,'.$this->clientFirstMessage,
158
                ]
159
            )
160
            . \chr(0);
161
    }
162 20
163
    private function verifyProtocol(?string $response): string
164 20
    {
165
        if (strpos($response, 'ERROR') === 0) {
166 20
            throw new Exception(
167 20
                'Received an unexpected reply. You may be attempting to connect to '
168
                . 'a RethinkDB server that is too old for this driver. The minimum '
169 20
                . 'supported server version is 2.3.0.'
170
            );
171 20
        }
172
173
        $json = json_decode($response, true);
174
        if ($json['success'] === false) {
175 20
            throw new Exception('Handshake failed: '.$json["error"]);
176
        }
177 20
        if ($this->protocolVersion > $json['max_protocol_version']
178 20
            || $this->protocolVersion < $json['min_protocol_version']) {
179 20
            throw new Exception('Unsupported protocol version.');
180
        }
181
182 20
        $this->state = 2;
183
184
        return '';
185
    }
186
187
    /**
188
     * @throws Exception
189
     */
190 18
    private function createAuthenticationMessage($response): string
191
    {
192 18
        $json = json_decode($response, true);
193
        if ($json['success'] === false) {
194
            throw new Exception('Handshake failed: '.$json['error']);
195
        }
196
        $serverFirstMessage = $json['authentication'];
197
        $authentication = [];
198
        foreach (explode(',', $json['authentication']) as $var) {
199
            $pair = explode('=', $var);
200 18
            $authentication[$pair[0]] = $pair[1];
201 18
        }
202 1
        $serverR = $authentication['r'];
203
        if (strpos($serverR, $this->myR) !== 0) {
204 17
            throw new Exception('Invalid nonce from server.');
205 17
        }
206 1
        $salt = base64_decode($authentication['s']);
207
        $iterations = (int) $authentication['i'];
208
209 16
        $clientFinalMessageWithoutProof = 'c=biws,r='.$serverR;
210
        $saltedPassword = $this->pkbdf2Hmac($this->password, $salt, $iterations);
211 16
        $clientKey = hash_hmac('sha256', 'Client Key', $saltedPassword, true);
212
        $storedKey = hash('sha256', $clientKey, true);
213
214
        $authMessage =
215
            $this->clientFirstMessage.','.$serverFirstMessage.','.$clientFinalMessageWithoutProof;
216
217
        $clientSignature = hash_hmac('sha256', $authMessage, $storedKey, true);
218
219 16
        $clientProof = $clientKey ^ $clientSignature;
220
221 16
        $serverKey = hash_hmac('sha256', 'Server Key', $saltedPassword, true);
222 16
223
        $this->serverSignature = hash_hmac('sha256', $authMessage, $serverKey, true);
224
225 16
        $this->state = 3;
226 16
227 16
        return
228 16
            json_encode(
229 16
                [
230
                    'authentication' => $clientFinalMessageWithoutProof.',p='.base64_encode($clientProof),
231 16
                ]
232 16
            )
233
            . \chr(0);
234
    }
235 16
236 16
    /**
237
     * @throws Exception
238 16
     */
239 16
    private function verifyAuthentication(string $response): string
240 16
    {
241 16
        $json = json_decode($response, true);
242
        if ($json['success'] === false) {
243
            throw new Exception('Handshake failed: '.$json['error']);
244 16
        }
245
        $authentication = [];
246 16
        foreach (explode(',', $json['authentication']) as $var) {
247
            $pair = explode('=', $var);
248 16
            $authentication[$pair[0]] = $pair[1];
249
        }
250 16
251
        $this->checkSignature(base64_decode($authentication['v']));
252 16
253
        $this->state = 4;
254 16
255
        return 'successful';
256
    }
257 16
258
    private function checkResponse(?string $handshakeResponse): void
259 16
    {
260
        if ($handshakeResponse !== null && preg_match(
261
            '/^ERROR:\s(.+)$/i',
262 16
            $handshakeResponse,
263
            $errorMatch
264
        )) {
265
            throw new Exception($errorMatch[1]);
266
        }
267
    }
268
269
    /**
270 16
     * @throws Exception
271
     */
272 16
    private function checkSignature(string $signature): void
273 16
    {
274
        if (!hash_equals($signature, $this->serverSignature)) {
275
            throw new Exception('Invalid server signature.');
276 16
        }
277 16
    }
278
}
279