summaryrefslogtreecommitdiffstats
path: root/vendor/minishlink/web-push/src/Encryption.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--vendor/minishlink/web-push/src/Encryption.php321
1 files changed, 321 insertions, 0 deletions
diff --git a/vendor/minishlink/web-push/src/Encryption.php b/vendor/minishlink/web-push/src/Encryption.php
new file mode 100644
index 0000000..e9fe1ac
--- /dev/null
+++ b/vendor/minishlink/web-push/src/Encryption.php
@@ -0,0 +1,321 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+use Base64Url\Base64Url;
+use Jose\Component\Core\Util\Ecc\NistCurve;
+use Jose\Component\Core\Util\Ecc\Point;
+use Jose\Component\Core\Util\Ecc\PrivateKey;
+use Jose\Component\Core\Util\Ecc\PublicKey;
+
+class Encryption
+{
+ public const MAX_PAYLOAD_LENGTH = 4078;
+ public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052;
+
+ /**
+ * @param string $payload
+ * @param int $maxLengthToPad
+ * @param string $contentEncoding
+ * @return string padded payload (plaintext)
+ * @throws \ErrorException
+ */
+ public static function padPayload(string $payload, int $maxLengthToPad, string $contentEncoding): string
+ {
+ $payloadLen = Utils::safeStrlen($payload);
+ $padLen = $maxLengthToPad ? $maxLengthToPad - $payloadLen : 0;
+
+ if ($contentEncoding === "aesgcm") {
+ return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT);
+ } elseif ($contentEncoding === "aes128gcm") {
+ return str_pad($payload.chr(2), $padLen + $payloadLen, chr(0), STR_PAD_RIGHT);
+ } else {
+ throw new \ErrorException("This content encoding is not supported");
+ }
+ }
+
+ /**
+ * @param string $payload With padding
+ * @param string $userPublicKey Base 64 encoded (MIME or URL-safe)
+ * @param string $userAuthToken Base 64 encoded (MIME or URL-safe)
+ * @param string $contentEncoding
+ * @return array
+ *
+ * @throws \ErrorException
+ */
+ public static function encrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding): array
+ {
+ return self::deterministicEncrypt(
+ $payload,
+ $userPublicKey,
+ $userAuthToken,
+ $contentEncoding,
+ self::createLocalKeyObject(),
+ random_bytes(16)
+ );
+ }
+
+ /**
+ * @param string $payload
+ * @param string $userPublicKey
+ * @param string $userAuthToken
+ * @param string $contentEncoding
+ * @param array $localKeyObject
+ * @param string $salt
+ * @return array
+ *
+ * @throws \ErrorException
+ */
+ 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);
+
+ $curve = NistCurve::curve256();
+
+ // get local key pair
+ list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject;
+ $localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject));
+ if (!$localPublicKey) {
+ throw new \ErrorException('Failed to convert local public key from hexadecimal to binary');
+ }
+
+ // get user public key object
+ [$userPublicKeyObjectX, $userPublicKeyObjectY] = Utils::unserializePublicKey($userPublicKey);
+ $userPublicKeyObject = $curve->getPublicKeyFrom(
+ gmp_init(bin2hex($userPublicKeyObjectX), 16),
+ gmp_init(bin2hex($userPublicKeyObjectY), 16)
+ );
+
+ // get shared secret from user public key and local private key
+ $sharedSecret = $curve->mul($userPublicKeyObject->getPoint(), $localPrivateKeyObject->getSecret())->getX();
+ $sharedSecret = hex2bin(str_pad(gmp_strval($sharedSecret, 16), 64, '0', STR_PAD_LEFT));
+ if (!$sharedSecret) {
+ throw new \ErrorException('Failed to convert shared secret from hexadecimal to binary');
+ }
+
+ // section 4.3
+ $ikm = self::getIKM($userAuthToken, $userPublicKey, $localPublicKey, $sharedSecret, $contentEncoding);
+
+ // section 4.2
+ $context = self::createContext($userPublicKey, $localPublicKey, $contentEncoding);
+
+ // derive the Content Encryption Key
+ $contentEncryptionKeyInfo = self::createInfo($contentEncoding, $context, $contentEncoding);
+ $contentEncryptionKey = self::hkdf($salt, $ikm, $contentEncryptionKeyInfo, 16);
+
+ // section 3.3, derive the nonce
+ $nonceInfo = self::createInfo('nonce', $context, $contentEncoding);
+ $nonce = self::hkdf($salt, $ikm, $nonceInfo, 12);
+
+ // encrypt
+ // "The additional data passed to each invocation of AEAD_AES_128_GCM is a zero-length octet sequence."
+ $tag = '';
+ $encryptedText = openssl_encrypt($payload, 'aes-128-gcm', $contentEncryptionKey, OPENSSL_RAW_DATA, $nonce, $tag);
+
+ // return values in url safe base64
+ return [
+ 'localPublicKey' => $localPublicKey,
+ 'salt' => $salt,
+ 'cipherText' => $encryptedText.$tag,
+ ];
+ }
+
+ public static function getContentCodingHeader($salt, $localPublicKey, $contentEncoding): string
+ {
+ if ($contentEncoding === "aes128gcm") {
+ return $salt
+ .pack('N*', 4096)
+ .pack('C*', Utils::safeStrlen($localPublicKey))
+ .$localPublicKey;
+ }
+
+ return "";
+ }
+
+ /**
+ * HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
+ *
+ * This is used to derive a secure encryption key from a mostly-secure shared
+ * secret.
+ *
+ * This is a partial implementation of HKDF tailored to our specific purposes.
+ * In particular, for us the value of N will always be 1, and thus T always
+ * equals HMAC-Hash(PRK, info | 0x01).
+ *
+ * See {@link https://www.rfc-editor.org/rfc/rfc5869.txt}
+ * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}
+ *
+ * @param string $salt A non-secret random value
+ * @param string $ikm Input keying material
+ * @param string $info Application-specific context
+ * @param int $length The length (in bytes) of the required output key
+ *
+ * @return string
+ */
+ private static function hkdf(string $salt, string $ikm, string $info, int $length): string
+ {
+ // extract
+ $prk = hash_hmac('sha256', $ikm, $salt, true);
+
+ // expand
+ return mb_substr(hash_hmac('sha256', $info.chr(1), $prk, true), 0, $length, '8bit');
+ }
+
+ /**
+ * Creates a context for deriving encryption parameters.
+ * See section 4.2 of
+ * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
+ * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
+ *
+ * @param string $clientPublicKey The client's public key
+ * @param string $serverPublicKey Our public key
+ *
+ * @return null|string
+ *
+ * @throws \ErrorException
+ */
+ private static function createContext(string $clientPublicKey, string $serverPublicKey, $contentEncoding): ?string
+ {
+ if ($contentEncoding === "aes128gcm") {
+ return null;
+ }
+
+ if (Utils::safeStrlen($clientPublicKey) !== 65) {
+ throw new \ErrorException('Invalid client public key length');
+ }
+
+ // This one should never happen, because it's our code that generates the key
+ if (Utils::safeStrlen($serverPublicKey) !== 65) {
+ throw new \ErrorException('Invalid server public key length');
+ }
+
+ $len = chr(0).'A'; // 65 as Uint16BE
+
+ return chr(0).$len.$clientPublicKey.$len.$serverPublicKey;
+ }
+
+ /**
+ * Returns an info record. See sections 3.2 and 3.3 of
+ * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
+ * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
+ *
+ * @param string $type The type of the info record
+ * @param string|null $context The context for the record
+ * @param string $contentEncoding
+ * @return string
+ *
+ * @throws \ErrorException
+ */
+ private static function createInfo(string $type, ?string $context, string $contentEncoding): string
+ {
+ if ($contentEncoding === "aesgcm") {
+ if (!$context) {
+ throw new \ErrorException('Context must exist');
+ }
+
+ if (Utils::safeStrlen($context) !== 135) {
+ throw new \ErrorException('Context argument has invalid size');
+ }
+
+ return 'Content-Encoding: '.$type.chr(0).'P-256'.$context;
+ } elseif ($contentEncoding === "aes128gcm") {
+ return 'Content-Encoding: '.$type.chr(0);
+ }
+
+ throw new \ErrorException('This content encoding is not supported.');
+ }
+
+ /**
+ * @return array
+ */
+ private static function createLocalKeyObject(): array
+ {
+ try {
+ return self::createLocalKeyObjectUsingOpenSSL();
+ } catch (\Exception $e) {
+ return self::createLocalKeyObjectUsingPurePhpMethod();
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private static function createLocalKeyObjectUsingPurePhpMethod(): array
+ {
+ $curve = NistCurve::curve256();
+ $privateKey = $curve->createPrivateKey();
+
+ return [
+ $curve->createPublicKey($privateKey),
+ $privateKey,
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private static function createLocalKeyObjectUsingOpenSSL(): array
+ {
+ $keyResource = openssl_pkey_new([
+ 'curve_name' => 'prime256v1',
+ 'private_key_type' => OPENSSL_KEYTYPE_EC,
+ ]);
+
+ if (!$keyResource) {
+ throw new \RuntimeException('Unable to create the key');
+ }
+
+ $details = openssl_pkey_get_details($keyResource);
+ openssl_pkey_free($keyResource);
+
+ if (!$details) {
+ throw new \RuntimeException('Unable to get the key details');
+ }
+
+ return [
+ PublicKey::create(Point::create(
+ gmp_init(bin2hex($details['ec']['x']), 16),
+ gmp_init(bin2hex($details['ec']['y']), 16)
+ )),
+ PrivateKey::create(gmp_init(bin2hex($details['ec']['d']), 16))
+ ];
+ }
+
+ /**
+ * @param string $userAuthToken
+ * @param string $userPublicKey
+ * @param string $localPublicKey
+ * @param string $sharedSecret
+ * @param string $contentEncoding
+ * @return string
+ * @throws \ErrorException
+ */
+ private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, string $contentEncoding): string
+ {
+ if (!empty($userAuthToken)) {
+ if ($contentEncoding === "aesgcm") {
+ $info = 'Content-Encoding: auth'.chr(0);
+ } elseif ($contentEncoding === "aes128gcm") {
+ $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey;
+ } else {
+ throw new \ErrorException("This content encoding is not supported");
+ }
+
+ return self::hkdf($userAuthToken, $sharedSecret, $info, 32);
+ }
+
+ return $sharedSecret;
+ }
+}