Passed
Pull Request — master (#18)
by Timon
11:18 queued 57s
created

Handshake::pkbdf2Hmac()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0932

Importance

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