Skip to content

Commit 34491d0

Browse files
committed
work on aesgcm
1 parent f6312ae commit 34491d0

File tree

2 files changed

+157
-54
lines changed

2 files changed

+157
-54
lines changed

src/Encryption.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the WebPush library.
5+
*
6+
* (c) Louis Lagrange <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Minishlink\WebPush;
13+
14+
use Base64Url\Base64Url;
15+
use Mdanter\Ecc\Crypto\Key\PublicKey;
16+
use Mdanter\Ecc\EccFactory;
17+
use Mdanter\Ecc\Serializer\Point\UncompressedPointSerializer;
18+
19+
final class Encryption
20+
{
21+
/**
22+
* @param string $payload
23+
* @param string $userPublicKey MIME base 64 encoded
24+
* @param string $userAuthToken MIME base 64 encoded
25+
* @param bool $nativeEncryption Use OpenSSL (>PHP7.1)
26+
*
27+
* @return array
28+
*/
29+
public static function encrypt($payload, $userPublicKey, $userAuthToken, $nativeEncryption)
30+
{
31+
$userPublicKey = base64_decode($userPublicKey);
32+
$userAuthToken = base64_decode($userAuthToken);
33+
$plaintext = chr(0).chr(0).utf8_decode($payload);
34+
35+
// initialize utilities
36+
$math = EccFactory::getAdapter();
37+
$keySerializer = new UncompressedPointSerializer($math);
38+
$curveGenerator = EccFactory::getNistCurves()->generator256();
39+
$curve = EccFactory::getNistCurves()->curve256();
40+
41+
// get local key pair
42+
$localPrivateKeyObject = $curveGenerator->createPrivateKey();
43+
$localPublicKeyObject = $localPrivateKeyObject->getPublicKey();
44+
$localPublicKey = hex2bin($keySerializer->serialize($localPublicKeyObject->getPoint()));
45+
46+
// get user public key object
47+
$userPublicKeyObject = new PublicKey($math, $curveGenerator, $keySerializer->unserialize($curve, bin2hex($userPublicKey)));
48+
49+
// get shared secret from user public key and local private key
50+
$sharedSecret = hex2bin($math->decHex($userPublicKeyObject->getPoint()->mul($localPrivateKeyObject->getSecret())->getX()));
51+
52+
// generate salt
53+
$salt = openssl_random_pseudo_bytes(16);
54+
55+
$prk = !empty($userAuthToken) ?
56+
self::hkdf($userAuthToken, $sharedSecret, utf8_decode('Content-Encoding: auth\0'), 32) :
57+
$sharedSecret;
58+
59+
$context = self::createContext($userPublicKey, $localPublicKey);
60+
61+
// derive the Content Encryption Key
62+
$contentEncryptionKeyInfo = self::createInfo('aesgcm', $context);
63+
$contentEncryptionKey = self::hkdf($salt, $prk, $contentEncryptionKeyInfo, 16);
64+
65+
// derive the Nonce
66+
$nonceInfo = self::createInfo('nonce', $context);
67+
$nonce = self::hkdf($salt, $prk, $nonceInfo, 12);
68+
69+
// encrypt
70+
if (!$nativeEncryption) {
71+
list($encryptedText, $tag) = \Jose\Util\GCM::encrypt($contentEncryptionKey, $nonce, $plaintext, "");
72+
$cipherText = $encryptedText.$tag;
73+
} else {
74+
$cipherText = openssl_encrypt($plaintext, 'aes-128-gcm', $contentEncryptionKey, false, $nonce); // base 64 encoded
75+
}
76+
77+
// return values in url safe base64
78+
return array(
79+
'localPublicKey' => Base64Url::encode($localPublicKey),
80+
'salt' => Base64Url::encode($salt),
81+
'cipherText' => $cipherText,
82+
);
83+
}
84+
85+
/**
86+
* HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
87+
*
88+
* This is used to derive a secure encryption key from a mostly-secure shared
89+
* secret.
90+
*
91+
* This is a partial implementation of HKDF tailored to our specific purposes.
92+
* In particular, for us the value of N will always be 1, and thus T always
93+
* equals HMAC-Hash(PRK, info | 0x01).
94+
*
95+
* See {@link https://www.rfc-editor.org/rfc/rfc5869.txt}
96+
* From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}
97+
*
98+
* @param $salt string A non-secret random value
99+
* @param $ikm string Input keying material
100+
* @param $info string Application-specfic context
101+
* @param $length int The length (in bytes) of the required output key
102+
* @return string
103+
*/
104+
private static function hkdf($salt, $ikm, $info, $length)
105+
{
106+
// extract
107+
$prk = hash_hmac('sha256', $ikm, $salt, true);
108+
109+
// expand
110+
return substr(hash_hmac('sha256', $info.chr(1), $prk, true), 0, $length);
111+
}
112+
113+
/**
114+
* Creates a context for deriving encyption parameters.
115+
* See section 4.2 of
116+
* {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
117+
* From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}
118+
*
119+
* @param $clientPublicKey string The client's public key
120+
* @param $serverPublicKey string Our public key
121+
* @return string
122+
* @throws \ErrorException
123+
*/
124+
private static function createContext($clientPublicKey, $serverPublicKey)
125+
{
126+
if (strlen($clientPublicKey) !== 65) {
127+
throw new \ErrorException('Invalid client public key length');
128+
}
129+
130+
// This one should never happen, because it's our code that generates the key
131+
if (strlen($serverPublicKey) !== 65) {
132+
throw new \ErrorException('Invalid server public key length');
133+
}
134+
135+
return chr(0).strlen($clientPublicKey).$clientPublicKey.strlen($serverPublicKey).$serverPublicKey;
136+
}
137+
138+
/**
139+
* Returns an info record. See sections 3.2 and 3.3 of
140+
* {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
141+
* From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}
142+
*
143+
* @param $type string The type of the info record
144+
* @param $context string The context for the record
145+
* @return string
146+
* @throws \ErrorException
147+
*/
148+
private static function createInfo($type, $context) {
149+
if (strlen($context) !== 135) {
150+
throw new \ErrorException('Context argument has invalid size');
151+
}
152+
153+
return 'Content-Encoding: '.$type.chr(0).'P-256'.$context;
154+
}
155+
}

