Skip to content

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

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

Merged
merged 3 commits into from
Mar 4, 2024
Merged
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
@@ -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.",
@@ -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
@@ -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 = Base64Url::decode($userPublicKey);
$userAuthToken = Base64Url::decode($userAuthToken);
$userPublicKey = Base64UrlSafe::decodeNoPadding($userPublicKey);
$userAuthToken = Base64UrlSafe::decodeNoPadding($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' => 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) {
@@ -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
@@ -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'])),
]),
];
}
6 changes: 3 additions & 3 deletions src/Utils.php
Original file line number Diff line number Diff line change
@@ -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(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;
}
22 changes: 11 additions & 11 deletions src/VAPID.php
Original file line number Diff line number Diff line change
@@ -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(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.');
@@ -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.');
@@ -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();
@@ -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 [
@@ -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
@@ -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
@@ -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);
24 changes: 12 additions & 12 deletions tests/EncryptionTest.php
Original file line number Diff line number Diff line change
@@ -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', 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(
@@ -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);