Passed
Pull Request — master (#18)
by Timon
02:57
created

Handshake::createAuthenticationMessage()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 45
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 4.0065

Importance

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