Passed
Pull Request — master (#15)
by Marc
04:02
created

Handshake::verifyAuthentication()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 1
dl 0
loc 18
ccs 10
cts 11
cp 0.9091
crap 3.0067
rs 9.4285
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 32
    public function __construct(string $username, string $password, int $version)
71
    {
72 32
        $this->username = $username;
73 32
        $this->password = $password;
74 32
        $this->state = 0;
75 32
        $this->version = $version;
76 32
    }
77
78
    /**
79
     * @inheritdoc
80
     * @throws \RuntimeException
81
     * @throws Exception
82
     */
83 26
    public function hello(StreamInterface $stream): void
84
    {
85
        try {
86 26
            $handshakeResponse = null;
87 26
            while (true) {
88 26
                if (!$stream->isWritable()) {
89 1
                    throw new Exception('Not connected');
90
                }
91
92 25
                $this->checkResponse($handshakeResponse);
93
94
                try {
95 25
                    $msg = $this->nextMessage($handshakeResponse);
96 2
                } catch (Exception $e) {
97 2
                    $stream->close();
98 2
                    throw $e;
99
                }
100
101 25
                if ($msg === 'successful') {
102 21
                    break;
103
                }
104
105 25
                if ($msg !== '') {
106 25
                    $stream->write($msg);
107
                }
108
109
                // Read null-terminated response
110 25
                $handshakeResponse = $stream->getContents();
111
            }
112 5
        } catch (Exception $e) {
113 5
            $stream->close();
114 5
            throw $e;
115
        }
116
117 21
    }
118
119
    /**
120
     * @param $response
121
     * @return string
122
     * @throws Exception
123
     */
124 25
    private function nextMessage(string $response = null): ?string
125
    {
126 25
        switch ($this->state) {
127 25
            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 25
                return $this->createHandshakeMessage($response);
129 23
            case 1:
130 23
                return $this->verifyProtocol($response);
131 21
            case 2:
132 21
                return $this->createAuthenticationMessage($response);
133 21
            case 3:
134 21
                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 21
    private function pkbdf2Hmac(string $password, string $salt, int $iterations): string
147
    {
148 21
        $t = hash_hmac('sha256', $salt . "\x00\x00\x00\x01", $password, true);
149 21
        $u = $t;
150 21
        for ($i = 0; $i < $iterations - 1; ++$i) {
151
            $t = hash_hmac('sha256', $t, $password, true);
152
            $u ^= $t;
153
        }
154
155 21
        return $u;
156
    }
157
158
    /**
159
     * @param null|string $response
160
     * @return string
161
     */
162 25
    private function createHandshakeMessage(?string $response): string
163
    {
164 25
        $response === null or die('Illegal handshake state');
165
166 25
        $this->myR = base64_encode(openssl_random_pseudo_bytes(18));
167 25
        $this->clientFirstMessage = 'n=' . $this->username . ',r=' . $this->myR;
168
169 25
        $binaryVersion = pack('V', $this->version);
170
171 25
        $this->state = 1;
172
173
        return
174
            $binaryVersion
175 25
            . json_encode(
176
                [
177 25
                    'protocol_version' => $this->protocolVersion,
178 25
                    'authentication_method' => 'SCRAM-SHA-256',
179 25
                    'authentication' => 'n,,' . $this->clientFirstMessage,
180
                ]
181
            )
182 25
            . \chr(0);
183
    }
184
185
    /**
186
     * @param null|string $response
187
     * @return string
188
     * @throws Exception
189
     */
190 23
    private function verifyProtocol(?string $response): string
191
    {
192 23
        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 23
        $json = json_decode($response, true);
201 23
        if ($json['success'] === false) {
202 1
            throw new Exception('Handshake failed: ' . $json["error"]);
203
        }
204 22
        if ($this->protocolVersion > $json['max_protocol_version']
205 22
            || $this->protocolVersion < $json['min_protocol_version']) {
206 1
            throw new Exception('Unsupported protocol version.');
207
        }
208
209 21
        $this->state = 2;
210
211 21
        return '';
212
    }
213
214
    /**
215
     * @param $response
216
     * @return null|string string
217
     * @throws Exception
218
     */
219 21
    private function createAuthenticationMessage($response): string
220
    {
221 21
        $json = json_decode($response, true);
222 21
        if ($json['success'] === false) {
223
            throw new Exception('Handshake failed: ' . $json['error']);
224
        }
225 21
        $serverFirstMessage = $json['authentication'];
226 21
        $authentication = [];
227 21
        foreach (explode(',', $json['authentication']) as $var) {
228 21
            $pair = explode('=', $var);
229 21
            $authentication[$pair[0]] = $pair[1];
230
        }
231 21
        $serverR = $authentication['r'];
232 21
        if (strpos($serverR, $this->myR) !== 0) {
233
            throw new Exception('Invalid nonce from server.');
234
        }
235 21
        $salt = base64_decode($authentication['s']);
236 21
        $iterations = (int)$authentication['i'];
237
238 21
        $clientFinalMessageWithoutProof = 'c=biws,r=' . $serverR;
239 21
        $saltedPassword = $this->pkbdf2Hmac($this->password, $salt, $iterations);
240 21
        $clientKey = hash_hmac('sha256', 'Client Key', $saltedPassword, true);
241 21
        $storedKey = hash('sha256', $clientKey, true);
242
243
        $authMessage =
244 21
            $this->clientFirstMessage . ',' . $serverFirstMessage . ',' . $clientFinalMessageWithoutProof;
245
246 21
        $clientSignature = hash_hmac('sha256', $authMessage, $storedKey, true);
247
248 21
        $clientProof = $clientKey ^ $clientSignature;
249
250 21
        $serverKey = hash_hmac('sha256', 'Server Key', $saltedPassword, true);
251
252 21
        $this->serverSignature = hash_hmac('sha256', $authMessage, $serverKey, true);
253
254 21
        $this->state = 3;
255
256
        return
257 21
            json_encode(
258
                [
259 21
                    'authentication' => $clientFinalMessageWithoutProof . ',p=' . base64_encode($clientProof),
260
                ]
261
            )
262 21
            . \chr(0);
263
    }
264
265
    /**
266
     * @param null|string $response
267
     * @return string
268
     * @throws Exception
269
     */
270 21
    private function verifyAuthentication(string $response): string
271
    {
272 21
        $json = json_decode($response, true);
273 21
        if ($json['success'] === false) {
274
            throw new Exception('Handshake failed: ' . $json['error']);
275
        }
276 21
        $authentication = [];
277 21
        foreach (explode(',', $json['authentication']) as $var) {
278 21
            $pair = explode('=', $var);
279 21
            $authentication[$pair[0]] = $pair[1];
280
        }
281
282 21
        $this->checkSignature(base64_decode($authentication['v']));
283
284 21
        $this->state = 4;
285
286 21
        return 'successful';
287
    }
288
289
    /**
290
     * @param null|string $handshakeResponse
291
     */
292 25
    private function checkResponse(?string $handshakeResponse)
293
    {
294 25
        if ($handshakeResponse !== null && preg_match('/^ERROR:\s(.+)$/i', $handshakeResponse,
295 25
                $errorMatch)) {
296 2
            throw new Exception($errorMatch[1]);
297
        }
298 25
    }
299
300
    /**
301
     * @param string $signature
302
     */
303 21
    private function checkSignature(string $signature)
304
    {
305 21
        if (!hash_equals($signature, $this->serverSignature)) {
306
            throw new Exception('Invalid server signature.');
307
        }
308 21
    }
309
}
310