diff options
Diffstat (limited to 'vendor/minishlink/web-push/src/Encryption.php')
-rw-r--r-- | vendor/minishlink/web-push/src/Encryption.php | 642 |
1 files changed, 321 insertions, 321 deletions
diff --git a/vendor/minishlink/web-push/src/Encryption.php b/vendor/minishlink/web-push/src/Encryption.php index e9fe1ac..c867265 100644 --- a/vendor/minishlink/web-push/src/Encryption.php +++ b/vendor/minishlink/web-push/src/Encryption.php @@ -1,321 +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; - } -} +<?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;
+ }
+}
|