Skip to content

Commit 20808ab

Browse files
authored
Hotfix for web-token internal class used by the library (#289)
* Hotfix for web-token internal class used by the library * Native scalar mul * Native scalar mul * Fix composer error * PHPStan config and bugs fixed
1 parent 50e38b9 commit 20808ab

11 files changed

+161
-59
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ phpunit.xml
66
cli.log
77
module.log
88
.vagrant
9-
Vagrantfile # temp, may be?
9+
Vagrantfile # temp, may be?
10+
/.phpunit.result.cache

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ env:
4040
- TRAVIS_NODE_VERSION="stable"
4141

4242
before_install:
43+
- composer config discard-changes true
4344
- nvm install node
4445

4546
install:

composer.json

+3-4
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@
1414
],
1515
"scripts": {
1616
"test:unit": "./vendor/bin/phpunit --color",
17-
"test:typing": "./vendor/bin/phpstan analyse --level max src",
17+
"test:typing": "./vendor/bin/phpstan analyse",
1818
"test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no"
1919
},
2020
"require": {
2121
"php": "^7.2",
2222
"ext-curl": "*",
23-
"ext-gmp": "*",
2423
"ext-json": "*",
2524
"ext-mbstring": "*",
2625
"ext-openssl": "*",
@@ -31,8 +30,8 @@
3130
"web-token/jwt-util-ecc": "^2.0"
3231
},
3332
"require-dev": {
34-
"phpunit/phpunit": "^7.0",
35-
"phpstan/phpstan": "0.11.19",
33+
"phpunit/phpunit": "^8.0|^9.0",
34+
"phpstan/phpstan": "^0.11|^0.12",
3635
"friendsofphp/php-cs-fixer": "^2.14"
3736
},
3837
"autoload": {

phpstan.neon

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 7
3+
paths:
4+
- src
5+
checkMissingIterableValueType: false

src/Encryption.php

+101-20
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
namespace Minishlink\WebPush;
1515

1616
use Base64Url\Base64Url;
17+
use Brick\Math\BigInteger;
18+
use Jose\Component\Core\JWK;
19+
use Jose\Component\Core\Util\Ecc\Curve;
1720
use Jose\Component\Core\Util\Ecc\NistCurve;
18-
use Jose\Component\Core\Util\Ecc\Point;
1921
use Jose\Component\Core\Util\Ecc\PrivateKey;
20-
use Jose\Component\Core\Util\Ecc\PublicKey;
22+
use Jose\Component\Core\Util\ECKey;
2123

2224
class Encryption
2325
{
@@ -82,25 +84,41 @@ public static function deterministicEncrypt(string $payload, string $userPublicK
8284
$userPublicKey = Base64Url::decode($userPublicKey);
8385
$userAuthToken = Base64Url::decode($userAuthToken);
8486

85-
$curve = NistCurve::curve256();
86-
8787
// get local key pair
88-
list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject;
89-
$localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject));
88+
if (count($localKeyObject) === 1) {
89+
/** @var JWK $localJwk */
90+
$localJwk = current($localKeyObject);
91+
$localPublicKey = hex2bin(Utils::serializePublicKeyFromJWK($localJwk));
92+
} else {
93+
/** @var PrivateKey $localPrivateKeyObject */
94+
list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject;
95+
$localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject));
96+
$localJwk = new JWK([
97+
'kty' => 'EC',
98+
'crv' => 'P-256',
99+
'd' => $localPrivateKeyObject->getSecret()->getX(), // @phpstan-ignore-line
100+
'x' => Base64Url::encode($localPublicKeyObject[0]),
101+
'y' => Base64Url::encode($localPublicKeyObject[1]),
102+
]);
103+
}
90104
if (!$localPublicKey) {
91105
throw new \ErrorException('Failed to convert local public key from hexadecimal to binary');
92106
}
93107

94108
// get user public key object
95109
[$userPublicKeyObjectX, $userPublicKeyObjectY] = Utils::unserializePublicKey($userPublicKey);
96-
$userPublicKeyObject = $curve->getPublicKeyFrom(
97-
gmp_init(bin2hex($userPublicKeyObjectX), 16),
98-
gmp_init(bin2hex($userPublicKeyObjectY), 16)
99-
);
110+
$userJwk = new JWK([
111+
'kty' => 'EC',
112+
'crv' => 'P-256',
113+
'x' => Base64Url::encode($userPublicKeyObjectX),
114+
'y' => Base64Url::encode($userPublicKeyObjectY),
115+
]);
100116

