Use `RequestInterface` as parameter
authorCyperghost <olaf_schmitz_1@t-online.de>
Tue, 20 Feb 2024 11:08:04 +0000 (12:08 +0100)
committerCyperghost <olaf_schmitz_1@t-online.de>
Fri, 23 Feb 2024 13:42:51 +0000 (14:42 +0100)
wcfsetup/install/files/lib/system/background/job/ServiceWorkerDeliveryBackgroundJob.class.php
wcfsetup/install/files/lib/system/service/worker/Encryption.class.php
wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php
wcfsetup/install/files/lib/system/service/worker/Util.class.php
wcfsetup/install/files/lib/system/service/worker/VAPID.class.php

index b1db3c4119df6df0ad035aee7c26da781ffc7dff..282c1fd1b6c92878647ff522123f4cc46740bb9a 100644 (file)
@@ -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);
index 91dc7d571fcb44d2a44bf815c48527c54ce904e7..3e5badfc51ad72ac1c4583a8df8f9968666fa8b4 100644 (file)
@@ -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(
index 6d55b87293f02bffcff515e6a7ac2f8bae96f1bc..0269b92f7c500a6733ac8baae8668135f430af0d 100644 (file)
@@ -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
index ecfd87d89ff4c23066ef066bf96bebb506cba9de..0701084379b188940a8c2443825ffb647b5fea15 100644 (file)
@@ -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)
+        ]));
+    }
 }
index e620f7c86119732e476b61162bcd481818c2a859..35674d07f0769e6a848b3d4674d967668e60bce9 100644 (file)
@@ -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 . '"');
         }