Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace spomky-labs/base64url with paragonie/constant_time_encoding #397

Merged
merged 3 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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