101117
// get shared secret from user public key and local private key
102-
$sharedSecret = $curve->mul($userPublicKeyObject->getPoint(), $localPrivateKeyObject->getSecret())->getX();
103-
$sharedSecret = hex2bin(str_pad(gmp_strval($sharedSecret, 16), 64, '0', STR_PAD_LEFT));
118+
119+
$sharedSecret = self::calculateAgreementKey($localJwk, $userJwk);
120+
121+
$sharedSecret = str_pad($sharedSecret, 32, chr(0), STR_PAD_LEFT);
104122
if (!$sharedSecret) {
105123
throw new \ErrorException('Failed to convert shared secret from hexadecimal to binary');
106124
}
@@ -132,7 +150,7 @@ public static function deterministicEncrypt(string $payload, string $userPublicK
132150
];
133151
}
134152

135-
public static function getContentCodingHeader($salt, $localPublicKey, $contentEncoding): string
153+
public static function getContentCodingHeader(string $salt, string $localPublicKey, string $contentEncoding): string
136154
{
137155
if ($contentEncoding === "aes128gcm") {
138156
return $salt
@@ -186,7 +204,7 @@ private static function hkdf(string $salt, string $ikm, string $info, int $lengt
186204
*
187205
* @throws \ErrorException
188206
*/
189-
private static function createContext(string $clientPublicKey, string $serverPublicKey, $contentEncoding): ?string
207+
private static function createContext(string $clientPublicKey, string $serverPublicKey, string $contentEncoding): ?string
190208
{
191209
if ($contentEncoding === "aes128gcm") {
192210
return null;
@@ -256,9 +274,10 @@ private static function createLocalKeyObjectUsingPurePhpMethod(): array
256274
{
257275
$curve = NistCurve::curve256();
258276
$privateKey = $curve->createPrivateKey();
277+
$publicKey = $curve->createPublicKey($privateKey);
259278

260279
return [
261-
$curve->createPublicKey($privateKey),
280+
$publicKey,
262281
$privateKey,
263282
];
264283
}
@@ -285,11 +304,13 @@ private static function createLocalKeyObjectUsingOpenSSL(): array
285304
}
286305

287306
return [
288-
new PublicKey(Point::create(
289-
gmp_init(bin2hex($details['ec']['x']), 16),
290-
gmp_init(bin2hex($details['ec']['y']), 16)
291-
)),
292-
PrivateKey::create(gmp_init(bin2hex($details['ec']['d']), 16))
307+
new JWK([
308+
'kty' => 'EC',
309+
'crv' => 'P-256',
310+
'x' => Base64Url::encode($details['ec']['x']),
311+
'y' => Base64Url::encode($details['ec']['y']),
312+
'd' => Base64Url::encode($details['ec']['d']),
313+
])
293314
];
294315
}
295316

@@ -318,4 +339,64 @@ private static function getIKM(string $userAuthToken, string $userPublicKey, str
318339

319340
return $sharedSecret;
320341
}
342+
343+
private static function calculateAgreementKey(JWK $private_key, JWK $public_key): string
344+
{
345+
if (function_exists('openssl_pkey_derive')) {
346+
try {
347+
$publicPem = ECKey::convertPublicKeyToPEM($public_key);
348+
$privatePem = ECKey::convertPrivateKeyToPEM($private_key);
349+
350+
$result = openssl_pkey_derive($publicPem, $privatePem, 256); // @phpstan-ignore-line
351+
if ($result === false) {
352+
throw new \Exception('Unable to compute the agreement key');
353+
}
354+
return $result;
355+
} catch (\Throwable $throwable) {
356+
//Does nothing. Will fallback to the pure PHP function
357+
}
358+
}
359+
360+
361+
$curve = NistCurve::curve256();
362+
try {
363+
$rec_x = self::convertBase64ToBigInteger($public_key->get('x'));
364+
$rec_y = self::convertBase64ToBigInteger($public_key->get('y'));
365+
$sen_d = self::convertBase64ToBigInteger($private_key->get('d'));
366+
$priv_key = PrivateKey::create($sen_d);
367+
$pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
368+
369+
return hex2bin($curve->mul($pub_key->getPoint(), $priv_key->getSecret())->getX()->toBase(16)); // @phpstan-ignore-line
370+
} catch (\Throwable $e) {
371+
$rec_x = self::convertBase64ToGMP($public_key->get('x'));
372+
$rec_y = self::convertBase64ToGMP($public_key->get('y'));
373+
$sen_d = self::convertBase64ToGMP($private_key->get('d'));
374+
$priv_key = PrivateKey::create($sen_d); // @phpstan-ignore-line
375+
$pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y); // @phpstan-ignore-line
376+
377+
return hex2bin(gmp_strval($curve->mul($pub_key->getPoint(), $priv_key->getSecret())->getX(), 16)); // @phpstan-ignore-line
378+
}
379+
}
380+
381+
/**
382+
* @param string $value
383+
* @return BigInteger
384+
*/
385+
private static function convertBase64ToBigInteger(string $value): BigInteger
386+
{
387+
$value = unpack('H*', Base64Url::decode($value));
388+
389+
return BigInteger::fromBase($value[1], 16);
390+
}
391+
392+
/**
393+
* @param string $value
394+
* @return \GMP
395+
*/
396+
private static function convertBase64ToGMP(string $value): \GMP
397+
{
398+
$value = unpack('H*', Base64Url::decode($value));
399+
400+
return gmp_init($value[1], 16);
401+
}
321402
}

src/Utils.php

+25-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace Minishlink\WebPush;
1515

16+
use Base64Url\Base64Url;
17+
use Brick\Math\BigInteger;
18+
use Jose\Component\Core\JWK;
1619
use Jose\Component\Core\Util\Ecc\PublicKey;
1720

1821
class Utils
@@ -35,8 +38,28 @@ public static function safeStrlen(string $value): int
3538
public static function serializePublicKey(PublicKey $publicKey): string
3639
{
3740
$hexString = '04';
38-
$hexString .= str_pad(gmp_strval($publicKey->getPoint()->getX(), 16), 64, '0', STR_PAD_LEFT);
39-
$hexString .= str_pad(gmp_strval($publicKey->getPoint()->getY(), 16), 64, '0', STR_PAD_LEFT);
41+
$point = $publicKey->getPoint();
42+
if ($point->getX() instanceof BigInteger) {
43+
$hexString .= str_pad($point->getX()->toBase(16), 64, '0', STR_PAD_LEFT);
44+
$hexString .= str_pad($point->getY()->toBase(16), 64, '0', STR_PAD_LEFT);
45+
} else { // @phpstan-ignore-line
46+
$hexString .= str_pad(gmp_strval($point->getX(), 16), 64, '0', STR_PAD_LEFT);
47+
$hexString .= str_pad(gmp_strval($point->getY(), 16), 64, '0', STR_PAD_LEFT); // @phpstan-ignore-line
48+
}
49+
50+
return $hexString;
51+
}
52+
53+
/**
54+
* @param JWK $jwk
55+
*
56+
* @return string
57+
*/
58+
public static function serializePublicKeyFromJWK(JWK $jwk): string
59+
{
60+
$hexString = '04';
61+
$hexString .= str_pad(bin2hex(Base64Url::decode($jwk->get('x'))), 64, '0', STR_PAD_LEFT);
62+
$hexString .= str_pad(bin2hex(Base64Url::decode($jwk->get('y'))), 64, '0', STR_PAD_LEFT);
4063

4164
return $hexString;
4265
}

src/VAPID.php

+4-13
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
use Base64Url\Base64Url;
1717
use Jose\Component\Core\AlgorithmManager;
1818
use Jose\Component\Core\JWK;
19-
use Jose\Component\Core\Util\Ecc\NistCurve;
20-
use Jose\Component\Core\Util\Ecc\Point;
21-
use Jose\Component\Core\Util\Ecc\PublicKey;
2219
use Jose\Component\KeyManagement\JWKFactory;
2320
use Jose\Component\Signature\Algorithm\ES256;
2421
use Jose\Component\Signature\JWSBuilder;
@@ -55,12 +52,8 @@ public static function validate(array $vapid): array
5552
if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) {
5653
throw new \ErrorException('Invalid PEM data.');
5754
}
58-
$publicKey = new PublicKey(Point::create(
59-
gmp_init(bin2hex(Base64Url::decode($jwk->get('x'))), 16),
60-
gmp_init(bin2hex(Base64Url::decode($jwk->get('y'))), 16)
61-
));
6255

