/** * @dataProvider vapidProvider * * @param $audience * @param $vapid * @param $expiration * @param $expectedAuthorization * @param $expectedCryptoKey */ public function testGetVapidHeaders($audience, $vapid, $expiration, $expectedAuthorization, $expectedCryptoKey) { $vapid = VAPID::validate($vapid); $headers = VAPID::getVapidHeaders($audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $expiration); $this->assertArrayHasKey('Authorization', $headers); $this->assertEquals(Utils::safeStrlen($expectedAuthorization), Utils::safeStrlen($headers['Authorization'])); $this->assertEquals($this->explodeAuthorization($expectedAuthorization), $this->explodeAuthorization($headers['Authorization'])); $this->assertArrayHasKey('Crypto-Key', $headers); $this->assertEquals($expectedCryptoKey, $headers['Crypto-Key']); }
/** * @param array $vapid * * @return array * * @throws \ErrorException */ public static function validate(array $vapid) { if (!array_key_exists('subject', $vapid)) { throw new \ErrorException('[VAPID] You must provide a subject that is either a mailto: or a URL.'); } if (array_key_exists('pemFile', $vapid)) { $vapid['pem'] = file_get_contents($vapid['pemFile']); if (!$vapid['pem']) { throw new \ErrorException('Error loading PEM file.'); } } if (array_key_exists('pem', $vapid)) { $pem = $vapid['pem']; $posStartKey = strpos($pem, '-----BEGIN EC PRIVATE KEY-----'); $posEndKey = strpos($pem, '-----END EC PRIVATE KEY-----'); if ($posStartKey === false || $posEndKey === false) { throw new \ErrorException('Invalid PEM data.'); } $posStartKey += 30; // length of '-----BEGIN EC PRIVATE KEY-----' $pemSerializer = new PemPrivateKeySerializer(new DerPrivateKeySerializer()); $keys = self::getUncompressedKeys($pemSerializer->parse(substr($pem, $posStartKey, $posEndKey - $posStartKey))); $vapid['publicKey'] = $keys['publicKey']; $vapid['privateKey'] = $keys['privateKey']; } if (!array_key_exists('publicKey', $vapid)) { throw new \ErrorException('[VAPID] You must provide a public key.'); } $publicKey = Base64Url::decode($vapid['publicKey']); if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) { throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.'); } if (!array_key_exists('privateKey', $vapid)) { throw new \ErrorException('[VAPID] You must provide a private key.'); } $privateKey = Base64Url::decode($vapid['privateKey']); if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) { throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.'); } return array('subject' => $vapid['subject'], 'publicKey' => $publicKey, 'privateKey' => $privateKey); }
/** * 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 $type string The type of the info record * @param $context string The context for the record * * @return string * * @throws \ErrorException */ private static function createInfo($type, $context) { if (Utils::safeStrlen($context) !== 135) { throw new \ErrorException('Context argument has invalid size'); } return 'Content-Encoding: ' . $type . chr(0) . 'P-256' . $context; }
/** * @dataProvider payloadProvider * * @param string $payload * @param integer $maxLengthToPad * @param integer $expectedResLength */ public function testPadPayload($payload, $maxLengthToPad, $expectedResLength) { $res = Encryption::padPayload($payload, $maxLengthToPad); $this->assertContains('test', $res); $this->assertEquals($expectedResLength, Utils::safeStrlen($res)); }
private function prepareAndSend(array $notifications) { $responses = array(); /** @var Notification $notification */ foreach ($notifications as $notification) { $endpoint = $notification->getEndpoint(); $payload = $notification->getPayload(); $userPublicKey = $notification->getUserPublicKey(); $userAuthToken = $notification->getUserAuthToken(); $options = $notification->getOptions($this->getDefaultOptions()); $auth = $notification->getAuth($this->auth); if (isset($payload) && isset($userPublicKey) && isset($userAuthToken)) { $encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $this->nativePayloadEncryptionSupport); $headers = array('Content-Length' => Utils::safeStrlen($encrypted['cipherText']), 'Content-Type' => 'application/octet-stream', 'Content-Encoding' => 'aesgcm', 'Encryption' => 'salt=' . $encrypted['salt'], 'Crypto-Key' => 'dh=' . $encrypted['localPublicKey']); $content = $encrypted['cipherText']; } else { $headers = array('Content-Length' => 0); $content = ''; } $headers['TTL'] = $options['TTL']; if (isset($options['urgency'])) { $headers['Urgency'] = $options['urgency']; } if (isset($options['topic'])) { $headers['Topic'] = $options['topic']; } // if GCM if (substr($endpoint, 0, strlen(self::GCM_URL)) === self::GCM_URL) { if (array_key_exists('GCM', $auth)) { $headers['Authorization'] = 'key=' . $auth['GCM']; } else { throw new \ErrorException('No GCM API Key specified.'); } } elseif (array_key_exists('VAPID', $auth)) { $vapid = $auth['VAPID']; $audience = parse_url($endpoint, PHP_URL_SCHEME) . '://' . parse_url($endpoint, PHP_URL_HOST); if (!parse_url($audience)) { throw new \ErrorException('Audience "' . $audience . '"" could not be generated.'); } $vapidHeaders = VAPID::getVapidHeaders($audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey']); $headers['Authorization'] = $vapidHeaders['Authorization']; if (array_key_exists('Crypto-Key', $headers)) { // FUTURE replace ';' with ',' $headers['Crypto-Key'] .= ';' . $vapidHeaders['Crypto-Key']; } else { $headers['Crypto-Key'] = $vapidHeaders['Crypto-Key']; } } $responses[] = $this->sendRequest($endpoint, $headers, $content); } return $responses; }