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