Skip to content

Commit

Permalink
Merge pull request #54 from zwirek/feature/scram-sha-256-authorization
Browse files Browse the repository at this point in the history
Support for scram-sha-256 authentication method
  • Loading branch information
mbonneau authored Aug 22, 2023
2 parents 11dcb5b + 2ca0b98 commit 8c0ce4c
Show file tree
Hide file tree
Showing 14 changed files with 421 additions and 9 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
image: postgres:${{ matrix.postgres }}
env:
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: --auth-host=md5
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
Expand All @@ -34,7 +35,7 @@ jobs:
fail-fast: false
matrix:
php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }}
postgres: [12, 13]
postgres: [12, 13, 14, 15]
composer: [lowest, locked, highest]
needs:
- supported-versions-matrix
Expand All @@ -46,6 +47,8 @@ jobs:
PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasync PASSWORD 'pgasync'"
PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER pgasyncpw"
PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasyncpw PASSWORD 'example_password'"
PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER scram_user"
PGPASSWORD=postgres psql -h localhost -U postgres -c "SET password_encryption='scram-sha-256';ALTER ROLE scram_user PASSWORD 'scram_password'"
PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE DATABASE pgasync_test OWNER pgasync"
PGPASSWORD=pgasync psql -h localhost -U pgasync -f tests/test_db.sql pgasync_test
# PGPASSWORD=postgres cat tests/test_db.sql | xargs -I % psql -h localhost -U postgres -c "%"
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@
"require-dev": {
"phpunit/phpunit": ">=8.5.23 || ^6.5.5",
"react/dns": "^1.0"
},
"scripts": {
"docker-up": "cd docker && docker-compose up -d",
"docker-down": "cd docker && docker-compose down"
}
}
6 changes: 6 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
environment:
- PGDATA=/database
- POSTGRES_PASSWORD=some_password
- POSTGRES_INITDB_ARGS=--auth-host=md5
- TZ=America/New_York
volumes:
- .:/app
Expand All @@ -15,3 +16,8 @@ services:
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
ports:
- "5432:5432"

configs:
pg_hba:
file: pg_hba_new.conf

2 changes: 2 additions & 0 deletions docker/docker-entrypoint-initdb.d/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ echo "Running as $USER in $PWD"

createuser -U postgres --createdb pgasync
createuser -U postgres --createdb pgasyncpw
createuser -U postgres --createdb scram_user
psql -U postgres -c "ALTER ROLE pgasyncpw PASSWORD 'example_password'"
psql -U postgres -c "SET password_encryption='scram-sha-256'; ALTER ROLE scram_user PASSWORD 'scram_password'"

cd /app
cp pg_hba_new.conf database/pg_hba.conf
Expand Down
41 changes: 41 additions & 0 deletions src/PgAsync/Command/SaslInitialResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);

namespace PgAsync\Command;

use PgAsync\ScramSha256;

class SaslInitialResponse implements CommandInterface
{
use CommandTrait;

const SCRAM_SHA_256 = "SCRAM-SHA-256";

/**
* @var ScramSha256
*/
private $scramSha265;

public function __construct(ScramSha256 $scramSha265)
{
$this->scramSha265 = $scramSha265;
}

public function encodedMessage(): string
{
$mechanism = self::SCRAM_SHA_256 . "\0";
$clientFirstMessage = $this->scramSha265->getClientFirstMessage();

$message = "p";
$messageLength = strlen($mechanism) + strlen($clientFirstMessage) + 8;
$message .= pack("N", $messageLength) . $mechanism;
$message .= pack("N", strlen($clientFirstMessage)) . $clientFirstMessage;

return $message;
}

public function shouldWaitForComplete(): bool
{
return false;
}
}
39 changes: 39 additions & 0 deletions src/PgAsync/Command/SaslResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);

namespace PgAsync\Command;

use PgAsync\ScramSha256;

class SaslResponse implements CommandInterface
{
use CommandTrait;

/**
* @var ScramSha256
*/
private $scramSha265;

public function __construct(ScramSha256 $scramSha265)
{
$this->scramSha265 = $scramSha265;
}

public function encodedMessage(): string
{
$clientFinalMessage = $this->createClientFinalMessage();
$messageLength = strlen($clientFinalMessage) + 4;

return 'p' . pack('N', $messageLength) . $clientFinalMessage;
}

public function shouldWaitForComplete(): bool
{
return false;
}

private function createClientFinalMessage(): string
{
return $this->scramSha265->getClientFirstMessageWithoutProof() . ',p=' . base64_encode($this->scramSha265->getClientProof());
}
}
32 changes: 31 additions & 1 deletion src/PgAsync/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use PgAsync\Command\Execute;
use PgAsync\Command\Parse;
use PgAsync\Command\PasswordMessage;
use PgAsync\Command\SaslInitialResponse;
use PgAsync\Command\SaslResponse;
use PgAsync\Command\Sync;
use PgAsync\Command\Terminate;
use PgAsync\Message\Authentication;
Expand Down Expand Up @@ -119,6 +121,9 @@ class Connection extends EventEmitter
/** @var bool */
private $cancelRequested;

