From c74b408882f4cbf86e41ade65701fcc94495ce06 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 20 Feb 2024 12:08:04 +0100 Subject: [PATCH] Use `RequestInterface` as parameter --- ...rviceWorkerDeliveryBackgroundJob.class.php | 3 +- .../service/worker/Encryption.class.php | 28 +++++++++------ .../worker/ServiceWorkerHandler.class.php | 21 ++++-------- .../lib/system/service/worker/Util.class.php | 34 +++++++++++++++++-- .../lib/system/service/worker/VAPID.class.php | 15 ++++---- 5 files changed, 67 insertions(+), 34 deletions(-) diff --git a/wcfsetup/install/files/lib/system/background/job/ServiceWorkerDeliveryBackgroundJob.class.php b/wcfsetup/install/files/lib/system/background/job/ServiceWorkerDeliveryBackgroundJob.class.php index b1db3c4119..282c1fd1b6 100644 --- a/wcfsetup/install/files/lib/system/background/job/ServiceWorkerDeliveryBackgroundJob.class.php +++ b/wcfsetup/install/files/lib/system/background/job/ServiceWorkerDeliveryBackgroundJob.class.php @@ -44,7 +44,7 @@ final class ServiceWorkerDeliveryBackgroundJob extends AbstractBackgroundJob $user = UserProfileRuntimeCache::getInstance()->getObject($serviceWorker->userID); $style = new Style($user->styleID); if (!$style->styleID) { - $style = StyleHandler::getInstance()->getDefaultStyle(); + $style = StyleHandler::getInstance()->getStyle(); } /** @see NotificationEmailDeliveryBackgroundJob::perform() */ @@ -91,6 +91,7 @@ final class ServiceWorkerDeliveryBackgroundJob extends AbstractBackgroundJob ServiceWorkerHandler::getInstance()->sendToServiceWorker($serviceWorker, JSON::encode($content)); } catch (ClientExceptionInterface $e) { + \wcfDebug($e->getCode(), $e); if ($e->getCode() === 413) { // Payload too large, we can't do anything here other than discard the message. \wcf\functions\exception\logThrowable($e); diff --git a/wcfsetup/install/files/lib/system/service/worker/Encryption.class.php b/wcfsetup/install/files/lib/system/service/worker/Encryption.class.php index 91dc7d571f..3e5badfc51 100644 --- a/wcfsetup/install/files/lib/system/service/worker/Encryption.class.php +++ b/wcfsetup/install/files/lib/system/service/worker/Encryption.class.php @@ -6,8 +6,10 @@ use Base64Url\Base64Url; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\ECKey; use Jose\Component\KeyManagement\JWKFactory; +use Laminas\Diactoros\Stream; use ParagonIE\ConstantTime\Base64; use ParagonIE\ConstantTime\Base64UrlSafe; +use Psr\Http\Message\RequestInterface; use wcf\data\service\worker\ServiceWorker; /** @@ -22,20 +24,20 @@ final class Encryption public const HASH_ALGORITHM = 'sha256'; /** - * Return the encrypted payload and necessary headers to send a push message to the push-service. + * Return a request with the encrypted payload and add necessary headers. * {@link https://www.rfc-editor.org/rfc/rfc8291} * * @param ServiceWorker $serviceWorker * @param string $payload - * @param array $headers to this array the encryption headers will be added - * @return string the encrypted payload + * @param RequestInterface $request + * @return RequestInterface the new request with the encrypted payload */ public static function encrypt( ServiceWorker $serviceWorker, #[\SensitiveParameter] string $payload, - array &$headers - ): string { + RequestInterface $request, + ): RequestInterface { // Section 3.1 // user $userPublicKey = Base64Url::decode($serviceWorker->publicKey); @@ -86,12 +88,16 @@ final class Encryption } $record = $encryptedText . $tag; - return Encryption::getEncryptionContentCodingHeader( + $body = new Stream('php://temp', 'wb+'); + $body->write(Encryption::getEncryptionContentCodingHeader( $serviceWorker->contentEncoding, \mb_strlen($record, '8bit'), $salt, $newPublicKey - ) . $record; + ) . $record); + $body->rewind(); + + return $request->withBody($body)->withHeader('content-length', \mb_strlen($body, '8bit')); } private static function getSharedSecret(JWK $publicKey, #[\SensitiveParameter] JWK $privateKey): string @@ -115,7 +121,7 @@ final class Encryption } elseif ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { return \pack('n*', $paddingLength) . \str_pad($payload, $paddingLength + $length, "\x00", STR_PAD_LEFT); } else { - throw new \RuntimeException('Unknown content encoding "' . $contentEncoding . '"'); + throw new \RuntimeException("Unknown content encoding '{$contentEncoding}'"); } } @@ -129,7 +135,7 @@ final class Encryption } elseif ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { return "Content-Encoding: {$type}\x00"; } else { - throw new \RuntimeException('Unknown content encoding "' . $contentEncoding . '"'); + throw new \RuntimeException("Unknown content encoding '{$contentEncoding}'"); } } @@ -148,7 +154,7 @@ final class Encryption } elseif ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { return ""; } else { - throw new \RuntimeException('Unknown content encoding "' . $contentEncoding . '"'); + throw new \RuntimeException("Unknown content encoding '{$contentEncoding}'"); } } @@ -181,7 +187,7 @@ final class Encryption } elseif ($serviceWorker->contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { $info = "WebPush: info\x00" . $userPublicKey . $newPublicKey; } else { - throw new \RuntimeException('Unknown content encoding "' . $serviceWorker->contentEncoding . '"'); + throw new \RuntimeException("Unknown content encoding '{$serviceWorker->contentEncoding}'"); } return \hash_hkdf( diff --git a/wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php b/wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php index 6d55b87293..0269b92f7c 100644 --- a/wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php +++ b/wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php @@ -81,27 +81,20 @@ final class ServiceWorkerHandler extends SingletonFactory ); } - $headers = [ + $request = new Request('POST', $serviceWorker->endpoint, [ 'content-type' => 'application/octet-stream', 'content-encoding' => $serviceWorker->contentEncoding, 'ttl' => ServiceWorkerHandler::TTL, - ]; - $content = Encryption::encrypt( + ]); + + $request = Encryption::encrypt( $serviceWorker, $payload, - $headers + $request ); - $headers['content-length'] = \mb_strlen($content, '8bit'); - VAPID::addHeader($serviceWorker, $headers); + $request = VAPID::addHeader($serviceWorker, $request); - $this->getClient()->send( - new Request( - 'POST', - $serviceWorker->endpoint, - $headers, - $content - ) - ); + $this->getClient()->send($request); } private function getClient(): ClientInterface diff --git a/wcfsetup/install/files/lib/system/service/worker/Util.class.php b/wcfsetup/install/files/lib/system/service/worker/Util.class.php index ecfd87d89f..0701084379 100644 --- a/wcfsetup/install/files/lib/system/service/worker/Util.class.php +++ b/wcfsetup/install/files/lib/system/service/worker/Util.class.php @@ -5,6 +5,7 @@ namespace wcf\system\service\worker; use Base64Url\Base64Url; use Jose\Component\Core\JWK; use ParagonIE\ConstantTime\Binary; +use Psr\Http\Message\RequestInterface; /** * @author Olaf Braun @@ -58,8 +59,16 @@ final class Util * * @return JWK */ - public static function createJWK(?string $x = null, string $y = null, #[\SensitiveParameter] ?string $d = null): JWK - { + public static function createJWK( + ?string $x = null, + ?string $y = null, + #[\SensitiveParameter] ?string $d = null + ): JWK { + \assert( + ($x === null && $y === null) || ($x !== null && $y !== null), + "Both x and y must be set or both must be null." + ); + $values = [ 'kty' => 'EC', 'crv' => Encryption::CURVE_ALGORITHM, @@ -76,4 +85,25 @@ final class Util return new JWK($values); } + + /** + * Return a request with an updated crypto-key header. + * This header needs a `;` to separate multiple keys. + * + * @param RequestInterface $request + * @param string $name + * @param string $value + * + * @return RequestInterface + */ + public static function updateCryptoKeyHeader( + RequestInterface $request, + string $name, + string $value + ): RequestInterface { + return $request->withHeader('crypto-key', \implode(';', [ + ...$request->getHeader('crypto-key'), + \sprintf('%s=%s', $name, $value) + ])); + } } diff --git a/wcfsetup/install/files/lib/system/service/worker/VAPID.class.php b/wcfsetup/install/files/lib/system/service/worker/VAPID.class.php index e620f7c861..35674d07f0 100644 --- a/wcfsetup/install/files/lib/system/service/worker/VAPID.class.php +++ b/wcfsetup/install/files/lib/system/service/worker/VAPID.class.php @@ -8,6 +8,7 @@ use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer; use ParagonIE\ConstantTime\Base64UrlSafe; +use Psr\Http\Message\RequestInterface; use wcf\data\service\worker\ServiceWorker; use wcf\util\JSON; @@ -23,13 +24,15 @@ final class VAPID public const PRIVATE_KEY_LENGTH = 32; /** - * Add the VAPID header to the request headers. + * Return a request with the VAPID header. * {@link https://www.rfc-editor.org/rfc/rfc8282} * * @param ServiceWorker $serviceWorker - * @param array $headers + * @param RequestInterface $request + * + * @return RequestInterface */ - public static function addHeader(ServiceWorker $serviceWorker, array &$headers): void + public static function addHeader(ServiceWorker $serviceWorker, RequestInterface $request): RequestInterface { $rawPublicKey = Base64Url::decode(SERVICE_WORKER_PUBLIC_KEY); // Validate the length of the public key @@ -62,10 +65,10 @@ final class VAPID $jwt = $compactSerializer->serialize($jws, 0); if ($serviceWorker->contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { - $headers['Authorization'] = 'WebPush ' . $jwt; - $headers['Crypto-Key'] .= ';p256ecdsa=' . SERVICE_WORKER_PUBLIC_KEY; + $request = $request->withHeader('authorization', "WebPush {$jwt}"); + return Util::updateCryptoKeyHeader($request, 'p256ecdsa', SERVICE_WORKER_PUBLIC_KEY); } elseif ($serviceWorker->contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { - $headers['Authorization'] = 'vapid t=' . $jwt . ', k=' . SERVICE_WORKER_PUBLIC_KEY; + return $request->withHeader('authorization', \sprintf("vapid t=%s, k=%s", $jwt, SERVICE_WORKER_PUBLIC_KEY)); } else { throw new \InvalidArgumentException('Invalid content encoding: "' . $serviceWorker->contentEncoding . '"'); } -- 2.20.1