From ed065bed1fe7a1c329292c1832f67cb4873dbaee Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 28 Feb 2024 13:00:40 +0100 Subject: [PATCH 1/3] Replace `spomky-labs/base64url` with `paragonie/constant_time_encoding` --- composer.json | 5 ++--- src/Encryption.php | 21 ++++++++++----------- src/Utils.php | 16 +++++++++++++--- src/VAPID.php | 19 +++++++++---------- src/WebPush.php | 5 ++--- tests/EncryptionTest.php | 23 +++++++++++------------ 6 files changed, 47 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index b9b34b9..4c8f7e7 100644 --- a/composer.json +++ b/composer.json @@ -34,8 +34,7 @@ "ext-mbstring": "*", "ext-openssl": "*", "guzzlehttp/guzzle": "^7.4.5", - "web-token/jwt-library": "^3.3.0", - "spomky-labs/base64url": "^2.0.4" + "web-token/jwt-library": "^3.3.0" }, "suggest": { "ext-bcmath": "Optional for performance.", @@ -51,4 +50,4 @@ "Minishlink\\WebPush\\": "src" } } -} \ No newline at end of file +} diff --git a/src/Encryption.php b/src/Encryption.php index e6bb708..c2110a8 100644 --- a/src/Encryption.php +++ b/src/Encryption.php @@ -13,7 +13,6 @@ namespace Minishlink\WebPush; -use Base64Url\Base64Url; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Ecc\PrivateKey; use Jose\Component\Core\Util\ECKey; @@ -66,8 +65,8 @@ public static function encrypt(string $payload, string $userPublicKey, string $u */ public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array { - $userPublicKey = Base64Url::decode($userPublicKey); - $userAuthToken = Base64Url::decode($userAuthToken); + $userPublicKey = Utils::base64Decode($userPublicKey); + $userAuthToken = Utils::base64Decode($userAuthToken); // get local key pair if (count($localKeyObject) === 1) { @@ -81,9 +80,9 @@ public static function deterministicEncrypt(string $payload, string $userPublicK $localJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'd' => Base64Url::encode($localPrivateKeyObject->getSecret()->toBytes(false)), - 'x' => Base64Url::encode($localPublicKeyObject[0]), - 'y' => Base64Url::encode($localPublicKeyObject[1]), + 'd' => Utils::base64Encode($localPrivateKeyObject->getSecret()->toBytes(false)), + 'x' => Utils::base64Encode($localPublicKeyObject[0]), + 'y' => Utils::base64Encode($localPublicKeyObject[1]), ]); } if (!$localPublicKey) { @@ -95,8 +94,8 @@ public static function deterministicEncrypt(string $payload, string $userPublicK $userJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Base64Url::encode($userPublicKeyObjectX), - 'y' => Base64Url::encode($userPublicKeyObjectY), + 'x' => Utils::base64Encode($userPublicKeyObjectX), + 'y' => Utils::base64Encode($userPublicKeyObjectY), ]); // get shared secret from user public key and local private key @@ -252,9 +251,9 @@ private static function createLocalKeyObject(): array new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Base64Url::encode(self::addNullPadding($details['ec']['x'])), - 'y' => Base64Url::encode(self::addNullPadding($details['ec']['y'])), - 'd' => Base64Url::encode(self::addNullPadding($details['ec']['d'])), + 'x' => Utils::base64Encode(self::addNullPadding($details['ec']['x'])), + 'y' => Utils::base64Encode(self::addNullPadding($details['ec']['y'])), + 'd' => Utils::base64Encode(self::addNullPadding($details['ec']['d'])), ]), ]; } diff --git a/src/Utils.php b/src/Utils.php index 887acb0..0a39c4a 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -13,9 +13,9 @@ namespace Minishlink\WebPush; -use Base64Url\Base64Url; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Ecc\PublicKey; +use ParagonIE\ConstantTime\Base64UrlSafe; class Utils { @@ -37,8 +37,8 @@ public static function serializePublicKey(PublicKey $publicKey): string public static function serializePublicKeyFromJWK(JWK $jwk): string { $hexString = '04'; - $hexString .= str_pad(bin2hex(Base64Url::decode($jwk->get('x'))), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(bin2hex(Base64Url::decode($jwk->get('y'))), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad(bin2hex(Utils::base64Decode($jwk->get('x'))), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad(bin2hex(Utils::base64Decode($jwk->get('y'))), 64, '0', STR_PAD_LEFT); return $hexString; } @@ -58,6 +58,16 @@ public static function unserializePublicKey(string $data): array ]; } + public static function base64Decode(string $string): string + { + return Base64UrlSafe::decodeNoPadding(strtr($string, '-_', '+/')); + } + + public static function base64Encode(string $string): string + { + return strtr(Base64UrlSafe::encodeUnpadded($string), '+/', '-_'); + } + /** * Generates user warning/notice if some requirements are not met. * Does not throw exception to allow unusual or polyfill environments. diff --git a/src/VAPID.php b/src/VAPID.php index 5a40cd9..e42e87e 100644 --- a/src/VAPID.php +++ b/src/VAPID.php @@ -13,7 +13,6 @@ namespace Minishlink\WebPush; -use Base64Url\Base64Url; use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\JWK; use Jose\Component\KeyManagement\JWKFactory; @@ -54,14 +53,14 @@ public static function validate(array $vapid): array throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); } $vapid['publicKey'] = base64_encode($binaryPublicKey); - $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + $vapid['privateKey'] = base64_encode(str_pad(Utils::base64Decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); } if (!isset($vapid['publicKey'])) { throw new \ErrorException('[VAPID] You must provide a public key.'); } - $publicKey = Base64Url::decode($vapid['publicKey']); + $publicKey = Utils::base64Decode($vapid['publicKey']); if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) { throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.'); @@ -71,7 +70,7 @@ public static function validate(array $vapid): array throw new \ErrorException('[VAPID] You must provide a private key.'); } - $privateKey = Base64Url::decode($vapid['privateKey']); + $privateKey = Utils::base64Decode($vapid['privateKey']); if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) { throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.'); @@ -122,9 +121,9 @@ public static function getVapidHeaders(string $audience, string $subject, string $jwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Base64Url::encode($x), - 'y' => Base64Url::encode($y), - 'd' => Base64Url::encode($privateKey), + 'x' => Utils::base64Encode($x), + 'y' => Utils::base64Encode($y), + 'd' => Utils::base64Encode($privateKey), ]); $jwsCompactSerializer = new CompactSerializer(); @@ -136,7 +135,7 @@ public static function getVapidHeaders(string $audience, string $subject, string ->build(); $jwt = $jwsCompactSerializer->serialize($jws, 0); - $encodedPublicKey = Base64Url::encode($publicKey); + $encodedPublicKey = Utils::base64Encode($publicKey); if ($contentEncoding === "aesgcm") { return [ @@ -175,8 +174,8 @@ public static function createVapidKeys(): array } return [ - 'publicKey' => Base64Url::encode($binaryPublicKey), - 'privateKey' => Base64Url::encode($binaryPrivateKey), + 'publicKey' => Utils::base64Encode($binaryPublicKey), + 'privateKey' => Utils::base64Encode($binaryPrivateKey), ]; } } diff --git a/src/WebPush.php b/src/WebPush.php index 68de138..6c2705b 100644 --- a/src/WebPush.php +++ b/src/WebPush.php @@ -13,7 +13,6 @@ namespace Minishlink\WebPush; -use Base64Url\Base64Url; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; @@ -208,8 +207,8 @@ protected function prepare(array $notifications): array ]; if ($contentEncoding === "aesgcm") { - $headers['Encryption'] = 'salt='.Base64Url::encode($salt); - $headers['Crypto-Key'] = 'dh='.Base64Url::encode($localPublicKey); + $headers['Encryption'] = 'salt='.Utils::base64Encode($salt); + $headers['Crypto-Key'] = 'dh='.Utils::base64Encode($localPublicKey); } $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding); diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php index 68e1ca9..995603a 100644 --- a/tests/EncryptionTest.php +++ b/tests/EncryptionTest.php @@ -8,7 +8,6 @@ * file that was distributed with this source code. */ -use Base64Url\Base64Url; use Jose\Component\Core\JWK; use Minishlink\WebPush\Encryption; use Minishlink\WebPush\Utils; @@ -23,30 +22,30 @@ public function testDeterministicEncrypt(): void { $contentEncoding = "aes128gcm"; $plaintext = 'When I grow up, I want to be a watermelon'; - $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Base64Url::encode($plaintext)); + $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Utils::base64Encode($plaintext)); $payload = Encryption::padPayload($plaintext, 0, $contentEncoding); - $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Base64Url::encode($payload)); + $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Utils::base64Encode($payload)); $userPublicKey = 'BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4'; $userAuthToken = 'BTBZMqHH6r4Tts7J_aSIgg'; - $localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); - $salt = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlw'); + $localPublicKey = Utils::base64Decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $salt = Utils::base64Decode('DGv6ra1nlYgDCS1FRnbzlw'); [$localPublicKeyObjectX, $localPublicKeyObjectY] = Utils::unserializePublicKey($localPublicKey); $localJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', 'd' => 'yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw', - 'x' => Base64Url::encode($localPublicKeyObjectX), - 'y' => Base64Url::encode($localPublicKeyObjectY), + 'x' => Utils::base64Encode($localPublicKeyObjectX), + 'y' => Utils::base64Encode($localPublicKeyObjectY), ]); $expected = [ 'localPublicKey' => $localPublicKey, 'salt' => $salt, - 'cipherText' => Base64Url::decode('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrd irN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'), + 'cipherText' => Utils::base64Decode('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrd irN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'), ]; $result = Encryption::deterministicEncrypt( @@ -59,17 +58,17 @@ public function testDeterministicEncrypt(): void ); $this->assertEquals(Utils::safeStrlen($expected['cipherText']), Utils::safeStrlen($result['cipherText'])); - $this->assertEquals(Base64Url::encode($expected['cipherText']), Base64Url::encode($result['cipherText'])); + $this->assertEquals(Utils::base64Encode($expected['cipherText']), Utils::base64Encode($result['cipherText'])); $this->assertEquals($expected, $result); } public function testGetContentCodingHeader(): void { - $localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); - $salt = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlw'); + $localPublicKey = Utils::base64Decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $salt = Utils::base64Decode('DGv6ra1nlYgDCS1FRnbzlw'); $result = Encryption::getContentCodingHeader($salt, $localPublicKey, "aes128gcm"); - $expected = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $expected = Utils::base64Decode('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); $this->assertEquals(Utils::safeStrlen($expected), Utils::safeStrlen($result)); $this->assertEquals($expected, $result); From 905c6bfa5d28df672b616f0a48aef0c9ba426f5b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 28 Feb 2024 13:26:41 +0100 Subject: [PATCH 2/3] Directly using the `Base64UrlSafe` class --- src/Encryption.php | 21 +++++++++++---------- src/Utils.php | 14 ++------------ src/VAPID.php | 21 +++++++++++---------- src/WebPush.php | 5 +++-- tests/EncryptionTest.php | 23 ++++++++++++----------- 5 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/Encryption.php b/src/Encryption.php index c2110a8..86868dc 100644 --- a/src/Encryption.php +++ b/src/Encryption.php @@ -16,6 +16,7 @@ use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Ecc\PrivateKey; use Jose\Component\Core\Util\ECKey; +use ParagonIE\ConstantTime\Base64UrlSafe; class Encryption { @@ -65,8 +66,8 @@ public static function encrypt(string $payload, string $userPublicKey, string $u */ public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array { - $userPublicKey = Utils::base64Decode($userPublicKey); - $userAuthToken = Utils::base64Decode($userAuthToken); + $userPublicKey = Base64UrlSafe::decodeNoPadding($userPublicKey); + $userAuthToken = Base64UrlSafe::decodeNoPadding($userAuthToken); // get local key pair if (count($localKeyObject) === 1) { @@ -80,9 +81,9 @@ public static function deterministicEncrypt(string $payload, string $userPublicK $localJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'd' => Utils::base64Encode($localPrivateKeyObject->getSecret()->toBytes(false)), - 'x' => Utils::base64Encode($localPublicKeyObject[0]), - 'y' => Utils::base64Encode($localPublicKeyObject[1]), + 'd' => Base64UrlSafe::encodeUnpadded($localPrivateKeyObject->getSecret()->toBytes(false)), + 'x' => Base64UrlSafe::encodeUnpadded($localPublicKeyObject[0]), + 'y' => Base64UrlSafe::encodeUnpadded($localPublicKeyObject[1]), ]); } if (!$localPublicKey) { @@ -94,8 +95,8 @@ public static function deterministicEncrypt(string $payload, string $userPublicK $userJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Utils::base64Encode($userPublicKeyObjectX), - 'y' => Utils::base64Encode($userPublicKeyObjectY), + 'x' => Base64UrlSafe::encodeUnpadded($userPublicKeyObjectX), + 'y' => Base64UrlSafe::encodeUnpadded($userPublicKeyObjectY), ]); // get shared secret from user public key and local private key @@ -251,9 +252,9 @@ private static function createLocalKeyObject(): array new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Utils::base64Encode(self::addNullPadding($details['ec']['x'])), - 'y' => Utils::base64Encode(self::addNullPadding($details['ec']['y'])), - 'd' => Utils::base64Encode(self::addNullPadding($details['ec']['d'])), + 'x' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['x'])), + 'y' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['y'])), + 'd' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['d'])), ]), ]; } diff --git a/src/Utils.php b/src/Utils.php index 0a39c4a..c34572e 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -37,8 +37,8 @@ public static function serializePublicKey(PublicKey $publicKey): string public static function serializePublicKeyFromJWK(JWK $jwk): string { $hexString = '04'; - $hexString .= str_pad(bin2hex(Utils::base64Decode($jwk->get('x'))), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(bin2hex(Utils::base64Decode($jwk->get('y'))), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad(bin2hex(Base64UrlSafe::decodeNoPadding($jwk->get('x'))), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad(bin2hex(Base64UrlSafe::decodeNoPadding($jwk->get('y'))), 64, '0', STR_PAD_LEFT); return $hexString; } @@ -58,16 +58,6 @@ public static function unserializePublicKey(string $data): array ]; } - public static function base64Decode(string $string): string - { - return Base64UrlSafe::decodeNoPadding(strtr($string, '-_', '+/')); - } - - public static function base64Encode(string $string): string - { - return strtr(Base64UrlSafe::encodeUnpadded($string), '+/', '-_'); - } - /** * Generates user warning/notice if some requirements are not met. * Does not throw exception to allow unusual or polyfill environments. diff --git a/src/VAPID.php b/src/VAPID.php index e42e87e..4ba3de9 100644 --- a/src/VAPID.php +++ b/src/VAPID.php @@ -19,6 +19,7 @@ use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer; +use ParagonIE\ConstantTime\Base64UrlSafe; class VAPID { @@ -53,14 +54,14 @@ public static function validate(array $vapid): array throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); } $vapid['publicKey'] = base64_encode($binaryPublicKey); - $vapid['privateKey'] = base64_encode(str_pad(Utils::base64Decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + $vapid['privateKey'] = base64_encode(str_pad(Base64UrlSafe::decodeNoPadding($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); } if (!isset($vapid['publicKey'])) { throw new \ErrorException('[VAPID] You must provide a public key.'); } - $publicKey = Utils::base64Decode($vapid['publicKey']); + $publicKey = Base64UrlSafe::decodeNoPadding($vapid['publicKey']); if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) { throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.'); @@ -70,7 +71,7 @@ public static function validate(array $vapid): array throw new \ErrorException('[VAPID] You must provide a private key.'); } - $privateKey = Utils::base64Decode($vapid['privateKey']); + $privateKey = Base64UrlSafe::decodeNoPadding($vapid['privateKey']); if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) { throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.'); @@ -121,9 +122,9 @@ public static function getVapidHeaders(string $audience, string $subject, string $jwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Utils::base64Encode($x), - 'y' => Utils::base64Encode($y), - 'd' => Utils::base64Encode($privateKey), + 'x' => Base64UrlSafe::encodeUnpadded($x), + 'y' => Base64UrlSafe::encodeUnpadded($y), + 'd' => Base64UrlSafe::encodeUnpadded($privateKey), ]); $jwsCompactSerializer = new CompactSerializer(); @@ -135,7 +136,7 @@ public static function getVapidHeaders(string $audience, string $subject, string ->build(); $jwt = $jwsCompactSerializer->serialize($jws, 0); - $encodedPublicKey = Utils::base64Encode($publicKey); + $encodedPublicKey = Base64UrlSafe::encodeUnpadded($publicKey); if ($contentEncoding === "aesgcm") { return [ @@ -168,14 +169,14 @@ public static function createVapidKeys(): array throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); } - $binaryPrivateKey = hex2bin(str_pad(bin2hex(Base64Url::decode($jwk->get('d'))), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + $binaryPrivateKey = hex2bin(str_pad(bin2hex(Base64UrlSafe::decodeNoPadding($jwk->get('d'))), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); if (!$binaryPrivateKey) { throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary'); } return [ - 'publicKey' => Utils::base64Encode($binaryPublicKey), - 'privateKey' => Utils::base64Encode($binaryPrivateKey), + 'publicKey' => Base64UrlSafe::encodeUnpadded($binaryPublicKey), + 'privateKey' => Base64UrlSafe::encodeUnpadded($binaryPrivateKey), ]; } } diff --git a/src/WebPush.php b/src/WebPush.php index 6c2705b..4f20faa 100644 --- a/src/WebPush.php +++ b/src/WebPush.php @@ -16,6 +16,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; +use ParagonIE\ConstantTime\Base64UrlSafe; use Psr\Http\Message\ResponseInterface; class WebPush @@ -207,8 +208,8 @@ protected function prepare(array $notifications): array ]; if ($contentEncoding === "aesgcm") { - $headers['Encryption'] = 'salt='.Utils::base64Encode($salt); - $headers['Crypto-Key'] = 'dh='.Utils::base64Encode($localPublicKey); + $headers['Encryption'] = 'salt='.Base64UrlSafe::encodeUnpadded($salt); + $headers['Crypto-Key'] = 'dh='.Base64UrlSafe::encodeUnpadded($localPublicKey); } $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding); diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php index 995603a..c7af076 100644 --- a/tests/EncryptionTest.php +++ b/tests/EncryptionTest.php @@ -11,6 +11,7 @@ use Jose\Component\Core\JWK; use Minishlink\WebPush\Encryption; use Minishlink\WebPush\Utils; +use ParagonIE\ConstantTime\Base64UrlSafe; use PHPUnit\Framework\Attributes\DataProvider; /** @@ -22,30 +23,30 @@ public function testDeterministicEncrypt(): void { $contentEncoding = "aes128gcm"; $plaintext = 'When I grow up, I want to be a watermelon'; - $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Utils::base64Encode($plaintext)); + $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Base64UrlSafe::encodeUnpadded($plaintext)); $payload = Encryption::padPayload($plaintext, 0, $contentEncoding); - $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Utils::base64Encode($payload)); + $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Base64UrlSafe::encodeUnpadded($payload)); $userPublicKey = 'BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4'; $userAuthToken = 'BTBZMqHH6r4Tts7J_aSIgg'; - $localPublicKey = Utils::base64Decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); - $salt = Utils::base64Decode('DGv6ra1nlYgDCS1FRnbzlw'); + $localPublicKey = Base64UrlSafe::decodeNoPadding('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $salt = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlw'); [$localPublicKeyObjectX, $localPublicKeyObjectY] = Utils::unserializePublicKey($localPublicKey); $localJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', 'd' => 'yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw', - 'x' => Utils::base64Encode($localPublicKeyObjectX), - 'y' => Utils::base64Encode($localPublicKeyObjectY), + 'x' => Base64UrlSafe::encodeUnpadded($localPublicKeyObjectX), + 'y' => Base64UrlSafe::encodeUnpadded($localPublicKeyObjectY), ]); $expected = [ 'localPublicKey' => $localPublicKey, 'salt' => $salt, - 'cipherText' => Utils::base64Decode('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrd irN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'), + 'cipherText' => Base64UrlSafe::decodeNoPadding('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrdirN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'), ]; $result = Encryption::deterministicEncrypt( @@ -58,17 +59,17 @@ public function testDeterministicEncrypt(): void ); $this->assertEquals(Utils::safeStrlen($expected['cipherText']), Utils::safeStrlen($result['cipherText'])); - $this->assertEquals(Utils::base64Encode($expected['cipherText']), Utils::base64Encode($result['cipherText'])); + $this->assertEquals(Base64UrlSafe::encodeUnpadded($expected['cipherText']), Base64UrlSafe::encodeUnpadded($result['cipherText'])); $this->assertEquals($expected, $result); } public function testGetContentCodingHeader(): void { - $localPublicKey = Utils::base64Decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); - $salt = Utils::base64Decode('DGv6ra1nlYgDCS1FRnbzlw'); + $localPublicKey = Base64UrlSafe::decodeNoPadding('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $salt = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlw'); $result = Encryption::getContentCodingHeader($salt, $localPublicKey, "aes128gcm"); - $expected = Utils::base64Decode('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $expected = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); $this->assertEquals(Utils::safeStrlen($expected), Utils::safeStrlen($result)); $this->assertEquals($expected, $result); From c6ae82453bfcafadf439ff1fb4a01c0bcd5d5ba9 Mon Sep 17 00:00:00 2001 From: Olaf Braun Date: Wed, 28 Feb 2024 15:07:52 +0100 Subject: [PATCH 3/3] Add composer package `paragonie/constant_time_encoding` --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4c8f7e7..da1bdf5 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "ext-mbstring": "*", "ext-openssl": "*", "guzzlehttp/guzzle": "^7.4.5", - "web-token/jwt-library": "^3.3.0" + "web-token/jwt-library": "^3.3.0", + "paragonie/constant_time_encoding": "^2.6" }, "suggest": { "ext-bcmath": "Optional for performance.",