/** @var ScramSha256 */
private $scramSha256;

/**
* Can be 'I' for Idle, 'T' if in transactions block
* or 'E' if in failed transaction block (queries will fail until end of trans)
Expand Down Expand Up @@ -174,6 +179,7 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt
$this->cancelRequested = false;

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

private function start()
Expand Down Expand Up @@ -268,7 +274,9 @@ private function processData($data)

$type = $data[0];

$message = Message::createMessageFromIdentifier($type);
$message = Message::createMessageFromIdentifier($type, [
'SCRAM_SHA_256' => $this->scramSha256
]);
if ($message !== false) {
$this->currentMessage = $message;
return $data;
Expand Down Expand Up @@ -388,6 +396,28 @@ private function handleAuthentication(Authentication $message)
return;
}

if ($message->getAuthCode() === $message::AUTH_SCRAM) {
$saslInitialResponse = new SaslInitialResponse($this->scramSha256);
$this->stream->write($saslInitialResponse->encodedMessage());

return;
}

if ($message->getAuthCode() === $message::AUTH_SCRAM_CONTINUE) {
$saslResponse = new SaslResponse($this->scramSha256);
$this->stream->write($saslResponse->encodedMessage());

return;
}

if ($message->getAuthCode() === $message::AUTH_SCRAM_FIN) {
if ($this->scramSha256->verify()) {
return;
}

$this->lastError = 'Invalid server signature sent by server on SCRAM FIN stage';
}

$this->connStatus = $this::CONNECTION_BAD;
$this->failAllCommandsWith(new \Exception($this->lastError));
$this->emit('error', [new \Exception($this->lastError)]);
Expand Down
40 changes: 40 additions & 0 deletions src/PgAsync/Message/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace PgAsync\Message;

use PgAsync\ScramSha256;

class Authentication extends Message
{
const AUTH_OK = 0; // AuthenticationOk
Expand All @@ -12,11 +14,26 @@ class Authentication extends Message
const AUTH_GSS = 7; // AuthenticationGSS
const AUTH_GSS_CONTINUE = 8; // AuthenticationGSSContinue
const AUTH_SSPI = 9; // AuthenticationSSPI
const AUTH_SCRAM = 10; // AuthenticationSASL
const AUTH_SCRAM_CONTINUE = 11; // AuthenticationSASLContinue
const AUTH_SCRAM_FIN = 12; // AuthenticationSASLFinal

private $authCode;

private $salt;

/** @var ScramSha256 */
private $scramSha256;

private $iteration;

private $nonce;

public function __construct(ScramSha256 $scramSha265)
{
$this->scramSha256 = $scramSha265;
}

/**
* @inheritDoc
* @throws \InvalidArgumentException
Expand Down Expand Up @@ -47,6 +64,23 @@ public function parseMessage(string $rawMessage)
break; // AuthenticationGSSContinue
case $this::AUTH_SSPI:
break; // AuthenticationSSPI
case $this::AUTH_SCRAM:
$this->scramSha256->beginFirstClientMessageStage();
break;
case $this::AUTH_SCRAM_CONTINUE:
$content = $this->getContent($rawMessage);
$parts = explode(',', $content);
$this->scramSha256->beginFinalClientMessageStage(
substr($parts[0], 2),
substr($parts[1], 2),
(int) substr($parts[2], 2)
);

break;
case $this::AUTH_SCRAM_FIN:
$content = $this->getContent($rawMessage);
$this->scramSha256->beginVerificationStage(substr($content, 2));
break;
}

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

return $this->salt;
}

private function getContent(string $rawMessage): string
{
$messageLength = unpack('N', substr($rawMessage, 1, 4))[1];
return substr($rawMessage, 9, $messageLength - 8);
}
}
4 changes: 2 additions & 2 deletions src/PgAsync/Message/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public static function prependLengthInt32(string $s): string
return Message::int32($len + 4) . $s;
}

public static function createMessageFromIdentifier(string $identifier): ParserInterface
public static function createMessageFromIdentifier(string $identifier, array $dependencies = []): ParserInterface
{
switch ($identifier) {
case 'R':
return new Authentication();
return new Authentication($dependencies['SCRAM_SHA_256']);
case 'K':
return new BackendKeyData();
case 'C':
Expand Down
Loading

0 comments on commit 8c0ce4c

Please sign in to comment.