Passed
Pull Request — master (#18)
by Marc
03:37
created

Handshake::hello()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 20
nc 13
nop 1
dl 0
loc 35
ccs 19
cts 19
cp 1
crap 7
rs 6.7272
c 0
b 0
f 0
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
    /**
66
     * @param string $username
67
     * @param string $password
68
     * @param int $version
69
     */
70 21
    public function __construct(string $username, string $password, int $version)
71
    {
72 21
        $this->username = $username;
73 21
        $this->password = $password;
74 21
        $this->state = 0;
75 21
        $this->version = $version;
76 21
    }
77
78
    /**
79
     * @inheritdoc
80
     * @throws \RuntimeException
81
     * @throws Exception
82
     */
83 19
    public function hello(StreamInterface $stream): void
84
    {
85
        try {
86 19
            $handshakeResponse = null;
87 19
            while (true) {
88 19
                if (!$stream->isWritable()) {
89 1
                    throw new Exception('Not connected');
90
                }
91
92 18
                $this->checkResponse($handshakeResponse);
93
94
                try {
95 18
                    $msg = $this->nextMessage($handshakeResponse);
96 2
                } catch (Exception $e) {
97 2
                    $stream->close();
98 2
                    throw $e;
99
                }
100
101 18
                if ($msg === 'successful') {
102 14
                    break;
103
                }
104
105 18
                if ($msg !== '') {
106 18
                    $stream->write($msg);
107
                }
108
109
                // Read null-terminated response
110 18
                $handshakeResponse = $stream->getContents();
111
            }
112 5
        } catch (Exception $e) {
113 5
            $stream->close();
114 5
            throw $e;
115
        }
116
117 14
    }
118
119
    /**
120
     * @param $response
121
     * @return string
122
     * @throws Exception
123
     */
124 18
    private function nextMessage(string $response = null): ?string
125
    {
126 18
        switch ($this->state) {
127 18
            case 0:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
128 18
                return $this->createHandshakeMessage($response);
129 16
            case 1:
130 16
                return $this->verifyProtocol($response);
131 14
            case 2:
132 14
                return $this->createAuthenticationMessage($response);
133 14
            case 3:
134 14
                return $this->verifyAuthentication($response);
135
            default:
136
                throw new Exception('Illegal handshake state');
137
        }
138
    }
139
140
    /**
141
     * @param string $password
142
     * @param string $salt
143
     * @param int $iterations
144
     * @return string
145
     */
146 14
    private function pkbdf2Hmac(string $password, string $salt, int $iterations): string
147
    {
148 14
        $t = hash_hmac('sha256', $salt . "\x00\x00\x00\x01", $password, true);
149 14
        $u = $t;
150 14
        for ($i = 0; $i < $iterations - 1; ++$i) {
151
            $t = hash_hmac('sha256', $t, $password, true);
152
            $u ^= $t;
153
        }
154
155 14
        return $u;
156
    }
157
158
    /**
159
     * @param null|string $response
160
     * @return string
161
     */
162 18
    private function createHandshakeMessage(?string $response): string
163
    {
164 18
        $response === null or die('Illegal handshake state');
165
166 18
        $this->myR = base64_encode(openssl_random_pseudo_bytes(18));
167 18
        $this->clientFirstMessage = 'n=' . $this->username . ',r=' . $this->myR;
168
169 18
        $binaryVersion = pack('V', $this->version);
170
171 18
        $this->state = 1;
172
173
        return
174
            $binaryVersion
175 18
            . json_encode(
176
                [
177 18
                    'protocol_version' => $this->protocolVersion,
178 18
                    'authentication_method' => 'SCRAM-SHA-256',
179 18
                    'authentication' => 'n,,' . $this->clientFirstMessage,
180
                ]
181
            )
182 18
            . \chr(0);
183
    }
184
185
    /**
186
     * @param null|string $response
187
     * @return string
188
     * @throws Exception
189
     */
190 16
    private function verifyProtocol(?string $response): string
191
    {
192 16
        if (strpos($response, 'ERROR') === 0) {
193
            throw new Exception(
194
                'Received an unexpected reply. You may be attempting to connect to '
195
                . 'a RethinkDB server that is too old for this driver. The minimum '
196
                . 'supported server version is 2.3.0.'
197
            );
198
        }
199
200 16
        $json = json_decode($response, true);
201 16
        if ($json['success'] === false) {
202 1
            throw new Exception('Handshake failed: ' . $json["error"]);
203
        }
204 15
        if ($this->protocolVersion > $json['max_protocol_version']
205 15
            || $this->protocolVersion < $json['min_protocol_version']) {
206 1
            throw new Exception('Unsupported protocol version.');
207
        }
208
209 14
        $this->state = 2;
210
211 14
        return '';
212
    }
213
214
    /**
215
     * @param $response
216
     * @return null|string
217
     * @throws Exception
218
     */
219 14
    private function createAuthenticationMessage($response): string
220
    {
221 14
        $json = json_decode($response, true);
222 14
        if ($json['success'] === false) {
223
            throw new Exception('Handshake failed: ' . $json['error']);
224
        }
225 14
        $serverFirstMessage = $json['authentication'];
226 14
        $authentication = [];
227 14
        foreach (explode(',', $json['authentication']) as $var) {
228 14
            $pair = explode('=', $var);
229 14
            $authentication[$pair[0]] = $pair[1];
230
        }
231 14
        $serverR = $authentication['r'];
232 14
        if (strpos($serverR, $this->myR) !== 0) {
233
            throw new Exception('Invalid nonce from server.');
234
        }
235 14
        $salt = base64_decode($authentication['s']);
236 14
        $iterations = (int)$authentication['i'];
237
238 14
        $clientFinalMessageWithoutProof = 'c=biws,r=' . $serverR;
239 14
        $saltedPassword = $this->pkbdf2Hmac($this->password, $salt, $iterations);
240 14
        $clientKey = hash_hmac('sha256', 'Client Key', $saltedPassword, true);
241 14
        $storedKey = hash('sha256', $clientKey, true);
242
243
        $authMessage =
244 14
            $this->clientFirstMessage . ',' . $serverFirstMessage . ',' . $clientFinalMessageWithoutProof;
245
246 14
        $clientSignature = hash_hmac('sha256', $authMessage, $storedKey, true);
247
248 14
        $clientProof = $clientKey ^ $clientSignature;
249
250 14
        $serverKey = hash_hmac('sha256', 'Server Key', $saltedPassword, true);
251
252 14
        $this->serverSignature = hash_hmac('sha256', $authMessage, $serverKey, true);
253
254 14
        $this->state = 3;
255
256
        return
257 14
            json_encode(
258
                [
259 14
                    'authentication' => $clientFinalMessageWithoutProof . ',p=' . base64_encode($clientProof),
260
                ]
261
            )
262 14
            . \chr(0);
263
    }
264
265
    /**
266
     * @param null|string $response
267
     * @return string
268
     * @throws Exception
269
     */
270 14
    private function verifyAuthentication(string $response): string
271
    {
272 14
        $json = json_decode($response, true);
273 14
        if ($json['success'] === false) {
274
            throw new Exception('Handshake failed: ' . $json['error']);
275
        }
276 14
        $authentication = [];
277 14
        foreach (explode(',', $json['authentication']) as $var) {
278 14
            $pair = explode('=', $var);
279 14
            $authentication[$pair[0]] = $pair[1];
280
        }
281
282 14
        $this->checkSignature(base64_decode($authentication['v']));
283
284 14
        $this->state = 4;
285
286 14
        return 'successful';
287
    }
288
289
    /**
290
     * @param null|string $handshakeResponse
291
     */
292 18
    private function checkResponse(?string $handshakeResponse)
293
    {
294 18
        if ($handshakeResponse !== null && preg_match('/^ERROR:\s(.+)$/i', $handshakeResponse,
295 18
                $errorMatch)) {
296 2
            throw new Exception($errorMatch[1]);
297
        }
298 18
    }
299
300
    /**
301
     * @param string $signature
302
     */
303 14
    private function checkSignature(string $signature)
304
    {
305 14
        if (!hash_equals($signature, $this->serverSignature)) {
306
            throw new Exception('Invalid server signature.');
307
        }
308 14
    }
309
}
310