Skip to content

Commit 1a8d1d3

Browse files
committed
RethinkDB 2.3.0 handshake and authentication support
1 parent 2740c72 commit 1a8d1d3

File tree

4 files changed

+191
-45
lines changed

4 files changed

+191
-45
lines changed

pb4php/ql2.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ message VersionDummy { // We need to wrap it like this for some
4848
V0_2 = 0x723081e1; // Authorization key during handshake
4949
V0_3 = 0x5f75e83e; // Authorization key and protocol during handshake
5050
V0_4 = 0x400c2d20; // Queries execute in parallel
51+
V1_0 = 0x34c2bdc3; // Users and permissions
5152
}
5253

5354
// The protocol to use after the handshake, specified in V0_3

rdb/Connection.php

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace r;
44

55
use r\DatumConverter;
6+
use r\Handshake;
67
use r\Queries\Dbs\Db;
78
use r\ProtocolBuffer\QueryQueryType;
89
use r\ProtocolBuffer\ResponseResponseType;
@@ -17,7 +18,8 @@ class Connection extends DatumConverter
1718
private $host;
1819
private $port;
1920
private $defaultDb;
20-
private $apiKey;
21+
private $user;
22+
private $password;
2123
private $activeTokens;
2224
private $timeout;
2325
private $ssl;
@@ -40,6 +42,8 @@ public function __construct(
4042
}
4143

4244
$ssl = null;
45+
$user = null;
46+
$password = null;
4347

