From 49e3f5c206eac73d55ecd74e09d5820c7846950f Mon Sep 17 00:00:00 2001 From: wuxi Date: Thu, 24 Aug 2023 11:38:15 +0800 Subject: [PATCH 01/11] Add support SCRAM-SHA-512 --- CHANGELOG.md | 20 ++++ src/Client/SyncClient.php | 56 ++++++++- src/Sasl/ScramSha512Sasl.php | 225 +++++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/Sasl/ScramSha512Sasl.php diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4d93228 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +本仓库是在 [longlang/kafka](https://github.com/swoole/phpkafka) 的基础上增加了 `SCRAM-SHA-512` 加密方式的连接。 + +使用时 `sasl` 配置为 +```php +... +'sasl' => [ + 'type' => \longlang\phpkafka\Sasl\ScramSha512Sasl::class, + 'username' => env('KAFKA_SASL_USERNAME', ''), + 'password' => env('KAFKA_SASL_PASSWORD', ''), + // 是否验证第二次握手的服务器响应消息的签名 + 'verify_final_signature' => (bool) env('KAFKA_SASL_VERIFY_FINAL_SIGNATURE', false), +], +... +``` + + +# Changed Log +## [v1.2.3] - 2023-08-24 +### Added + - 增加基于 `SCRAM-SHA-512` 加密方式的连接; \ No newline at end of file diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index 2cb0d8e..5dfb7a9 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -23,7 +23,9 @@ use longlang\phpkafka\Protocol\SaslHandshake\SaslHandshakeRequest; use longlang\phpkafka\Protocol\SaslHandshake\SaslHandshakeResponse; use longlang\phpkafka\Protocol\Type\Int32; +use longlang\phpkafka\Sasl\PlainSasl; use longlang\phpkafka\Sasl\SaslInterface; +use longlang\phpkafka\Sasl\ScramSha512Sasl; use longlang\phpkafka\Socket\SocketInterface; use longlang\phpkafka\Socket\StreamSocket; @@ -200,13 +202,49 @@ protected function updateApiVersions(): void protected function sendAuthInfo(): void { $config = $this->getConfig()->getSasl(); - if (!isset($config['type']) || empty($config['type'])) { + if (! isset($config['type']) || empty($config['type'])) { return; } $class = new $config['type']($this->getConfig()); - if (!$class instanceof SaslInterface) { + if (! $class instanceof SaslInterface) { return; } + + if ($class instanceof PlainSasl) { + $this->sendPlainAuthInfo($class); + } + elseif ($class instanceof ScramSha512Sasl) { + $this->sendScramSha512AuthInfo($class); + } + else { + return; + } + } + + private function sendPlainAuthInfo(SaslInterface $class): void + { + /** @var \longlang\phpkafka\Sasl\PlainSasl $class */ + + $handshakeRequest = new SaslHandshakeRequest(); + $handshakeRequest->setMechanism($class->getName()); + $correlationId = $this->send($handshakeRequest); + /** @var SaslHandshakeResponse $handshakeResponse */ + $handshakeResponse = $this->recv($correlationId); + ErrorCode::check($handshakeResponse->getErrorCode()); + + $authenticateRequest = new SaslAuthenticateRequest(); + $authenticateRequest->setAuthBytes($class->getAuthBytes()); + $correlationId = $this->send($authenticateRequest); + /** @var SaslAuthenticateResponse $authenticateResponse */ + $authenticateResponse = $this->recv($correlationId); + ErrorCode::check($authenticateResponse->getErrorCode()); + } + + private function sendScramSha512AuthInfo(SaslInterface $class): void + { + /** @var \longlang\phpkafka\Sasl\ScramSha512Sasl $class */ + + // 发送第一次验证信息 $handshakeRequest = new SaslHandshakeRequest(); $handshakeRequest->setMechanism($class->getName()); $correlationId = $this->send($handshakeRequest); @@ -214,11 +252,25 @@ protected function sendAuthInfo(): void $handshakeResponse = $this->recv($correlationId); ErrorCode::check($handshakeResponse->getErrorCode()); + // 第一次握手 $authenticateRequest = new SaslAuthenticateRequest(); $authenticateRequest->setAuthBytes($class->getAuthBytes()); $correlationId = $this->send($authenticateRequest); /** @var SaslAuthenticateResponse $authenticateResponse */ $authenticateResponse = $this->recv($correlationId); ErrorCode::check($authenticateResponse->getErrorCode()); + + // 第二次握手 + $authenticateRequest = new SaslAuthenticateRequest(); + $authenticateRequest->setAuthBytes($class->getFinalMessage($authenticateResponse->getAuthBytes())); + $correlationId = $this->send($authenticateRequest); + /** @var SaslAuthenticateResponse $authenticateResponse */ + $authenticateResponse = $this->recv($correlationId); + ErrorCode::check($authenticateResponse->getErrorCode()); + + // 校验第二次服务器响应消息 + if ($class->enableFinalSignatureVerification()) { + $class->verifyFinalMessage($authenticateResponse->getAuthBytes()); + } } } diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php new file mode 100644 index 0000000..a63c521 --- /dev/null +++ b/src/Sasl/ScramSha512Sasl.php @@ -0,0 +1,225 @@ +config = $config; + $this->nonce = base64_encode(random_bytes(16)); + } + + /** + * 授权模式. + */ + public function getName(): string + { + return 'SCRAM-SHA-512'; + } + + /** + * SCRAM-SHA-512 第一次握手信息 + * + * @return string + */ + public function getAuthBytes(): string + { + $config = $this->config->getSasl(); + if (empty($config['username']) || empty($config['password'])) { + throw new KafkaErrorException('sasl not found auth info'); + } + + return sprintf('n,,%s', $this->getFirstMessageBare()); + } + + /** + * 获取第一次握手信息 + * + * @return string + */ + private function getFirstMessageBare(): string + { + return sprintf('n=%s,r=%s', $this->getSaslConfig('username'), $this->nonce); + } + + /** + * 获取 SASL 所有配置 + * + * @return array + */ + public function getSaslConfigs(): array + { + return $this->config->getSasl(); + } + + /** + * 获取 SASL 配置 + * + * @param string $key + * @return mixed + */ + public function getSaslConfig(string $key): mixed + { + return $this->getSaslConfigs()[$key] ?? null; + } + + /** + * 获取 SASL 密码 + * + * @return string + */ + private function getPassword(): string + { + return $this->getSaslConfig('password') ?: ''; + } + + /** + * 计算第二次握手信息 + * + * @param string $response + * @return string + */ + public function getFinalMessage(string $response): string + { + // 拆分第一次握手后的响应 + [$r, $s, $i] = explode(',', $response); + + // 提取随机数、盐和迭代次数 + $serverNonce = $this->ltrimMessage($r); + $salt = base64_decode($this->ltrimMessage($s)); + $iterations = (int) $this->ltrimMessage($i); + + // 计算第二次握手的参数 + $this->saltedPassword = $saltedPassword = $this->calculateSaltedPassword($this->getPassword(), $salt, $iterations); + + $clientKey = $this->calculateClientKey($saltedPassword); + $storedKey = $this->calculateStoredKey($clientKey); + + $clientFirstMessageBare = $this->getFirstMessageBare(); + $serverFirstMessage = $response; + $clientFinalMessageWithoutProof = $this->getMessageWithoutProof($serverNonce); + + $this->authMessage = $authMessage = sprintf('%s,%s,%s', $clientFirstMessageBare, $serverFirstMessage, $clientFinalMessageWithoutProof); + $clientSignature = $this->hmac($authMessage, $storedKey); + + return sprintf('%s,p=%s', $clientFinalMessageWithoutProof, base64_encode($clientKey ^ $clientSignature)); + } + + /** + * 计算盐化密码 + * 使用 PBKDF2 函数和服务器提供的盐和迭代次数来计算盐化密码 + * + * @param string $password + * @param string $salt + * @param integer $iterations + * @return string + */ + private function calculateSaltedPassword(string $password, string $salt, int $iterations): string + { + return hash_pbkdf2('sha512', $password, $salt, $iterations, 0, true); + } + + /** + * 计算客户端密钥 + * 使用盐化密码和 HMAC 函数来计算客户端密钥 + * + * @param string $saltedPassword + * @return string + */ + private function calculateClientKey(string $saltedPassword): string + { + // 在 SCRAM-SHA-512 中需要用盐化密码来加密计算密,密钥钥固定是 Client Key + return $this->hmac('Client Key', $saltedPassword); + } + + /** + * 计算存储密钥 + * 使用客户端密钥和 SHA-256 函数来计算存储密钥 + * + * @param string $clientKey + * @return string + */ + private function calculateStoredKey(string $clientKey): string + { + return hash('sha512', $clientKey, true); + } + + /** + * 获取不带证明的消息 + * + * @param string $nonce + * @return string + */ + private function getMessageWithoutProof(string $nonce): string + { + return sprintf('c=biws,r=%s', $nonce); + } + + /** + * sha512 加密 + * + * @param string $data + * @param string $key + * @return string + */ + public function hmac(string $data, string $key): string + { + return hash_hmac('sha512', $data, $key, true); + } + + /** + * 删除服务响应信息的前两个字符 + * + * @param string $param + * @return string + */ + public function ltrimMessage(string $param): string + { + return substr($param, 2); + } + + /** + * 是否启用最终签名验证 + * + * @return boolean + */ + public function enableFinalSignatureVerification(): bool + { + return (bool) $this->getSaslConfig('verify_final_signature'); + } + + /** + * 验证最终签名 + * + * @param string $message + * @return void + */ + public function verifyFinalMessage(string $message): void + { + $receivedSignature = $this->ltrimMessage($message); + $receivedSignature = base64_decode($receivedSignature); + + $serverKey = $this->hmac('Server Key', $this->saltedPassword); + $expectedSignature = $this->hmac($this->authMessage, $serverKey); + + if (! hash_equals($receivedSignature, $expectedSignature)) { + ErrorCode::check(ErrorCode::SASL_AUTHENTICATION_FAILED); + } + } +} From 89ce747940e0a1c65c18520a98202cfbcb2a8622 Mon Sep 17 00:00:00 2001 From: wuxi Date: Thu, 24 Aug 2023 14:01:44 +0800 Subject: [PATCH 02/11] Fixed error --- src/Sasl/ScramSha512Sasl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php index a63c521..5197ac6 100644 --- a/src/Sasl/ScramSha512Sasl.php +++ b/src/Sasl/ScramSha512Sasl.php @@ -5,6 +5,7 @@ namespace longlang\phpkafka\Sasl; use longlang\phpkafka\Config\CommonConfig; +use longlang\phpkafka\Exception\KafkaErrorException; use longlang\phpkafka\Protocol\ErrorCode; use longlang\phpkafka\Sasl\SaslInterface; From 9543e813b58ea799bf734c5dd3d6b1247c358218 Mon Sep 17 00:00:00 2001 From: wuxi Date: Mon, 28 Aug 2023 16:03:00 +0800 Subject: [PATCH 03/11] Fixed first heartbeat --- src/Consumer/Consumer.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Consumer/Consumer.php b/src/Consumer/Consumer.php index 357a749..410fbd1 100644 --- a/src/Consumer/Consumer.php +++ b/src/Consumer/Consumer.php @@ -411,11 +411,21 @@ protected function heartbeat(): void } } + protected function getLastHeartbeatTime(): float + { + return $this->lastHeartbeatTime > 0 ? $this->lastHeartbeatTime : microtime(true); + } + + protected function setLastHeartbeatTime(float $lastHeartbeatTime): void + { + $this->lastHeartbeatTime = $lastHeartbeatTime; + } + protected function checkBeartbeat(): void { $time = microtime(true); - if ($time - $this->lastHeartbeatTime >= $this->config->getGroupHeartbeat()) { - $this->lastHeartbeatTime = $time; + if ($time - $this->getLastHeartbeatTime() >= $this->config->getGroupHeartbeat()) { + $this->setLastHeartbeatTime($time); $this->heartbeat(); } } From 0024531093044df9d2726b9243ff6a54c433244c Mon Sep 17 00:00:00 2001 From: wuxi Date: Tue, 20 Feb 2024 14:35:19 +0800 Subject: [PATCH 04/11] add English comments --- src/Client/SyncClient.php | 8 +++---- src/Sasl/ScramSha512Sasl.php | 43 ++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index 5dfb7a9..256445d 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -244,7 +244,7 @@ private function sendScramSha512AuthInfo(SaslInterface $class): void { /** @var \longlang\phpkafka\Sasl\ScramSha512Sasl $class */ - // 发送第一次验证信息 + // Send first verification message $handshakeRequest = new SaslHandshakeRequest(); $handshakeRequest->setMechanism($class->getName()); $correlationId = $this->send($handshakeRequest); @@ -252,7 +252,7 @@ private function sendScramSha512AuthInfo(SaslInterface $class): void $handshakeResponse = $this->recv($correlationId); ErrorCode::check($handshakeResponse->getErrorCode()); - // 第一次握手 + // First handshake $authenticateRequest = new SaslAuthenticateRequest(); $authenticateRequest->setAuthBytes($class->getAuthBytes()); $correlationId = $this->send($authenticateRequest); @@ -260,7 +260,7 @@ private function sendScramSha512AuthInfo(SaslInterface $class): void $authenticateResponse = $this->recv($correlationId); ErrorCode::check($authenticateResponse->getErrorCode()); - // 第二次握手 + // Second handshake $authenticateRequest = new SaslAuthenticateRequest(); $authenticateRequest->setAuthBytes($class->getFinalMessage($authenticateResponse->getAuthBytes())); $correlationId = $this->send($authenticateRequest); @@ -268,7 +268,7 @@ private function sendScramSha512AuthInfo(SaslInterface $class): void $authenticateResponse = $this->recv($correlationId); ErrorCode::check($authenticateResponse->getErrorCode()); - // 校验第二次服务器响应消息 + // Verify the second server response if ($class->enableFinalSignatureVerification()) { $class->verifyFinalMessage($authenticateResponse->getAuthBytes()); } diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php index 5197ac6..f9e27cf 100644 --- a/src/Sasl/ScramSha512Sasl.php +++ b/src/Sasl/ScramSha512Sasl.php @@ -35,7 +35,7 @@ public function getName(): string } /** - * SCRAM-SHA-512 第一次握手信息 + * SCRAM-SHA-512 first handshake * * @return string */ @@ -50,7 +50,7 @@ public function getAuthBytes(): string } /** - * 获取第一次握手信息 + * Get first handshake information of SCRAM-SHA-512 * * @return string */ @@ -60,7 +60,7 @@ private function getFirstMessageBare(): string } /** - * 获取 SASL 所有配置 + * Get all SASL configurations * * @return array */ @@ -70,7 +70,7 @@ public function getSaslConfigs(): array } /** - * 获取 SASL 配置 + * Get SASL simple configuration * * @param string $key * @return mixed @@ -81,7 +81,7 @@ public function getSaslConfig(string $key): mixed } /** - * 获取 SASL 密码 + * Get SASL password * * @return string */ @@ -91,22 +91,22 @@ private function getPassword(): string } /** - * 计算第二次握手信息 + * Second handshake of SCRAM-SHA-512 * * @param string $response * @return string */ public function getFinalMessage(string $response): string { - // 拆分第一次握手后的响应 + // Split the response after the first handshake [$r, $s, $i] = explode(',', $response); - // 提取随机数、盐和迭代次数 + // Extract the random number, salt, and number of iterations $serverNonce = $this->ltrimMessage($r); $salt = base64_decode($this->ltrimMessage($s)); $iterations = (int) $this->ltrimMessage($i); - // 计算第二次握手的参数 + // Calculate the parameters for the second handshake $this->saltedPassword = $saltedPassword = $this->calculateSaltedPassword($this->getPassword(), $salt, $iterations); $clientKey = $this->calculateClientKey($saltedPassword); @@ -123,8 +123,8 @@ public function getFinalMessage(string $response): string } /** - * 计算盐化密码 - * 使用 PBKDF2 函数和服务器提供的盐和迭代次数来计算盐化密码 + * Compute salted password + * Using PBKDF2 function and the salt and iteration count provided by the server * * @param string $password * @param string $salt @@ -137,21 +137,22 @@ private function calculateSaltedPassword(string $password, string $salt, int $it } /** - * 计算客户端密钥 - * 使用盐化密码和 HMAC 函数来计算客户端密钥 + * Compute client key + * Using salted password and HMAC function to calculate client key * * @param string $saltedPassword * @return string */ private function calculateClientKey(string $saltedPassword): string { - // 在 SCRAM-SHA-512 中需要用盐化密码来加密计算密,密钥钥固定是 Client Key + // In SCRAM-SHA-512, a salted password is required to encrypt the calculation secret + // and the key is fixed to "Client Key" return $this->hmac('Client Key', $saltedPassword); } /** - * 计算存储密钥 - * 使用客户端密钥和 SHA-256 函数来计算存储密钥 + * Compute stored key + * Using client key and SHA-256 function to calculate stored key * * @param string $clientKey * @return string @@ -162,7 +163,7 @@ private function calculateStoredKey(string $clientKey): string } /** - * 获取不带证明的消息 + * Get message without proof * * @param string $nonce * @return string @@ -173,7 +174,7 @@ private function getMessageWithoutProof(string $nonce): string } /** - * sha512 加密 + * SHA-512 encryption * * @param string $data * @param string $key @@ -185,7 +186,7 @@ public function hmac(string $data, string $key): string } /** - * 删除服务响应信息的前两个字符 + * Remove the first two characters of the server response message * * @param string $param * @return string @@ -196,7 +197,7 @@ public function ltrimMessage(string $param): string } /** - * 是否启用最终签名验证 + * Whether to enable final signature verification * * @return boolean */ @@ -206,7 +207,7 @@ public function enableFinalSignatureVerification(): bool } /** - * 验证最终签名 + * Verify final signature * * @param string $message * @return void From 094a0da5b5735870d13c3157b6327423f39d0139 Mon Sep 17 00:00:00 2001 From: wuxi Date: Wed, 21 Feb 2024 18:15:41 +0800 Subject: [PATCH 05/11] lint-fix --- src/Client/SyncClient.php | 12 ++++-------- src/Sasl/ScramSha512Sasl.php | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index 256445d..a8d6332 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -202,21 +202,19 @@ protected function updateApiVersions(): void protected function sendAuthInfo(): void { $config = $this->getConfig()->getSasl(); - if (! isset($config['type']) || empty($config['type'])) { + if (!isset($config['type']) || empty($config['type'])) { return; } $class = new $config['type']($this->getConfig()); - if (! $class instanceof SaslInterface) { + if (!$class instanceof SaslInterface) { return; } if ($class instanceof PlainSasl) { $this->sendPlainAuthInfo($class); - } - elseif ($class instanceof ScramSha512Sasl) { + } elseif ($class instanceof ScramSha512Sasl) { $this->sendScramSha512AuthInfo($class); - } - else { + } else { return; } } @@ -224,7 +222,6 @@ protected function sendAuthInfo(): void private function sendPlainAuthInfo(SaslInterface $class): void { /** @var \longlang\phpkafka\Sasl\PlainSasl $class */ - $handshakeRequest = new SaslHandshakeRequest(); $handshakeRequest->setMechanism($class->getName()); $correlationId = $this->send($handshakeRequest); @@ -243,7 +240,6 @@ private function sendPlainAuthInfo(SaslInterface $class): void private function sendScramSha512AuthInfo(SaslInterface $class): void { /** @var \longlang\phpkafka\Sasl\ScramSha512Sasl $class */ - // Send first verification message $handshakeRequest = new SaslHandshakeRequest(); $handshakeRequest->setMechanism($class->getName()); diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php index f9e27cf..9539608 100644 --- a/src/Sasl/ScramSha512Sasl.php +++ b/src/Sasl/ScramSha512Sasl.php @@ -75,7 +75,7 @@ public function getSaslConfigs(): array * @param string $key * @return mixed */ - public function getSaslConfig(string $key): mixed + public function getSaslConfig(string $key) { return $this->getSaslConfigs()[$key] ?? null; } From a0d476001d29b96b83d7f9e5f56495d8d39283bd Mon Sep 17 00:00:00 2001 From: wuxi Date: Wed, 21 Feb 2024 19:14:44 +0800 Subject: [PATCH 06/11] php cs fix --- src/Client/SyncClient.php | 4 +- src/Sasl/ScramSha512Sasl.php | 80 ++++++++---------------------------- 2 files changed, 20 insertions(+), 64 deletions(-) diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index a8d6332..ad20a11 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -163,7 +163,7 @@ public function send(AbstractRequest $request, ?RequestHeader $header = null, bo return $correlationId; } - public function recv(?int $correlationId, ?ResponseHeader &$header = null): AbstractResponse + public function recv(?int $correlationId, ?ResponseHeader & $header = null): AbstractResponse { if (!isset($this->waitResponseMaps[$correlationId])) { throw new InvalidArgumentException(sprintf('Invalid correlationId %s', $correlationId)); @@ -182,7 +182,7 @@ public function recv(?int $correlationId, ?ResponseHeader &$header = null): Abst return $result; } - public function sendRecv(AbstractRequest $request, ?RequestHeader $requestHeader = null, ?ResponseHeader &$responseHeader = null): AbstractResponse + public function sendRecv(AbstractRequest $request, ?RequestHeader $requestHeader = null, ?ResponseHeader & $responseHeader = null): AbstractResponse { $correlationId = $this->send($request, $requestHeader); diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php index 9539608..c7d6627 100644 --- a/src/Sasl/ScramSha512Sasl.php +++ b/src/Sasl/ScramSha512Sasl.php @@ -7,7 +7,6 @@ use longlang\phpkafka\Config\CommonConfig; use longlang\phpkafka\Exception\KafkaErrorException; use longlang\phpkafka\Protocol\ErrorCode; -use longlang\phpkafka\Sasl\SaslInterface; class ScramSha512Sasl implements SaslInterface { @@ -35,9 +34,7 @@ public function getName(): string } /** - * SCRAM-SHA-512 first handshake - * - * @return string + * SCRAM-SHA-512 first handshake. */ public function getAuthBytes(): string { @@ -50,9 +47,7 @@ public function getAuthBytes(): string } /** - * Get first handshake information of SCRAM-SHA-512 - * - * @return string + * Get first handshake information of SCRAM-SHA-512. */ private function getFirstMessageBare(): string { @@ -60,9 +55,7 @@ private function getFirstMessageBare(): string } /** - * Get all SASL configurations - * - * @return array + * Get all SASL configurations. */ public function getSaslConfigs(): array { @@ -70,31 +63,23 @@ public function getSaslConfigs(): array } /** - * Get SASL simple configuration - * - * @param string $key - * @return mixed + * Get SASL simple configuration. */ - public function getSaslConfig(string $key) + public function getSaslConfig(string $key): mixed { return $this->getSaslConfigs()[$key] ?? null; } /** - * Get SASL password - * - * @return string + * Get SASL password. */ private function getPassword(): string { - return $this->getSaslConfig('password') ?: ''; + return $this->getSaslConfigs()['password'] ?? ''; } /** - * Second handshake of SCRAM-SHA-512 - * - * @param string $response - * @return string + * Second handshake of SCRAM-SHA-512. */ public function getFinalMessage(string $response): string { @@ -123,13 +108,7 @@ public function getFinalMessage(string $response): string } /** - * Compute salted password - * Using PBKDF2 function and the salt and iteration count provided by the server - * - * @param string $password - * @param string $salt - * @param integer $iterations - * @return string + * Compute salted password using PBKDF2 function and the salt and iteration count provided by the server. */ private function calculateSaltedPassword(string $password, string $salt, int $iterations): string { @@ -137,11 +116,7 @@ private function calculateSaltedPassword(string $password, string $salt, int $it } /** - * Compute client key - * Using salted password and HMAC function to calculate client key - * - * @param string $saltedPassword - * @return string + * Compute client key using salted password and HMAC function to calculate client key. */ private function calculateClientKey(string $saltedPassword): string { @@ -151,11 +126,7 @@ private function calculateClientKey(string $saltedPassword): string } /** - * Compute stored key - * Using client key and SHA-256 function to calculate stored key - * - * @param string $clientKey - * @return string + * Compute stored key using client key and SHA-256 function to calculate stored key. */ private function calculateStoredKey(string $clientKey): string { @@ -163,10 +134,7 @@ private function calculateStoredKey(string $clientKey): string } /** - * Get message without proof - * - * @param string $nonce - * @return string + * Get message without proof. */ private function getMessageWithoutProof(string $nonce): string { @@ -174,11 +142,7 @@ private function getMessageWithoutProof(string $nonce): string } /** - * SHA-512 encryption - * - * @param string $data - * @param string $key - * @return string + * SHA-512 encryption. */ public function hmac(string $data, string $key): string { @@ -186,10 +150,7 @@ public function hmac(string $data, string $key): string } /** - * Remove the first two characters of the server response message - * - * @param string $param - * @return string + * Remove the first two characters of the server response message. */ public function ltrimMessage(string $param): string { @@ -197,9 +158,7 @@ public function ltrimMessage(string $param): string } /** - * Whether to enable final signature verification - * - * @return boolean + * Whether to enable final signature verification. */ public function enableFinalSignatureVerification(): bool { @@ -207,10 +166,7 @@ public function enableFinalSignatureVerification(): bool } /** - * Verify final signature - * - * @param string $message - * @return void + * Verify final signature. */ public function verifyFinalMessage(string $message): void { @@ -219,8 +175,8 @@ public function verifyFinalMessage(string $message): void $serverKey = $this->hmac('Server Key', $this->saltedPassword); $expectedSignature = $this->hmac($this->authMessage, $serverKey); - - if (! hash_equals($receivedSignature, $expectedSignature)) { + + if (!hash_equals($receivedSignature, $expectedSignature)) { ErrorCode::check(ErrorCode::SASL_AUTHENTICATION_FAILED); } } From a344f788d0f270698b92966d27e338c71c512798 Mon Sep 17 00:00:00 2001 From: wuxi Date: Wed, 21 Feb 2024 19:43:21 +0800 Subject: [PATCH 07/11] php cs fixed --- src/Client/SyncClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index ad20a11..a8d6332 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -163,7 +163,7 @@ public function send(AbstractRequest $request, ?RequestHeader $header = null, bo return $correlationId; } - public function recv(?int $correlationId, ?ResponseHeader & $header = null): AbstractResponse + public function recv(?int $correlationId, ?ResponseHeader &$header = null): AbstractResponse { if (!isset($this->waitResponseMaps[$correlationId])) { throw new InvalidArgumentException(sprintf('Invalid correlationId %s', $correlationId)); @@ -182,7 +182,7 @@ public function recv(?int $correlationId, ?ResponseHeader & $header = null): Abs return $result; } - public function sendRecv(AbstractRequest $request, ?RequestHeader $requestHeader = null, ?ResponseHeader & $responseHeader = null): AbstractResponse + public function sendRecv(AbstractRequest $request, ?RequestHeader $requestHeader = null, ?ResponseHeader &$responseHeader = null): AbstractResponse { $correlationId = $this->send($request, $requestHeader); From f68434598d39a6a31499f1f499594b0746cabf57 Mon Sep 17 00:00:00 2001 From: wuxi Date: Thu, 29 Aug 2024 16:56:38 +0800 Subject: [PATCH 08/11] php cs fix --- src/Client/SyncClient.php | 2 + src/Sasl/ScramSha512Sasl.php | 106 +++++++++++++++++++---------------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index a8d6332..4e022d0 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -211,8 +211,10 @@ protected function sendAuthInfo(): void } if ($class instanceof PlainSasl) { + /** \longlang\phpkafka\Sasl\PlainSasl $class */ $this->sendPlainAuthInfo($class); } elseif ($class instanceof ScramSha512Sasl) { + /** \longlang\phpkafka\Sasl\ScramSha512Sasl $class */ $this->sendScramSha512AuthInfo($class); } else { return; diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php index c7d6627..62f411f 100644 --- a/src/Sasl/ScramSha512Sasl.php +++ b/src/Sasl/ScramSha512Sasl.php @@ -1,6 +1,14 @@ getFirstMessageBare()); } - /** - * Get first handshake information of SCRAM-SHA-512. - */ - private function getFirstMessageBare(): string - { - return sprintf('n=%s,r=%s', $this->getSaslConfig('username'), $this->nonce); - } - /** * Get all SASL configurations. */ @@ -70,14 +72,6 @@ public function getSaslConfig(string $key): mixed return $this->getSaslConfigs()[$key] ?? null; } - /** - * Get SASL password. - */ - private function getPassword(): string - { - return $this->getSaslConfigs()['password'] ?? ''; - } - /** * Second handshake of SCRAM-SHA-512. */ @@ -108,76 +102,92 @@ public function getFinalMessage(string $response): string } /** - * Compute salted password using PBKDF2 function and the salt and iteration count provided by the server. + * SHA-512 encryption. */ - private function calculateSaltedPassword(string $password, string $salt, int $iterations): string + public function hmac(string $data, string $key): string { - return hash_pbkdf2('sha512', $password, $salt, $iterations, 0, true); + return hash_hmac('sha512', $data, $key, true); } /** - * Compute client key using salted password and HMAC function to calculate client key. + * Remove the first two characters of the server response message. */ - private function calculateClientKey(string $saltedPassword): string + public function ltrimMessage(string $param): string { - // In SCRAM-SHA-512, a salted password is required to encrypt the calculation secret - // and the key is fixed to "Client Key" - return $this->hmac('Client Key', $saltedPassword); + return substr($param, 2); } /** - * Compute stored key using client key and SHA-256 function to calculate stored key. + * Whether to enable final signature verification. */ - private function calculateStoredKey(string $clientKey): string + public function enableFinalSignatureVerification(): bool { - return hash('sha512', $clientKey, true); + return (bool) $this->getSaslConfig('verify_final_signature'); } /** - * Get message without proof. + * Verify final signature. */ - private function getMessageWithoutProof(string $nonce): string + public function verifyFinalMessage(string $message): void { - return sprintf('c=biws,r=%s', $nonce); + $receivedSignature = $this->ltrimMessage($message); + $receivedSignature = base64_decode($receivedSignature); + + $serverKey = $this->hmac('Server Key', $this->saltedPassword); + $expectedSignature = $this->hmac($this->authMessage, $serverKey); + + if (! hash_equals($receivedSignature, $expectedSignature)) { + ErrorCode::check(ErrorCode::SASL_AUTHENTICATION_FAILED); + } } /** - * SHA-512 encryption. + * Get first handshake information of SCRAM-SHA-512. */ - public function hmac(string $data, string $key): string + private function getFirstMessageBare(): string { - return hash_hmac('sha512', $data, $key, true); + return sprintf('n=%s,r=%s', $this->getSaslConfig('username'), $this->nonce); } /** - * Remove the first two characters of the server response message. + * Get SASL password. */ - public function ltrimMessage(string $param): string + private function getPassword(): string { - return substr($param, 2); + return $this->getSaslConfigs()['password'] ?? ''; } /** - * Whether to enable final signature verification. + * Compute salted password using PBKDF2 function and the salt and iteration count provided by the server. */ - public function enableFinalSignatureVerification(): bool + private function calculateSaltedPassword(string $password, string $salt, int $iterations): string { - return (bool) $this->getSaslConfig('verify_final_signature'); + return hash_pbkdf2('sha512', $password, $salt, $iterations, 0, true); } /** - * Verify final signature. + * Compute client key using salted password and HMAC function to calculate client key. */ - public function verifyFinalMessage(string $message): void + private function calculateClientKey(string $saltedPassword): string { - $receivedSignature = $this->ltrimMessage($message); - $receivedSignature = base64_decode($receivedSignature); + // In SCRAM-SHA-512, a salted password is required to encrypt the calculation secret + // and the key is fixed to "Client Key" + return $this->hmac('Client Key', $saltedPassword); + } - $serverKey = $this->hmac('Server Key', $this->saltedPassword); - $expectedSignature = $this->hmac($this->authMessage, $serverKey); + /** + * Compute stored key using client key and SHA-256 function to calculate stored key. + */ + private function calculateStoredKey(string $clientKey): string + { + return hash('sha512', $clientKey, true); + } - if (!hash_equals($receivedSignature, $expectedSignature)) { - ErrorCode::check(ErrorCode::SASL_AUTHENTICATION_FAILED); - } + /** + * Get message without proof. + */ + private function getMessageWithoutProof(string $nonce): string + { + return sprintf('c=biws,r=%s', $nonce); } } From 59d6a6f47bdeaaec6275fa63d44cf85c975bcc53 Mon Sep 17 00:00:00 2001 From: wuxi Date: Thu, 29 Aug 2024 18:03:37 +0800 Subject: [PATCH 09/11] remote scram-sha-512 file --- src/Client/SyncClient.php | 4 +- src/Sasl/ScramSha512Sasl.php | 193 ----------------------------------- 2 files changed, 2 insertions(+), 195 deletions(-) delete mode 100644 src/Sasl/ScramSha512Sasl.php diff --git a/src/Client/SyncClient.php b/src/Client/SyncClient.php index 4e022d0..83f396c 100644 --- a/src/Client/SyncClient.php +++ b/src/Client/SyncClient.php @@ -211,10 +211,10 @@ protected function sendAuthInfo(): void } if ($class instanceof PlainSasl) { - /** \longlang\phpkafka\Sasl\PlainSasl $class */ + /* \longlang\phpkafka\Sasl\PlainSasl $class */ $this->sendPlainAuthInfo($class); } elseif ($class instanceof ScramSha512Sasl) { - /** \longlang\phpkafka\Sasl\ScramSha512Sasl $class */ + /* \longlang\phpkafka\Sasl\ScramSha512Sasl $class */ $this->sendScramSha512AuthInfo($class); } else { return; diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php deleted file mode 100644 index 62f411f..0000000 --- a/src/Sasl/ScramSha512Sasl.php +++ /dev/null @@ -1,193 +0,0 @@ -config = $config; - $this->nonce = base64_encode(random_bytes(16)); - } - - /** - * 授权模式. - */ - public function getName(): string - { - return 'SCRAM-SHA-512'; - } - - /** - * SCRAM-SHA-512 first handshake. - */ - public function getAuthBytes(): string - { - $config = $this->config->getSasl(); - if (empty($config['username']) || empty($config['password'])) { - throw new KafkaErrorException('sasl not found auth info'); - } - - return sprintf('n,,%s', $this->getFirstMessageBare()); - } - - /** - * Get all SASL configurations. - */ - public function getSaslConfigs(): array - { - return $this->config->getSasl(); - } - - /** - * Get SASL simple configuration. - */ - public function getSaslConfig(string $key): mixed - { - return $this->getSaslConfigs()[$key] ?? null; - } - - /** - * Second handshake of SCRAM-SHA-512. - */ - public function getFinalMessage(string $response): string - { - // Split the response after the first handshake - [$r, $s, $i] = explode(',', $response); - - // Extract the random number, salt, and number of iterations - $serverNonce = $this->ltrimMessage($r); - $salt = base64_decode($this->ltrimMessage($s)); - $iterations = (int) $this->ltrimMessage($i); - - // Calculate the parameters for the second handshake - $this->saltedPassword = $saltedPassword = $this->calculateSaltedPassword($this->getPassword(), $salt, $iterations); - - $clientKey = $this->calculateClientKey($saltedPassword); - $storedKey = $this->calculateStoredKey($clientKey); - - $clientFirstMessageBare = $this->getFirstMessageBare(); - $serverFirstMessage = $response; - $clientFinalMessageWithoutProof = $this->getMessageWithoutProof($serverNonce); - - $this->authMessage = $authMessage = sprintf('%s,%s,%s', $clientFirstMessageBare, $serverFirstMessage, $clientFinalMessageWithoutProof); - $clientSignature = $this->hmac($authMessage, $storedKey); - - return sprintf('%s,p=%s', $clientFinalMessageWithoutProof, base64_encode($clientKey ^ $clientSignature)); - } - - /** - * SHA-512 encryption. - */ - public function hmac(string $data, string $key): string - { - return hash_hmac('sha512', $data, $key, true); - } - - /** - * Remove the first two characters of the server response message. - */ - public function ltrimMessage(string $param): string - { - return substr($param, 2); - } - - /** - * Whether to enable final signature verification. - */ - public function enableFinalSignatureVerification(): bool - { - return (bool) $this->getSaslConfig('verify_final_signature'); - } - - /** - * Verify final signature. - */ - public function verifyFinalMessage(string $message): void - { - $receivedSignature = $this->ltrimMessage($message); - $receivedSignature = base64_decode($receivedSignature); - - $serverKey = $this->hmac('Server Key', $this->saltedPassword); - $expectedSignature = $this->hmac($this->authMessage, $serverKey); - - if (! hash_equals($receivedSignature, $expectedSignature)) { - ErrorCode::check(ErrorCode::SASL_AUTHENTICATION_FAILED); - } - } - - /** - * Get first handshake information of SCRAM-SHA-512. - */ - private function getFirstMessageBare(): string - { - return sprintf('n=%s,r=%s', $this->getSaslConfig('username'), $this->nonce); - } - - /** - * Get SASL password. - */ - private function getPassword(): string - { - return $this->getSaslConfigs()['password'] ?? ''; - } - - /** - * Compute salted password using PBKDF2 function and the salt and iteration count provided by the server. - */ - private function calculateSaltedPassword(string $password, string $salt, int $iterations): string - { - return hash_pbkdf2('sha512', $password, $salt, $iterations, 0, true); - } - - /** - * Compute client key using salted password and HMAC function to calculate client key. - */ - private function calculateClientKey(string $saltedPassword): string - { - // In SCRAM-SHA-512, a salted password is required to encrypt the calculation secret - // and the key is fixed to "Client Key" - return $this->hmac('Client Key', $saltedPassword); - } - - /** - * Compute stored key using client key and SHA-256 function to calculate stored key. - */ - private function calculateStoredKey(string $clientKey): string - { - return hash('sha512', $clientKey, true); - } - - /** - * Get message without proof. - */ - private function getMessageWithoutProof(string $nonce): string - { - return sprintf('c=biws,r=%s', $nonce); - } -} From 8223fb0855e0f404561ac46cf81fb30c75d7f1f3 Mon Sep 17 00:00:00 2001 From: wuxi Date: Thu, 29 Aug 2024 18:06:06 +0800 Subject: [PATCH 10/11] add scram-sha-512 file --- src/Sasl/ScramSha512Sasl.php | 187 +++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/Sasl/ScramSha512Sasl.php diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php new file mode 100644 index 0000000..3171412 --- /dev/null +++ b/src/Sasl/ScramSha512Sasl.php @@ -0,0 +1,187 @@ +config = $config; + $this->nonce = base64_encode(random_bytes(16)); + } + + /** + * 授权模式. + */ + public function getName(): string + { + return 'SCRAM-SHA-512'; + } + + /** + * SCRAM-SHA-512 first handshake. + */ + public function getAuthBytes(): string + { + $config = $this->config->getSasl(); + if (empty($config['username']) || empty($config['password'])) { + throw new KafkaErrorException('sasl not found auth info'); + } + + return sprintf('n,,%s', $this->getFirstMessageBare()); + } + + /** + * Get all SASL configurations. + */ + public function getSaslConfigs(): array + { + return $this->config->getSasl(); + } + + /** + * Get SASL simple configuration. + */ + public function getSaslConfig(string $key): mixed + { + return $this->getSaslConfigs()[$key] ?? null; + } + + /** + * Second handshake of SCRAM-SHA-512. + */ + public function getFinalMessage(string $response): string + { + // Split the response after the first handshake + [$r, $s, $i] = explode(',', $response); + + // Extract the random number, salt, and number of iterations + $serverNonce = $this->ltrimMessage($r); + $salt = base64_decode($this->ltrimMessage($s)); + $iterations = (int) $this->ltrimMessage($i); + + // Calculate the parameters for the second handshake + $saltedPassword = $this->calculateSaltedPassword($this->getPassword(), $salt, $iterations); + $this->saltedPassword = $saltedPassword; + + $clientKey = $this->calculateClientKey($saltedPassword); + $storedKey = $this->calculateStoredKey($clientKey); + + $clientFirstMessageBare = $this->getFirstMessageBare(); + $serverFirstMessage = $response; + $clientFinalMessageWithoutProof = $this->getMessageWithoutProof($serverNonce); + + $authMessage = sprintf('%s,%s,%s', $clientFirstMessageBare, $serverFirstMessage, $clientFinalMessageWithoutProof); + $this->authMessage = $authMessage; + $clientSignature = $this->hmac($authMessage, $storedKey); + + return sprintf('%s,p=%s', $clientFinalMessageWithoutProof, base64_encode($clientKey ^ $clientSignature)); + } + + /** + * SHA-512 encryption. + */ + public function hmac(string $data, string $key): string + { + return hash_hmac('sha512', $data, $key, true); + } + + /** + * Remove the first two characters of the server response message. + */ + public function ltrimMessage(string $param): string + { + return substr($param, 2); + } + + /** + * Whether to enable final signature verification. + */ + public function enableFinalSignatureVerification(): bool + { + return (bool) $this->getSaslConfig('verify_final_signature'); + } + + /** + * Verify final signature. + */ + public function verifyFinalMessage(string $message): void + { + $receivedSignature = $this->ltrimMessage($message); + $receivedSignature = base64_decode($receivedSignature); + + $serverKey = $this->hmac('Server Key', $this->saltedPassword); + $expectedSignature = $this->hmac($this->authMessage, $serverKey); + + if (!hash_equals($receivedSignature, $expectedSignature)) { + ErrorCode::check(ErrorCode::SASL_AUTHENTICATION_FAILED); + } + } + + /** + * Get first handshake information of SCRAM-SHA-512. + */ + private function getFirstMessageBare(): string + { + return sprintf('n=%s,r=%s', $this->getSaslConfig('username'), $this->nonce); + } + + /** + * Get SASL password. + */ + private function getPassword(): string + { + return $this->getSaslConfigs()['password'] ?? ''; + } + + /** + * Compute salted password using PBKDF2 function and the salt and iteration count provided by the server. + */ + private function calculateSaltedPassword(string $password, string $salt, int $iterations): string + { + return hash_pbkdf2('sha512', $password, $salt, $iterations, 0, true); + } + + /** + * Compute client key using salted password and HMAC function to calculate client key. + */ + private function calculateClientKey(string $saltedPassword): string + { + // In SCRAM-SHA-512, a salted password is required to encrypt the calculation secret + // and the key is fixed to "Client Key" + return $this->hmac('Client Key', $saltedPassword); + } + + /** + * Compute stored key using client key and SHA-256 function to calculate stored key. + */ + private function calculateStoredKey(string $clientKey): string + { + return hash('sha512', $clientKey, true); + } + + /** + * Get message without proof. + */ + private function getMessageWithoutProof(string $nonce): string + { + return sprintf('c=biws,r=%s', $nonce); + } +} From 7d38cb4b603a9e736a15951419d0117ea4bf0e80 Mon Sep 17 00:00:00 2001 From: wuxi Date: Thu, 29 Aug 2024 18:14:46 +0800 Subject: [PATCH 11/11] php cs fixed --- src/Sasl/ScramSha512Sasl.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Sasl/ScramSha512Sasl.php b/src/Sasl/ScramSha512Sasl.php index 3171412..6b894b4 100644 --- a/src/Sasl/ScramSha512Sasl.php +++ b/src/Sasl/ScramSha512Sasl.php @@ -15,11 +15,20 @@ class ScramSha512Sasl implements SaslInterface */ protected $config; - protected string $nonce = ''; + /** + * @var string + */ + protected $nonce = ''; - protected string $saltedPassword = ''; + /** + * @var string + */ + protected $saltedPassword = ''; - protected string $authMessage = ''; + /** + * @var string + */ + protected $authMessage = ''; public function __construct(CommonConfig $config) {