From e8e6fb5855f9e7ebe6165f7ca195f0e2b8e88043 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 14 Feb 2024 13:20:42 +0100 Subject: [PATCH] Support encryption aesgcm --- .../service/worker/Encryption.class.php | 87 ++++++++++++++++--- .../worker/ServiceWorkerHandler.class.php | 1 + 2 files changed, 76 insertions(+), 12 deletions(-) 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 34818d3b90..7f08917df1 100644 --- a/wcfsetup/install/files/lib/system/service/worker/Encryption.class.php +++ b/wcfsetup/install/files/lib/system/service/worker/Encryption.class.php @@ -21,14 +21,15 @@ final class Encryption public const HASH_ALGORITHM = 'sha256'; /** - * Return the encrypted payload. + * Return the encrypted payload and necessary headers to send a push message to the push-service. * {@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 */ - public static function encrypt(ServiceWorker $serviceWorker, string $payload): string + public static function encrypt(ServiceWorker $serviceWorker, string $payload, array &$headers): string { // Section 3.1 // user @@ -52,26 +53,24 @@ final class Encryption ); // Section 3.4 $salt = \random_bytes(16); + $content = Encryption::createContext($userPublicKey, $newPublicKey, $serviceWorker->contentEncoding); + $cek = \hash_hkdf( Encryption::HASH_ALGORITHM, $ikm, 16, - "Content-Encoding: aes128gcm\0", + Encryption::createInfo($serviceWorker->contentEncoding, $content, $serviceWorker->contentEncoding), $salt ); $nonce = \hash_hkdf( Encryption::HASH_ALGORITHM, $ikm, 12, - "Content-Encoding: nonce\0", + Encryption::createInfo('nonce', $content, $serviceWorker->contentEncoding), $salt ); // Section 4 - $payload = Encryption::addPadding($payload); - $encryptionContentCodingHeader = $salt - . \pack('N*', 4096) - . \pack('C*', Binary::safeStrlen($newPublicKey)) - . $newPublicKey; + $payload = Encryption::addPadding($payload, $serviceWorker->contentEncoding); $tag = ''; $encryptedText = \openssl_encrypt( @@ -83,7 +82,16 @@ final class Encryption $tag ); - return $encryptionContentCodingHeader . $encryptedText . $tag; + if ($serviceWorker->contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { + $headers['Encryption'] = 'salt=' . Base64Url::encode($salt); + $headers['Crypto-Key'] = 'dh=' . Base64Url::encode($newPublicKey); + } + + return Encryption::getEncryptionContentCodingHeader( + $serviceWorker->contentEncoding, + $salt, + $newPublicKey + ) . $encryptedText . $tag; } private static function getSharedSecret(JWK $publicKey, #[\SensitiveParameter] JWK $privateKey): string @@ -97,11 +105,66 @@ final class Encryption return \str_pad($result, 32, "\0", STR_PAD_LEFT); } - private static function addPadding(string $payload): string + private static function addPadding(string $payload, string $contentEncoding): string { $length = Binary::safeStrlen($payload); $paddingLength = ServiceWorkerHandler::MAX_PAYLOAD_LENGTH - $length; - return \str_pad($payload . "\2", $paddingLength + $length, "\0", STR_PAD_RIGHT); + if ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { + return \str_pad($payload . "\2", $paddingLength + $length, "\0", STR_PAD_RIGHT); + } elseif ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { + return \pack('n*', $paddingLength) . \str_pad($payload, $paddingLength + $length, "\0", STR_PAD_LEFT); + } else { + throw new \RuntimeException('Unknown content encoding "' . $contentEncoding . '"'); + } + } + + private static function createInfo(string $type, ?string $context, string $contentEncoding) + { + if ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { + \assert($context !== null); + \assert(Binary::safeStrlen($context) === 135); + + return 'Content-Encoding: ' . $type . "\0" . Encryption::CURVE_ALGORITHM . $context; + } elseif ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { + return 'Content-Encoding: ' . $type . "\0"; + } else { + throw new \RuntimeException('Unknown content encoding "' . $contentEncoding . '"'); + } + } + + private static function getEncryptionContentCodingHeader( + string $contentEncoding, + string $salt, + string $publicKey + ): string { + if ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { + return $salt + . \pack('N*', 4096) + . \pack('C*', Binary::safeStrlen($publicKey)) + . $publicKey; + } elseif ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AESGCM) { + return ""; + } else { + throw new \RuntimeException('Unknown content encoding "' . $contentEncoding . '"'); + } + } + + /** + * {@link https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-encryption-encoding-00#section-4.2} + */ + private static function createContext( + string $clientPublicKey, + string $serverPublicKey, + string $contentEncoding + ): ?string { + if ($contentEncoding === ServiceWorker::CONTENT_ENCODING_AES128GCM) { + return null; + } + \assert(Binary::safeStrlen($clientPublicKey) === 65); + + $len = "\0A"; // 65 as Uint16BE + + return "\0" . $len . $clientPublicKey . $len . $serverPublicKey; } } 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 36b05490be..2334ed8821 100644 --- a/wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php +++ b/wcfsetup/install/files/lib/system/service/worker/ServiceWorkerHandler.class.php @@ -73,6 +73,7 @@ final class ServiceWorkerHandler extends SingletonFactory $content = Encryption::encrypt( $serviceWorker, $payload, + $headers ); $headers['Content-Length'] = Binary::safeStrlen($content); VAPID::addHeader($serviceWorker, $headers); -- 2.20.1