diff --git a/composer.json b/composer.json index da1bdf5..b9b34b9 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "ext-openssl": "*", "guzzlehttp/guzzle": "^7.4.5", "web-token/jwt-library": "^3.3.0", - "paragonie/constant_time_encoding": "^2.6" + "spomky-labs/base64url": "^2.0.4" }, "suggest": { "ext-bcmath": "Optional for performance.", @@ -51,4 +51,4 @@ "Minishlink\\WebPush\\": "src" } } -} +} \ No newline at end of file diff --git a/src/Encryption.php b/src/Encryption.php index 86868dc..e6bb708 100644 --- a/src/Encryption.php +++ b/src/Encryption.php @@ -13,10 +13,10 @@ namespace Minishlink\WebPush; +use Base64Url\Base64Url; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Ecc\PrivateKey; use Jose\Component\Core\Util\ECKey; -use ParagonIE\ConstantTime\Base64UrlSafe; class Encryption { @@ -66,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 = Base64UrlSafe::decodeNoPadding($userPublicKey); - $userAuthToken = Base64UrlSafe::decodeNoPadding($userAuthToken); + $userPublicKey = Base64Url::decode($userPublicKey); + $userAuthToken = Base64Url::decode($userAuthToken); // get local key pair if (count($localKeyObject) === 1) { @@ -81,9 +81,9 @@ public static function deterministicEncrypt(string $payload, string $userPublicK $localJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'd' => Base64UrlSafe::encodeUnpadded($localPrivateKeyObject->getSecret()->toBytes(false)), - 'x' => Base64UrlSafe::encodeUnpadded($localPublicKeyObject[0]), - 'y' => Base64UrlSafe::encodeUnpadded($localPublicKeyObject[1]), + 'd' => Base64Url::encode($localPrivateKeyObject->getSecret()->toBytes(false)), + 'x' => Base64Url::encode($localPublicKeyObject[0]), + 'y' => Base64Url::encode($localPublicKeyObject[1]), ]); } if (!$localPublicKey) { @@ -95,8 +95,8 @@ public static function deterministicEncrypt(string $payload, string $userPublicK $userJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Base64UrlSafe::encodeUnpadded($userPublicKeyObjectX), - 'y' => Base64UrlSafe::encodeUnpadded($userPublicKeyObjectY), + 'x' => Base64Url::encode($userPublicKeyObjectX), + 'y' => Base64Url::encode($userPublicKeyObjectY), ]); // get shared secret from user public key and local private key @@ -252,9 +252,9 @@ private static function createLocalKeyObject(): array new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['x'])), - 'y' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['y'])), - 'd' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['d'])), + 'x' => Base64Url::encode(self::addNullPadding($details['ec']['x'])), + 'y' => Base64Url::encode(self::addNullPadding($details['ec']['y'])), + 'd' => Base64Url::encode(self::addNullPadding($details['ec']['d'])), ]), ]; } diff --git a/src/Utils.php b/src/Utils.php index c34572e..887acb0 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(Base64UrlSafe::decodeNoPadding($jwk->get('x'))), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(bin2hex(Base64UrlSafe::decodeNoPadding($jwk->get('y'))), 64, '0', STR_PAD_LEFT); + $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); return $hexString; } diff --git a/src/VAPID.php b/src/VAPID.php index 4ba3de9..5a40cd9 100644 --- a/src/VAPID.php +++ b/src/VAPID.php @@ -13,13 +13,13 @@ namespace Minishlink\WebPush; +use Base64Url\Base64Url; use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\JWK; use Jose\Component\KeyManagement\JWKFactory; use Jose\Component\Signature\Algorithm\ES256; use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer; -use ParagonIE\ConstantTime\Base64UrlSafe; class VAPID { @@ -54,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(Base64UrlSafe::decodeNoPadding($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($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 = Base64UrlSafe::decodeNoPadding($vapid['publicKey']); + $publicKey = Base64Url::decode($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 +71,7 @@ public static function validate(array $vapid): array throw new \ErrorException('[VAPID] You must provide a private key.'); } - $privateKey = Base64UrlSafe::decodeNoPadding($vapid['privateKey']); + $privateKey = Base64Url::decode($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 +122,9 @@ public static function getVapidHeaders(string $audience, string $subject, string $jwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', - 'x' => Base64UrlSafe::encodeUnpadded($x), - 'y' => Base64UrlSafe::encodeUnpadded($y), - 'd' => Base64UrlSafe::encodeUnpadded($privateKey), + 'x' => Base64Url::encode($x), + 'y' => Base64Url::encode($y), + 'd' => Base64Url::encode($privateKey), ]); $jwsCompactSerializer = new CompactSerializer(); @@ -136,7 +136,7 @@ public static function getVapidHeaders(string $audience, string $subject, string ->build(); $jwt = $jwsCompactSerializer->serialize($jws, 0); - $encodedPublicKey = Base64UrlSafe::encodeUnpadded($publicKey); + $encodedPublicKey = Base64Url::encode($publicKey); if ($contentEncoding === "aesgcm") { return [ @@ -169,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(Base64UrlSafe::decodeNoPadding($jwk->get('d'))), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + $binaryPrivateKey = hex2bin(str_pad(bin2hex(Base64Url::decode($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' => Base64UrlSafe::encodeUnpadded($binaryPublicKey), - 'privateKey' => Base64UrlSafe::encodeUnpadded($binaryPrivateKey), + 'publicKey' => Base64Url::encode($binaryPublicKey), + 'privateKey' => Base64Url::encode($binaryPrivateKey), ]; } } diff --git a/src/WebPush.php b/src/WebPush.php index 47ca09c..8739a98 100644 --- a/src/WebPush.php +++ b/src/WebPush.php @@ -13,11 +13,11 @@ namespace Minishlink\WebPush; +use Base64Url\Base64Url; use GuzzleHttp\Client; use GuzzleHttp\Pool; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; -use ParagonIE\ConstantTime\Base64UrlSafe; use Psr\Http\Message\ResponseInterface; class WebPush @@ -261,8 +261,8 @@ protected function prepare(array $notifications): array ]; if ($contentEncoding === "aesgcm") { - $headers['Encryption'] = 'salt='.Base64UrlSafe::encodeUnpadded($salt); - $headers['Crypto-Key'] = 'dh='.Base64UrlSafe::encodeUnpadded($localPublicKey); + $headers['Encryption'] = 'salt='.Base64Url::encode($salt); + $headers['Crypto-Key'] = 'dh='.Base64Url::encode($localPublicKey); } $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding); diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php index c7af076..68e1ca9 100644 --- a/tests/EncryptionTest.php +++ b/tests/EncryptionTest.php @@ -8,10 +8,10 @@ * file that was distributed with this source code. */ +use Base64Url\Base64Url; use Jose\Component\Core\JWK; use Minishlink\WebPush\Encryption; use Minishlink\WebPush\Utils; -use ParagonIE\ConstantTime\Base64UrlSafe; use PHPUnit\Framework\Attributes\DataProvider; /** @@ -23,30 +23,30 @@ public function testDeterministicEncrypt(): void { $contentEncoding = "aes128gcm"; $plaintext = 'When I grow up, I want to be a watermelon'; - $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Base64UrlSafe::encodeUnpadded($plaintext)); + $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24', Base64Url::encode($plaintext)); $payload = Encryption::padPayload($plaintext, 0, $contentEncoding); - $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Base64UrlSafe::encodeUnpadded($payload)); + $this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Base64Url::encode($payload)); $userPublicKey = 'BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4'; $userAuthToken = 'BTBZMqHH6r4Tts7J_aSIgg'; - $localPublicKey = Base64UrlSafe::decodeNoPadding('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); - $salt = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlw'); + $localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $salt = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlw'); [$localPublicKeyObjectX, $localPublicKeyObjectY] = Utils::unserializePublicKey($localPublicKey); $localJwk = new JWK([ 'kty' => 'EC', 'crv' => 'P-256', 'd' => 'yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw', - 'x' => Base64UrlSafe::encodeUnpadded($localPublicKeyObjectX), - 'y' => Base64UrlSafe::encodeUnpadded($localPublicKeyObjectY), + 'x' => Base64Url::encode($localPublicKeyObjectX), + 'y' => Base64Url::encode($localPublicKeyObjectY), ]); $expected = [ 'localPublicKey' => $localPublicKey, 'salt' => $salt, - 'cipherText' => Base64UrlSafe::decodeNoPadding('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrdirN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'), + 'cipherText' => Base64Url::decode('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrd irN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'), ]; $result = Encryption::deterministicEncrypt( @@ -59,17 +59,17 @@ public function testDeterministicEncrypt(): void ); $this->assertEquals(Utils::safeStrlen($expected['cipherText']), Utils::safeStrlen($result['cipherText'])); - $this->assertEquals(Base64UrlSafe::encodeUnpadded($expected['cipherText']), Base64UrlSafe::encodeUnpadded($result['cipherText'])); + $this->assertEquals(Base64Url::encode($expected['cipherText']), Base64Url::encode($result['cipherText'])); $this->assertEquals($expected, $result); } public function testGetContentCodingHeader(): void { - $localPublicKey = Base64UrlSafe::decodeNoPadding('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); - $salt = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlw'); + $localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $salt = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlw'); $result = Encryption::getContentCodingHeader($salt, $localPublicKey, "aes128gcm"); - $expected = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); + $expected = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8'); $this->assertEquals(Utils::safeStrlen($expected), Utils::safeStrlen($result)); $this->assertEquals($expected, $result);