Passed
Pull Request — master (#34)
by Marc
05:44
created

Handshake::createAuthenticationMessage()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 4.0072

Importance

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