Passed
Pull Request — master (#18)
by Timon
08:58
created

Handshake::nextMessage()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.0187

Importance

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