4448
if (isset($opts)) {
4549
if (isset($opts['host'])) {
@@ -54,6 +58,12 @@ public function __construct(
5458
if (isset($opts['apiKey'])) {
5559
$apiKey = $opts['apiKey'];
5660
}
61+
if (isset($opts['user'])) {
62+
$user = $opts['user'];
63+
}
64+
if (isset($opts['password'])) {
65+
$password = $opts['password'];
66+
}
5767
if (isset($opts['timeout'])) {
5868
$timeout = $opts['timeout'];
5969
}
@@ -65,7 +75,25 @@ public function __construct(
6575
if (isset($apiKey) && !is_string($apiKey)) {
6676
throw new RqlDriverError("The API key must be a string.");
6777
}
78+
if (isset($user) && !is_string($user)) {
79+
throw new RqlDriverError("The user name must be a string.");
80+
}
81+
if (isset($password) && !is_string($password)) {
82+
throw new RqlDriverError("The password must be a string.");
83+
}
6884

85+
if (!isset($user)) {
86+
$user = "admin";
87+
}
88+
if (isset($apiKey) && isset($password)) {
89+
throw new RqlDriverError("Either user or apiKey can be specified, but not both.");
90+
}
91+
if (isset($apiKey) && !isset($password)) {
92+
$password = $apiKey;
93+
}
94+
if (!isset($password)) {
95+
$password = "";
96+
}
6997
if (!isset($host)) {
7098
$host = "localhost";
7199
}
@@ -75,10 +103,8 @@ public function __construct(
75103

76104
$this->host = $host;
77105
$this->port = $port;
78-
if (!isset($apiKey)) {
79-
$apiKey = "";
80-
}
81-
$this->apiKey = $apiKey;
106+
$this->user = $user;
107+
$this->password = $password;
82108
$this->timeout = null;
83109
$this->ssl = $ssl;
84110

@@ -436,51 +462,39 @@ private function connect()
436462
$this->applyTimeout($this->timeout);
437463
}
438464

439-
$this->sendHandshake();
440-
$this->receiveHandshakeResponse();
441-
}
442-
443-
private function sendHandshake()
444-
{
445-
if (!$this->isOpen()) {
446-
throw new RqlDriverError("Not connected");
447-
}
448-
449-
$binaryVersion = pack("V", VersionDummyVersion::PB_V0_3); // "V" is little endian, 32 bit unsigned integer
450-
$handshake = $binaryVersion;
451-
452-
$binaryKeyLength = pack("V", strlen($this->apiKey));
453-
$handshake .= $binaryKeyLength . $this->apiKey;
454-
455-
$binaryProtocol = pack("V", VersionDummyProtocol::PB_JSON);
456-
$handshake .= $binaryProtocol;
457-
458-
$this->sendStr($handshake);
459-
}
460-
461-
private function receiveHandshakeResponse()
462-
{
463-
if (!$this->isOpen()) {
464-
throw new RqlDriverError("Not connected");
465-
}
466-
467-
$response = "";
465+
$handshake = new Handshake($this->user, $this->password);
466+
$handshakeResponse = null;
468467
while (true) {
469-
$ch = stream_get_contents($this->socket, 1);
470-
if ($ch === false || strlen($ch) < 1) {
468+
if (!$this->isOpen()) {
469+
throw new RqlDriverError("Not connected");
470+
}
471+
try {
472+
$msg = $handshake->nextMessage($handshakeResponse);
473+
} catch (Exception $e) {
471474
$this->close(false);
472-
throw new RqlDriverError("Unable to read from socket during handshake. Disconnected.");
475+
throw $e;
473476
}
474-
if ($ch === chr(0)) {
477+
if ($msg === null) {
478+
// Handshake is complete
475479
break;
476-
} else {
477-
$response .= $ch;
478480
}
479-
}
480-
481-
if ($response != "SUCCESS") {
482-
$this->close(false);
483-
throw new RqlDriverError("Handshake failed: $response Disconnected.");
481+
if ($msg != "") {
482+
$this->sendStr($msg);
483+
}
484+
// Read null-terminated response
485+
$handshakeResponse = "";
486+
while (true) {
487+
$ch = stream_get_contents($this->socket, 1);
488+
if ($ch === false || strlen($ch) < 1) {
489+
$this->close(false);
490+
throw new RqlDriverError("Unable to read from socket during handshake. Disconnected.");
491+
}
492+
if ($ch === chr(0)) {
493+
break;
494+
} else {
495+
$handshakeResponse .= $ch;
496+
}
497+
}
484498
}
485499
}
486500

rdb/Handshake.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace r;
4+
5+
use r\ProtocolBuffer\VersionDummyVersion;
6+
use r\Exceptions\RqlServerError;
7+
use r\Exceptions\RqlDriverError;
8+
9+
class Handshake
10+
{
11+
private $username;
12+
private $password;
13+
private $protocol_version = 0;
14+
private $state;
15+
private $myR;
16+
private $clientFirstMessage;
17+
private $serverSignature;
18+
19+
public function __construct($username,$password) {
20+
$this->username = str_replace(",", "=2C", str_replace("=", "=3D", $username));
21+
$this->password = $password;
22+
$this->state = 0;
23+
}
24+
25+
public function nextMessage($response) {
26+
if ($this->state == 0) {
27+
$response == null or die("Illegal handshake state");
28+
29+
$this->myR = base64_encode(openssl_random_pseudo_bytes(18));
30+
$this->clientFirstMessage = "n=" . $this->username . ",r=" . $this->myR;
31+
32+
$binaryVersion = pack("V", VersionDummyVersion::PB_V1_0); // "V" is little endian, 32 bit unsigned integer
33+
34+
$this->state = 1;
35+
return
36+
$binaryVersion
37+
. json_encode(array("protocol_version" => $this->protocol_version,
38+
"authentication_method" => "SCRAM-SHA-256",
39+
"authentication" => "n,," . $this->clientFirstMessage))
40+
. chr(0);
41+
} else if ($this->state == 1) {
42+
if (strpos($response, "ERROR") === 0) {
43+
throw new RqlDriverError("Received an unexpected reply. You may be attempting to connect to "
44+
. "a RethinkDB server that is too old for this driver. The minimum "
45+
. "supported server version is 2.3.0.");
46+
}
47+
48+
$json = json_decode($response, true);
49+
if ($json["success"] === false) {
50+
throw new RqlDriverError("Handshake failed: " . $json["error"]);
51+
}
52+
if ($this->protocol_version > $json["max_protocol_version"]
53+
|| $this->protocol_version < $json["min_protocol_version"]) {
54+
throw new RqlDriverError("Unsupported protocol version.");
55+
}
56+
57+
$this->state = 2;
58+
return "";
59+
} else if ($this->state == 2) {
60+
$json = json_decode($response, true);
61+
if ($json["success"] === false) {
62+
throw new RqlDriverError("Handshake failed: " . $json["error"]);
63+
}
64+
$serverFirstMessage = $json["authentication"];
65+
$authentication = array();
66+
foreach (explode(",", $json["authentication"]) as $var) {
67+
$pair = explode("=", $var);
68+
$authentication[$pair[0]] = $pair[1];
69+
}
70+
$serverR = $authentication["r"];
71+
if (strpos($serverR, $this->myR) !== 0) {
72+
throw new RqlDriverError("Invalid nonce from server.");
73+
}
74+
$salt = base64_decode($authentication["s"]);
75+
$iterations = (int)$authentication["i"];
76+
77+
$clientFinalMessageWithoutProof = "c=biws,r=" . $serverR;
78+
$saltedPassword = $this->pkbdf2Hmac($this->password, $salt, $iterations);
79+
$clientKey = hash_hmac("sha256", "Client Key", $saltedPassword, true);
80+
$storedKey = hash("sha256", $clientKey, true);
81+
82+
$authMessage = $this->clientFirstMessage . "," . $serverFirstMessage . "," . $clientFinalMessageWithoutProof;
83+
84+
$clientSignature = hash_hmac("sha256", $authMessage, $storedKey, true);
85+
86+
$clientProof = $clientKey ^ $clientSignature;
87+
88+
$serverKey = hash_hmac("sha256", "Server Key", $saltedPassword, true);
89+
90+
$this->serverSignature = hash_hmac("sha256", $authMessage, $serverKey, true);
91+
92+
$this->state = 3;
93+
return
94+
json_encode(array("authentication" => $clientFinalMessageWithoutProof . ",p=" . base64_encode($clientProof)))
95+
. chr(0);
96+
} else if ($this->state == 3) {
97+
$json = json_decode($response, true);
98+
if ($json["success"] === false) {
99+
throw new RqlDriverError("Handshake failed: " . $json["error"]);
100+
}
101+
$authentication = array();
102+
foreach (explode(",", $json["authentication"]) as $var) {
103+
$pair = explode("=", $var);
104+
$authentication[$pair[0]] = $pair[1];
105+
}
106+
107+
$v = base64_decode($authentication["v"]);
108+
109+
// TODO: Use cryptographic comparison
110+
if ($v != $this->serverSignature) {
111+
throw new RqlDriverError("Invalid server signature.");
112+
}
113+
114+
$this->state = 4;
115+
return null;
116+
} else {
117+
die("Illegal handshake state");
118+
}
119+
}
120+
121+
private function pkbdf2Hmac($password, $salt, $iterations) {
122+
$t = hash_hmac("sha256", $salt . "\x00\x00\x00\x01", $password, true);
123+
$u = $t;
124+
for ($i = 0; $i < $iterations - 1; ++$i) {
125+
$t = hash_hmac("sha256", $t, $password, true);
126+
$u = $u ^ $t;
127+
}
128+
return $u;
129+
}
130+
}

rdb/ProtocolBuffer/VersionDummyVersion.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ class VersionDummyVersion
88
const PB_V0_2 = 0x723081e1;
99
const PB_V0_3 = 0x5f75e83e;
1010
const PB_V0_4 = 0x400c2d20;
11+
const PB_V1_0 = 0x34c2bdc3;
1112
}

0 commit comments

Comments
 (0)