Skip to content

Commit 7889cef

Browse files
committed
Support for SCRAM_SHA_256
1 parent 696b766 commit 7889cef

File tree

11 files changed

+422
-5
lines changed

11 files changed

+422
-5
lines changed

composer.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,9 @@
4242
"require-dev": {
4343
"phpunit/phpunit": ">=8.5.23 || ^6.5.5",
4444
"react/dns": "^1.0"
45+
},
46+
"scripts": {
47+
"docker-up": "cd docker && docker-compose up -d",
48+
"docker-down": "cd docker && docker-compose down"
4549
}
4650
}

docker/docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,16 @@ services:
1515
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
1616
ports:
1717
- "5432:5432"
18+
19+
pgasync-postgres-15:
20+
container_name: pgasync-postgres-15
21+
image: postgres:15
22+
environment:
23+
- PGDATA=/database
24+
- POSTGRES_USER=sampleuser
25+
- POSTGRES_PASSWORD=some_password
26+
- TZ=America/New_York
27+
volumes:
28+
- .:/app
29+
ports:
30+
- "5415:5432"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace PgAsync\Command;
5+
6+
use PgAsync\ScramSha256;
7+
8+
class SaslInitialResponse implements CommandInterface
9+
{
10+
use CommandTrait;
11+
12+
const SCRAM_SHA_256 = "SCRAM-SHA-256";
13+
14+
/**
15+
* @var ScramSha256
16+
*/
17+
private $scramSha265;
18+
19+
public function __construct(ScramSha256 $scramSha265)
20+
{
21+
$this->scramSha265 = $scramSha265;
22+
}
23+
24+
public function encodedMessage(): string
25+
{
26+
$mechanism = self::SCRAM_SHA_256 . "\0";
27+
$clientFirstMessage = $this->scramSha265->getClientFirstMessage();
28+
29+
$message = "p";
30+
$messageLength = strlen($mechanism) + strlen($clientFirstMessage) + 8;
31+
$message .= pack("N", $messageLength) . $mechanism;
32+
$message .= pack("N", strlen($clientFirstMessage)) . $clientFirstMessage;
33+
34+
return $message;
35+
}
36+
37+
public function shouldWaitForComplete(): bool
38+
{
39+
return false;
40+
}
41+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace PgAsync\Command;
5+
6+
use PgAsync\ScramSha256;
7+
8+
class SaslResponse implements CommandInterface
9+
{
10+
use CommandTrait;
11+
12+
/**
13+
* @var ScramSha256
14+
*/
15+
private $scramSha265;
16+
17+
public function __construct(ScramSha256 $scramSha265)
18+
{
19+
$this->scramSha265 = $scramSha265;
20+
}
21+
22+
public function encodedMessage(): string
23+
{
24+
$clientFinalMessage = $this->createClientFinalMessage();
25+
$messageLength = strlen($clientFinalMessage) + 4;
26+
27+
return 'p' . pack('N', $messageLength) . $clientFinalMessage;
28+
}
29+
30+
public function shouldWaitForComplete(): bool
31+
{
32+
return false;
33+
}
34+
35+
private function createClientFinalMessage(): string
36+
{
37+
return $this->scramSha265->getClientFirstMessageWithoutProof() . ',p=' . base64_encode($this->scramSha265->getClientProof());
38+
}
39+
}

src/PgAsync/Connection.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use PgAsync\Command\Execute;
1111
use PgAsync\Command\Parse;
1212
use PgAsync\Command\PasswordMessage;
13+
use PgAsync\Command\SaslInitialResponse;
14+
use PgAsync\Command\SaslResponse;
1315
use PgAsync\Command\Sync;
1416
use PgAsync\Command\Terminate;
1517
use PgAsync\Message\Authentication;
@@ -119,6 +121,9 @@ class Connection extends EventEmitter
119121
/** @var bool */
120122
private $cancelRequested;
121123

124+
/** @var ScramSha256 */
125+
private $scramSha256;
126+
122127
/**
123128
* Can be 'I' for Idle, 'T' if in transactions block
124129
* or 'E' if in failed transaction block (queries will fail until end of trans)
@@ -174,6 +179,7 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt
174179
$this->cancelRequested = false;
175180

176181
$this->parameters = $parameters;
182+
$this->scramSha256 = new ScramSha256($parameters['user'], $this->password ?: '');
177183
}
178184

179185
private function start()
@@ -268,7 +274,9 @@ private function processData($data)
268274

269275
$type = $data[0];
270276

271-
$message = Message::createMessageFromIdentifier($type);
277+
$message = Message::createMessageFromIdentifier($type, [
278+
'SCRAM_SHA_256' => $this->scramSha256
279+
]);
272280
if ($message !== false) {
273281
$this->currentMessage = $message;
274282
return $data;
@@ -388,6 +396,28 @@ private function handleAuthentication(Authentication $message)
388396
return;
389397
}
390398

399+
if ($message->getAuthCode() === $message::AUTH_SCRAM) {
400+
$saslInitialResponse = new SaslInitialResponse($this->scramSha256);
401+
$this->stream->write($saslInitialResponse->encodedMessage());
402+
403+
return;
404+
}
405+
406+
if ($message->getAuthCode() === $message::AUTH_SCRAM_CONTINUE) {
407+
$saslResponse = new SaslResponse($this->scramSha256);
408+
$this->stream->write($saslResponse->encodedMessage());
409+
410+
return;
411+
}
412+
413+
if ($message->getAuthCode() === $message::AUTH_SCRAM_FIN) {
414+
if ($this->scramSha256->verify()) {
415+
return;
416+
}
417+
418+
$this->lastError = 'Invalid server signature sent by server on SCRAM FIN stage';
419+
}
420+
391421
$this->connStatus = $this::CONNECTION_BAD;
392422
$this->failAllCommandsWith(new \Exception($this->lastError));
393423
$this->emit('error', [new \Exception($this->lastError)]);

src/PgAsync/Message/Authentication.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PgAsync\Message;
44

5+
use PgAsync\ScramSha256;
6+
57
class Authentication extends Message
68
{
79
const AUTH_OK = 0; // AuthenticationOk
@@ -12,11 +14,26 @@ class Authentication extends Message
1214
const AUTH_GSS = 7; // AuthenticationGSS
1315
const AUTH_GSS_CONTINUE = 8; // AuthenticationGSSContinue
1416
const AUTH_SSPI = 9; // AuthenticationSSPI
17+
const AUTH_SCRAM = 10; // AuthenticationSASL
18+
const AUTH_SCRAM_CONTINUE = 11; // AuthenticationSASLContinue
19+
const AUTH_SCRAM_FIN = 12; // AuthenticationSASLFinal
1520

1621
private $authCode;
1722

1823
private $salt;
1924

25+
/** @var ScramSha256 */
26+
private $scramSha256;
27+
28+
private $iteration;
29+
30+
private $nonce;
31+
32+
public function __construct(ScramSha256 $scramSha265)
33+
{
34+
$this->scramSha256 = $scramSha265;
35+
}
36+
2037
/**
2138
* @inheritDoc
2239
* @throws \InvalidArgumentException
@@ -47,6 +64,23 @@ public function parseMessage(string $rawMessage)
4764
break; // AuthenticationGSSContinue
4865
case $this::AUTH_SSPI:
4966
break; // AuthenticationSSPI
67+
case $this::AUTH_SCRAM:
68+
$this->scramSha256->beginFirstClientMessageStage();
69+
break;
70+
case $this::AUTH_SCRAM_CONTINUE:
71+
$content = $this->getContent($rawMessage);
72+
$parts = explode(',', $content);
73+
$this->scramSha256->beginFinalClientMessageStage(
74+
substr($parts[0], 2),
75+
substr($parts[1], 2),
76+
(int) substr($parts[2], 2)
77+
);
78+
79+
break;
80+
case $this::AUTH_SCRAM_FIN:
81+
$content = $this->getContent($rawMessage);
82+
$this->scramSha256->beginVerificationStage(substr($content, 2));
83+
break;
5084
}
5185

5286
$this->authCode = $authCode;
@@ -70,4 +104,10 @@ public function getSalt(): string
70104

71105
return $this->salt;
72106
}
107+
108+
private function getContent(string $rawMessage): string
109+
{
110+
$messageLength = unpack('N', substr($rawMessage, 1, 4))[1];
111+
return substr($rawMessage, 9, $messageLength - 8);
112+
}
73113
}

src/PgAsync/Message/Message.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ public static function prependLengthInt32(string $s): string
2323
return Message::int32($len + 4) . $s;
2424
}
2525

26-
public static function createMessageFromIdentifier(string $identifier): ParserInterface
26+
public static function createMessageFromIdentifier(string $identifier, array $dependencies): ParserInterface
2727
{
2828
switch ($identifier) {
2929
case 'R':
30-
return new Authentication();
30+
return new Authentication($dependencies['SCRAM_SHA_256']);
3131
case 'K':
3232
return new BackendKeyData();
3333
case 'C':

0 commit comments

Comments
 (0)