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

Handshake::verifyAuthentication()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.0582

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 11
cts 13
cp 0.8462
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 13
nc 5
nop 1
crap 4.0582
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
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

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