Passed
Pull Request — master (#23)
by Marc
03:04
created

Handshake::createAuthenticationMessage()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 4.0065

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 25
cts 27
cp 0.9259
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 28
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
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 22
    public function __construct(string $username, string $password, int $version)
71
    {
72 22
        $this->username = $username;
73 22
        $this->password = $password;
74 22
        $this->state = 0;
75 22
        $this->version = $version;
76 22
    }
77
78
    /**
79
     * @inheritdoc
80
     * @throws \RuntimeException
81
     * @throws Exception
82
     */
83 20
    public function hello(StreamInterface $stream): void
84
    {
85
        try {
86 20
            $handshakeResponse = null;
87 20
            while (true) {
88 20
                if (!$stream->isWritable()) {
89 1
                    throw new Exception('Not connected');
90
                }
91
92 19
                $this->checkResponse($handshakeResponse);
93
94
                try {
95 19
                    $msg = $this->nextMessage($handshakeResponse);
96 2
                } catch (Exception $e) {
97 2
                    $stream->close();
98 2
                    throw $e;
99
                }
100
101 19
                if ($msg === 'successful') {
102 15
                    break;
103
                }
104
105 19
                if ($msg !== '') {
106 19
                    $stream->write($msg);
107
                }
108
109
                // Read null-terminated response
110 19
                $handshakeResponse = $stream->getContents();
111
            }
112 5
        } catch (Exception $e) {
113 5
            $stream->close();
114 5
            throw $e;
115
        }
116
117 15
    }
118
119
    /**
120
     * @param $response
121
     * @return string
122
     * @throws Exception
123
     */
124 19
    private function nextMessage(string $response = null): ?string
125
    {
126 19
        switch ($this->state) {
127 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...
128 19
                return $this->createHandshakeMessage($response);
129 17
            case 1:
130 17
                return $this->verifyProtocol($response);
131 15
            case 2:
132 15
                return $this->createAuthenticationMessage($response);
133 15
            case 3:
134 15
                return $this->verifyAuthentication($response);
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $response of TBolier\RethinkQL\Connec...:verifyAuthentication() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
                return $this->verifyAuthentication(/** @scrutinizer ignore-type */ $response);
Loading history...
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 15
    private function pkbdf2Hmac(string $password, string $salt, int $iterations): string
147
    {
148 15
        $t = hash_hmac('sha256', $salt . "\x00\x00\x00\x01", $password, true);
149 15
        $u = $t;
150 15
        for ($i = 0; $i < $iterations - 1; ++$i) {
151
            $t = hash_hmac('sha256', $t, $password, true);
152
            $u ^= $t;
153
        }
154
155 15
        return $u;
156
    }
157
158
    /**
159
     * @param null|string $response
160
     * @return string
161
     */
162 19
    private function createHandshakeMessage(?string $response): string
163
    {
164 19
        $response === null or die('Illegal handshake state');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
165
166 19
        $this->myR = base64_encode(openssl_random_pseudo_bytes(18));
167 19
        $this->clientFirstMessage = 'n=' . $this->username . ',r=' . $this->myR;
168
169 19
        $binaryVersion = pack('V', $this->version);
170
171 19
        $this->state = 1;
172
173
        return
174
            $binaryVersion
175 19
            . json_encode(
176
                [
177 19
                    'protocol_version' => $this->protocolVersion,
178 19
                    'authentication_method' => 'SCRAM-SHA-256',
179 19
                    'authentication' => 'n,,' . $this->clientFirstMessage,
180
                ]
181
            )
182 19
            . \chr(0);
183
    }
184
185
    /**
186
     * @param null|string $response
187
     * @return string
188
     * @throws Exception
189
     */
190 17
    private function verifyProtocol(?string $response): string
191
    {
192 17
        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 17
        $json = json_decode($response, true);
201 17
        if ($json['success'] === false) {
202 1
            throw new Exception('Handshake failed: ' . $json["error"]);
203
        }
204 16
        if ($this->protocolVersion > $json['max_protocol_version']
205 16
            || $this->protocolVersion < $json['min_protocol_version']) {
206 1
            throw new Exception('Unsupported protocol version.');
207
        }
208
209 15
        $this->state = 2;
210
211 15
        return '';
212
    }
213
214
    /**
215
     * @param $response
216
     * @return null|string
217
     * @throws Exception
218
     */
219 15
    private function createAuthenticationMessage($response): string
220
    {
221 15
        $json = json_decode($response, true);
222 15
        if ($json['success'] === false) {
223
            throw new Exception('Handshake failed: ' . $json['error']);
224
        }
225 15
        $serverFirstMessage = $json['authentication'];
226 15
        $authentication = [];
227 15
        foreach (explode(',', $json['authentication']) as $var) {
228 15
            $pair = explode('=', $var);
229 15
            $authentication[$pair[0]] = $pair[1];
230
        }
231 15
        $serverR = $authentication['r'];
232 15
        if (strpos($serverR, $this->myR) !== 0) {
233
            throw new Exception('Invalid nonce from server.');
234
        }
235 15
        $salt = base64_decode($authentication['s']);
236 15
        $iterations = (int)$authentication['i'];
237
238 15
        $clientFinalMessageWithoutProof = 'c=biws,r=' . $serverR;
239 15
        $saltedPassword = $this->pkbdf2Hmac($this->password, $salt, $iterations);
240 15
        $clientKey = hash_hmac('sha256', 'Client Key', $saltedPassword, true);
241 15
        $storedKey = hash('sha256', $clientKey, true);
242
243
        $authMessage =
244 15
            $this->clientFirstMessage . ',' . $serverFirstMessage . ',' . $clientFinalMessageWithoutProof;
245
246 15
        $clientSignature = hash_hmac('sha256', $authMessage, $storedKey, true);
247
248 15
        $clientProof = $clientKey ^ $clientSignature;
249
250 15
        $serverKey = hash_hmac('sha256', 'Server Key', $saltedPassword, true);
251
252 15
        $this->serverSignature = hash_hmac('sha256', $authMessage, $serverKey, true);
253
254 15
        $this->state = 3;
255
256
        return
257 15
            json_encode(
258
                [
259 15
                    'authentication' => $clientFinalMessageWithoutProof . ',p=' . base64_encode($clientProof),
260
                ]
261
            )
262 15
            . \chr(0);
263
    }
264
265
    /**
266
     * @param null|string $response
267
     * @return string
268
     * @throws Exception
269
     */
270 15
    private function verifyAuthentication(string $response): string
271
    {
272 15
        $json = json_decode($response, true);
273 15
        if ($json['success'] === false) {
274
            throw new Exception('Handshake failed: ' . $json['error']);
275
        }
276 15
        $authentication = [];
277 15
        foreach (explode(',', $json['authentication']) as $var) {
278 15
            $pair = explode('=', $var);
279 15
            $authentication[$pair[0]] = $pair[1];
280
        }
281
282 15
        $this->checkSignature(base64_decode($authentication['v']));
283
284 15
        $this->state = 4;
285
286 15
        return 'successful';
287
    }
288
289
    /**
290
     * @param null|string $handshakeResponse
291
     */
292 19
    private function checkResponse(?string $handshakeResponse): void
293
    {
294 19
        if ($handshakeResponse !== null && preg_match('/^ERROR:\s(.+)$/i', $handshakeResponse,
295 19
                $errorMatch)) {
296 2
            throw new Exception($errorMatch[1]);
297
        }
298 19
    }
299
300
    /**
301
     * @param string $signature
302
     * @throws Exception
303
     */
304 15
    private function checkSignature(string $signature): void
305
    {
306 15
        if (!hash_equals($signature, $this->serverSignature)) {
307
            throw new Exception('Invalid server signature.');
308
        }
309 15
    }
310
}
311