summaryrefslogtreecommitdiffstats
path: root/vendor/minishlink/web-push/src/VAPID.php
blob: e1f555f4606436326167f93460bb788f80bfea3a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
<?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\AlgorithmManager;
use Jose\Component\Core\Converter\StandardConverter;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Ecc\NistCurve;
use Jose\Component\Core\Util\Ecc\Point;
use Jose\Component\Core\Util\Ecc\PublicKey;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer;

class VAPID
{
    private const PUBLIC_KEY_LENGTH = 65;
    private const PRIVATE_KEY_LENGTH = 32;

    /**
     * @param array $vapid
     *
     * @return array
     *
     * @throws \ErrorException
     */
    public static function validate(array $vapid): array
    {
        if (!isset($vapid['subject'])) {
            throw new \ErrorException('[VAPID] You must provide a subject that is either a mailto: or a URL.');
        }

        if (isset($vapid['pemFile'])) {
            $vapid['pem'] = file_get_contents($vapid['pemFile']);

            if (!$vapid['pem']) {
                throw new \ErrorException('Error loading PEM file.');
            }
        }

        if (isset($vapid['pem'])) {
            $jwk = JWKFactory::createFromKey($vapid['pem']);
            if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) {
                throw new \ErrorException('Invalid PEM data.');
            }
            $publicKey = PublicKey::create(Point::create(
                gmp_init(bin2hex(Base64Url::decode($jwk->get('x'))), 16),
                gmp_init(bin2hex(Base64Url::decode($jwk->get('y'))), 16)
            ));

            $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
            if (!$binaryPublicKey) {
                throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
            }
            $vapid['publicKey'] = base64_encode($binaryPublicKey);
            $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
        }

        if (!isset($vapid['publicKey'])) {
            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 (!isset($vapid['privateKey'])) {
            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 [
            'subject' => $vapid['subject'],
            'publicKey' => $publicKey,
            'privateKey' => $privateKey,
        ];
    }

    /**
     * This method takes the required VAPID parameters and returns the required
     * header to be added to a Web Push Protocol Request.
     *
     * @param string $audience This must be the origin of the push service
     * @param string $subject This should be a URL or a 'mailto:' email address
     * @param string $publicKey The decoded VAPID public key
     * @param string $privateKey The decoded VAPID private key
     * @param string $contentEncoding
     * @param null|int $expiration The expiration of the VAPID JWT. (UNIX timestamp)
     *
     * @return array Returns an array with the 'Authorization' and 'Crypto-Key' values to be used as headers
     * @throws \ErrorException
     */
    public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null)
    {
        $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h
        if (null === $expiration || $expiration > $expirationLimit) {
            $expiration = $expirationLimit;
        }

        $header = [
            'typ' => 'JWT',
            'alg' => 'ES256',
        ];

        $jwtPayload = json_encode([
            'aud' => $audience,
            'exp' => $expiration,
            'sub' => $subject,
        ], JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
        if (!$jwtPayload) {
            throw new \ErrorException('Failed to encode JWT payload in JSON');
        }

        list($x, $y) = Utils::unserializePublicKey($publicKey);
        $jwk = JWK::create([
            'kty' => 'EC',
            'crv' => 'P-256',
            'x' => Base64Url::encode($x),
            'y' => Base64Url::encode($y),
            'd' => Base64Url::encode($privateKey),
        ]);

        $jsonConverter = new StandardConverter();
        $jwsCompactSerializer = new CompactSerializer($jsonConverter);
        $jwsBuilder = new JWSBuilder($jsonConverter, AlgorithmManager::create([new ES256()]));
        $jws = $jwsBuilder
            ->create()
            ->withPayload($jwtPayload)
            ->addSignature($jwk, $header)
            ->build();

        $jwt = $jwsCompactSerializer->serialize($jws, 0);
        $encodedPublicKey = Base64Url::encode($publicKey);

        if ($contentEncoding === "aesgcm") {
            return [
                'Authorization' => 'WebPush '.$jwt,
                'Crypto-Key' => 'p256ecdsa='.$encodedPublicKey,
            ];
        } elseif ($contentEncoding === 'aes128gcm') {
            return [
                'Authorization' => 'vapid t='.$jwt.', k='.$encodedPublicKey,
            ];
        }

        throw new \ErrorException('This content encoding is not supported');
    }

    /**
     * This method creates VAPID keys in case you would not be able to have a Linux bash.
     * DO NOT create keys at each initialization! Save those keys and reuse them.
     *
     * @return array
     * @throws \ErrorException
     */
    public static function createVapidKeys(): array
    {
        $curve = NistCurve::curve256();
        $privateKey = $curve->createPrivateKey();
        $publicKey = $curve->createPublicKey($privateKey);

        $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
        if (!$binaryPublicKey) {
            throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
        }

        $binaryPrivateKey = hex2bin(str_pad(gmp_strval($privateKey->getSecret(), 16), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
        if (!$binaryPrivateKey) {
            throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary');
        }

        return [
            'publicKey'  => Base64Url::encode($binaryPublicKey),
            'privateKey' => Base64Url::encode($binaryPrivateKey)
        ];
    }
}