diff options
Diffstat (limited to '')
-rw-r--r-- | vendor/minishlink/web-push/src/Encryption.php | 321 |
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; + } +} |