src/WebPush.php

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,11 @@
1111

1212
namespace Minishlink\WebPush;
1313

14-
use Base64Url\Base64Url;
1514
use Buzz\Browser;
1615
use Buzz\Client\AbstractClient;
1716
use Buzz\Client\MultiCurl;
1817
use Buzz\Exception\RequestException;
1918
use Buzz\Message\Response;
20-
use Mdanter\Ecc\Crypto\Key\PublicKey;
21-
use Mdanter\Ecc\EccFactory;
22-
use Mdanter\Ecc\Serializer\Point\UncompressedPointSerializer;
2319

2420
class WebPush
2521
{
@@ -177,54 +173,6 @@ public function flush()
177173
return $completeSuccess ? true : $return;
178174
}
179175

180-
/**
181-
* @param string $userPublicKey MIME base 64 encoded
182-
* @param string $payload
183-
*
184-
* @return array
185-
*/
186-
private function encrypt($userPublicKey, $payload)
187-
{
188-
// initialize utilities
189-
$math = EccFactory::getAdapter();
190-
$keySerializer = new UncompressedPointSerializer($math);
191-
$curveGenerator = EccFactory::getNistCurves()->generator256();
192-
$curve = EccFactory::getNistCurves()->curve256();
193-
194-
// get local key pair
195-
$localPrivateKeyObject = $curveGenerator->createPrivateKey();
196-
$localPublicKeyObject = $localPrivateKeyObject->getPublicKey();
197-
$localPublicKey = hex2bin($keySerializer->serialize($localPublicKeyObject->getPoint()));
198-
199-
// get user public key object
200-
$userPublicKeyObject = new PublicKey($math, $curveGenerator, $keySerializer->unserialize($curve, bin2hex(base64_decode($userPublicKey))));
201-
202-
// get shared secret from user public key and local private key
203-
$sharedSecret = $userPublicKeyObject->getPoint()->mul($localPrivateKeyObject->getSecret())->getX();
204-
205-
// generate salt
206-
$salt = openssl_random_pseudo_bytes(16);
207-
208-
// get encryption key
209-
$encryptionKey = hash_hmac('sha256', $salt, $sharedSecret, true);
210-
211-
// encrypt
212-
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-128-gcm'));
213-
if (!$this->nativePayloadEncryptionSupport && $this->payloadEncryptionSupport) {
214-
list($encryptedText, $tag) = \Jose\Util\GCM::encrypt($encryptionKey, $iv, $payload, "");
215-
$cipherText = $encryptedText.$tag;
216-
} else {
217-
$cipherText = openssl_encrypt($payload, 'aes-128-gcm', $encryptionKey, false, $iv); // base 64 encoded
218-
}
219-
220-
// return values in url safe base64
221-
return array(
222-
'localPublicKey' => Base64Url::encode($localPublicKey),
223-
'salt' => Base64Url::encode($salt),
224-
'cipherText' => Base64Url::encode($cipherText),
225-
);
226-
}
227-
228176
private function sendToStandardEndpoints(array $notifications)
229177
{
230178
$responses = array();
@@ -234,12 +182,12 @@ private function sendToStandardEndpoints(array $notifications)
234182
$userPublicKey = $notification->getUserPublicKey();
235183

236184
if (isset($payload) && isset($userPublicKey) && ($this->payloadEncryptionSupport || $this->nativePayloadEncryptionSupport)) {
237-
$encrypted = $this->encrypt($userPublicKey, $payload);
185+
$encrypted = Encryption::encrypt($payload, $userPublicKey, null, $this->nativePayloadEncryptionSupport);
238186

239187
$headers = array(
240188
'Content-Length' => strlen($encrypted['cipherText']),
241189
'Content-Type' => 'application/octet-stream',
242-
'Content-Encoding' => 'aesgcm128',
190+
'Content-Encoding' => 'aesgcm',
243191
'Encryption' => 'keyid="p256dh";salt="'.$encrypted['salt'].'"',
244192
'Crypto-Key' => 'keyid="p256dh";dh="'.$encrypted['localPublicKey'].'"',
245193
'TTL' => $this->TTL,

0 commit comments

Comments
 (0)