63-
$binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
56+
$binaryPublicKey = hex2bin(Utils::serializePublicKeyFromJWK($jwk));
6457
if (!$binaryPublicKey) {
6558
throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
6659
}
@@ -173,16 +166,14 @@ public static function getVapidHeaders(string $audience, string $subject, string
173166
*/
174167
public static function createVapidKeys(): array
175168
{
176-
$curve = NistCurve::curve256();
177-
$privateKey = $curve->createPrivateKey();
178-
$publicKey = $curve->createPublicKey($privateKey);
169+
$jwk = JWKFactory::createECKey('P-256');
179170

180-
$binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
171+
$binaryPublicKey = hex2bin(Utils::serializePublicKeyFromJWK($jwk));
181172
if (!$binaryPublicKey) {
182173
throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
183174
}
184175

185-
$binaryPrivateKey = hex2bin(str_pad(gmp_strval($privateKey->getSecret(), 16), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
176+
$binaryPrivateKey = hex2bin(str_pad(bin2hex(Base64Url::decode($jwk->get('d'))), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
186177
if (!$binaryPrivateKey) {
187178
throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary');
188179
}

tests/EncryptionTest.php

+11-9
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
*/
1313

1414
use Base64Url\Base64Url;
15+
use Jose\Component\Core\JWK;
1516
use Jose\Component\Core\Util\Ecc\NistCurve;
1617
use Jose\Component\Core\Util\Ecc\PrivateKey;
18+
use Jose\Component\KeyManagement\JWKFactory;
1719
use Minishlink\WebPush\Encryption;
1820
use Minishlink\WebPush\Utils;
1921

@@ -32,16 +34,16 @@ public function testDeterministicEncrypt()
3234
$userAuthToken = 'BTBZMqHH6r4Tts7J_aSIgg';
3335

3436
$localPublicKey = Base64Url::decode('BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8');
35-
$localPrivateKey = Base64Url::decode('yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw');
3637
$salt = Base64Url::decode('DGv6ra1nlYgDCS1FRnbzlw');
3738

38-
$localPrivateKeyObject = PrivateKey::create(gmp_init(bin2hex($localPrivateKey), 16));
39-
$curve = NistCurve::curve256();
4039
[$localPublicKeyObjectX, $localPublicKeyObjectY] = Utils::unserializePublicKey($localPublicKey);
41-
$localPublicKeyObject = $curve->getPublicKeyFrom(
42-
gmp_init(bin2hex($localPublicKeyObjectX), 16),
43-
gmp_init(bin2hex($localPublicKeyObjectY), 16)
44-
);
40+
$localJwk = new JWK([
41+
'kty' => 'EC',
42+
'crv' => 'P-256',
43+
'd' => 'yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw',
44+
'x' => Base64Url::encode($localPublicKeyObjectX),
45+
'y' => Base64Url::encode($localPublicKeyObjectY),
46+
]);
4547

4648
$expected = [
4749
'localPublicKey' => $localPublicKey,
@@ -54,7 +56,7 @@ public function testDeterministicEncrypt()
5456
$userPublicKey,
5557
$userAuthToken,
5658
$contentEncoding,
57-
[$localPublicKeyObject, $localPrivateKeyObject],
59+
[$localJwk],
5860
$salt
5961
);
6062

@@ -87,7 +89,7 @@ public function testPadPayload(string $payload, int $maxLengthToPad, int $expect
8789
{
8890
$res = Encryption::padPayload($payload, $maxLengthToPad, "aesgcm");
8991

90-
$this->assertContains('test', $res);
92+
$this->assertStringContainsString('test', $res);
9193
$this->assertEquals($expectedResLength, Utils::safeStrlen($res));
9294
}
9395

0 commit comments

Comments
 (0)