Skip to content

Commit

Permalink
Replace spomky-labs/base64url with `paragonie/constant_time_encodin…
Browse files Browse the repository at this point in the history
…g` (#397)

* Replace `spomky-labs/base64url` with `paragonie/constant_time_encoding`

* Directly using the `Base64UrlSafe` class

* Add composer package `paragonie/constant_time_encoding`
  • Loading branch information
Cyperghost committed Mar 4, 2024
1 parent 410b1fa commit 2c24f4c
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 42 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"ext-openssl": "*",
"guzzlehttp/guzzle": "^7.4.5",
"web-token/jwt-library": "^3.3.0",
"spomky-labs/base64url": "^2.0.4"
"paragonie/constant_time_encoding": "^2.6"
},
"suggest": {
"ext-bcmath": "Optional for performance.",
Expand All @@ -51,4 +51,4 @@
"Minishlink\\WebPush\\": "src"
}
}
}
}
22 changes: 11 additions & 11 deletions src/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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 = Base64Url::decode($userPublicKey);
$userAuthToken = Base64Url::decode($userAuthToken);
$userPublicKey = Base64UrlSafe::decodeNoPadding($userPublicKey);
$userAuthToken = Base64UrlSafe::decodeNoPadding($userAuthToken);

// get local key pair
if (count($localKeyObject) === 1) {
Expand All @@ -81,9 +81,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' => Base64UrlSafe::encodeUnpadded($localPrivateKeyObject->getSecret()->toBytes(false)),
'x' => Base64UrlSafe::encodeUnpadded($localPublicKeyObject[0]),
'y' => Base64UrlSafe::encodeUnpadded($localPublicKeyObject[1]),
]);
}
if (!$localPublicKey) {
Expand All @@ -95,8 +95,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' => Base64UrlSafe::encodeUnpadded($userPublicKeyObjectX),
'y' => Base64UrlSafe::encodeUnpadded($userPublicKeyObjectY),
]);

// get shared secret from user public key and local private key
Expand Down Expand Up @@ -252,9 +252,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' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['x'])),
'y' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['y'])),
'd' => Base64UrlSafe::encodeUnpadded(self::addNullPadding($details['ec']['d'])),
]),
];
}
Expand Down
6 changes: 3 additions & 3 deletions src/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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(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;
}
Expand Down
22 changes: 11 additions & 11 deletions src/VAPID.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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(Base64Url::decode($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 = Base64Url::decode($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.');
Expand All @@ -71,7 +71,7 @@ public static function validate(array $vapid): array
throw new \ErrorException('[VAPID] You must provide a private key.');
}

$privateKey = Base64Url::decode($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.');
Expand Down Expand Up @@ -122,9 +122,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' => Base64UrlSafe::encodeUnpadded($x),
'y' => Base64UrlSafe::encodeUnpadded($y),
'd' => Base64UrlSafe::encodeUnpadded($privateKey),
]);

$jwsCompactSerializer = new CompactSerializer();
Expand All @@ -136,7 +136,7 @@ public static function getVapidHeaders(string $audience, string $subject, string
->build();

$jwt = $jwsCompactSerializer->serialize($jws, 0);
$encodedPublicKey = Base64Url::encode($publicKey);
$encodedPublicKey = Base64UrlSafe::encodeUnpadded($publicKey);

if ($contentEncoding === "aesgcm") {
return [
Expand Down Expand Up @@ -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(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' => Base64Url::encode($binaryPublicKey),
'privateKey' => Base64Url::encode($binaryPrivateKey),
'publicKey' => Base64UrlSafe::encodeUnpadded($binaryPublicKey),
'privateKey' => Base64UrlSafe::encodeUnpadded($binaryPrivateKey),
];
}
}
6 changes: 3 additions & 3 deletions src/WebPush.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@

namespace Minishlink\WebPush;

use Base64Url\Base64Url;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Psr\Http\Message\ResponseInterface;

class WebPush
Expand Down Expand Up @@ -208,8 +208,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='.Base64UrlSafe::encodeUnpadded($salt);
$headers['Crypto-Key'] = 'dh='.Base64UrlSafe::encodeUnpadded($localPublicKey);
}

$encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding);
Expand Down
24 changes: 12 additions & 12 deletions tests/EncryptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -23,30 +23,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', Base64UrlSafe::encodeUnpadded($plaintext));

$payload = Encryption::padPayload($plaintext, 0, $contentEncoding);
$this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Base64Url::encode($payload));
$this->assertEquals('V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24C', Base64UrlSafe::encodeUnpadded($payload));

$userPublicKey = 'BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4';
$userAuthToken = 'BTBZMqHH6r4Tts7J_aSIgg';

$localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8');
$salt = Base64Url::decode('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' => Base64Url::encode($localPublicKeyObjectX),
'y' => Base64Url::encode($localPublicKeyObjectY),
'x' => Base64UrlSafe::encodeUnpadded($localPublicKeyObjectX),
'y' => Base64UrlSafe::encodeUnpadded($localPublicKeyObjectY),
]);

$expected = [
'localPublicKey' => $localPublicKey,
'salt' => $salt,
'cipherText' => Base64Url::decode('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrd irN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'),
'cipherText' => Base64UrlSafe::decodeNoPadding('8pfeW0KbunFT06SuDKoJH9Ql87S1QUrdirN6GcG7sFz1y1sqLgVi1VhjVkHsUoEsbI_0LpXMuGvnzQ'),
];

$result = Encryption::deterministicEncrypt(
Expand All @@ -59,17 +59,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(Base64UrlSafe::encodeUnpadded($expected['cipherText']), Base64UrlSafe::encodeUnpadded($result['cipherText']));
$this->assertEquals($expected, $result);
}

public function testGetContentCodingHeader(): void
{
$localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8');
$salt = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlw');
$localPublicKey = Base64UrlSafe::decodeNoPadding('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8');
$salt = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlw');

$result = Encryption::getContentCodingHeader($salt, $localPublicKey, "aes128gcm");
$expected = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8');
$expected = Base64UrlSafe::decodeNoPadding('DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8');

$this->assertEquals(Utils::safeStrlen($expected), Utils::safeStrlen($result));
$this->assertEquals($expected, $result);
Expand Down

0 comments on commit 2c24f4c

Please sign in to comment.