PNVapid::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 3
dl 0
loc 5
rs 10
c 2
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace SKien\PNServer;
5
6
/**
7
 * Class to create headers from VAPID key.
8
 *
9
 * parts of the code are based on the package spomky-labs/jose <br/>
10
 *  @link https://github.com/Spomky-Labs/Jose
11
 *
12
 * @package PNServer
13
 * @author Stefanius <[email protected]>
14
 * @copyright MIT License - see the LICENSE file for details
15
 */
16
class PNVapid
17
{
18
    use PNServerHelper;
19
20
    /** lenght of public key (Base64URL - decoded)  */
21
    const PUBLIC_KEY_LENGTH = 65;
22
    /** lenght of private key (Base64URL - decoded) */
23
    const PRIVATE_KEY_LENGTH = 32;
24
25
    const ERR_EMPTY_ARGUMENT = 'Empty Argument!';
26
    const ERR_INVALID_PUBLIC_KEY_LENGTH = 'Invalid public key length!';
27
    const ERR_INVALID_PRIVATE_KEY_LENGTH = 'Invalid private key length!';
28
    const ERR_NO_COMPRESSED_KEY_SUPPORTED = 'Invalid public key: only uncompressed keys are supported!';
29
30
    /** @var string VAPID subject (email or uri)    */
31
    protected string $strSubject = '';
32
    /** @var string public key                      */
33
    protected string $strPublicKey = '';
34
    /** @var string private key                     */
35
    protected string $strPrivateKey = '';
36
    /** @var string last error msg                  */
37
    protected string $strError = '';
38
39
    /**
40
     * @param string $strSubject usually 'mailto:[email protected]'
41
     * @param string $strPublicKey
42
     * @param string $strPrivateKey
43
     */
44
    public function __construct(string $strSubject, string $strPublicKey, string $strPrivateKey)
45
    {
46
        $this->strSubject = $strSubject;
47
        $this->strPublicKey = $this->decodeBase64URL($strPublicKey);
48
        $this->strPrivateKey = $this->decodeBase64URL($strPrivateKey);
49
    }
50
51
    /**
52
     * Check for valid VAPID.
53
     * - subject, public key and private key must be set <br>
54
     * - decoded public key must be 65 bytes long  <br>
55
     * - no compresed public key supported <br>
56
     * - decoded private key must be 32 bytes long <br>
57
     * @return bool
58
     */
59
    public function isValid() : bool
60
    {
61
        if (strlen($this->strSubject) == 0 ||
62
            strlen($this->strPublicKey) == 0 ||
63
            strlen($this->strPrivateKey) == 0) {
64
            $this->strError = self::ERR_EMPTY_ARGUMENT;
65
            return false;
66
        }
67
        if (mb_strlen($this->strPublicKey, '8bit') !== self::PUBLIC_KEY_LENGTH) {
68
            $this->strError = self::ERR_INVALID_PUBLIC_KEY_LENGTH;
69
            return false;
70
        }
71
        $hexPublicKey = bin2hex($this->strPublicKey);
72
        if (mb_substr($hexPublicKey, 0, 2, '8bit') !== '04') {
73
            $this->strError = self::ERR_NO_COMPRESSED_KEY_SUPPORTED;
74
            return false;
75
        }
76
        if (mb_strlen($this->strPrivateKey, '8bit') !== self::PRIVATE_KEY_LENGTH) {
77
            $this->strError = self::ERR_INVALID_PRIVATE_KEY_LENGTH;
78
            return false;
79
        }
80
        return true;
81
    }
82
83
    /**
84
     * Create header for endpoint using current timestamp.
85
     * @param string $strEndpoint
86
     * @return array<string,string>|false headers if succeeded, false on error
87
     */
88
    public function getHeaders(string $strEndpoint)
89
    {
90
        $aHeaders = false;
91
92
        // info
93
        $aJwtInfo = array("typ" => "JWT", "alg" => "ES256");
94
        $jsonJwtInfo = json_encode($aJwtInfo);
95
        $strJwtInfo = 'invalid';
96
        if ($jsonJwtInfo !== false) {
97
            $strJwtInfo = self::encodeBase64URL($jsonJwtInfo);
98
        }
99
100
        // data
101
        // - origin from endpoint
102
        // - timeout 12h from now
103
        // - subject (e-mail or URL to invoker of VAPID-keys)
104
        // TODO: change param to $strEndPointOrigin to eliminate dependency to PNSubscription!
105
        $aJwtData = array(
106
                'aud' => PNSubscription::getOrigin($strEndpoint),
107
                'exp' => time() + 43200,
108
                'sub' => $this->strSubject
109
            );
110
        $jsonJwtData = json_encode($aJwtData);
111
        $strJwtData = 'invalid';
112
        if ($jsonJwtData !== false) {
113
            $strJwtData = self::encodeBase64URL($jsonJwtData);
114
        }
115
116
        // signature
117
        // ECDSA encrypting "JwtInfo.JwtData" using the P-256 curve and the SHA-256 hash algorithm
118
        $strData = $strJwtInfo . '.' . $strJwtData;
119
        $pem = self::getP256PEM($this->strPublicKey, $this->strPrivateKey);
120
121
        $this->strError = 'Error creating signature!';
122
        $strSignature = '';
123
        if (\openssl_sign($strData, $strSignature, $pem, OPENSSL_ALGO_SHA256)) {
124
            if (($sig = self::signatureFromDER($strSignature)) !== false) {
125
                $this->strError = '';
126
                $strSignature = self::encodeBase64URL($sig);
127
                $aHeaders = [
128
                    'Authorization' => 'WebPush ' . $strJwtInfo . '.' . $strJwtData . '.' . $strSignature,
129
                    'Crypto-Key'    => 'p256ecdsa=' . self::encodeBase64URL($this->strPublicKey),
130
                ];
131
            }
132
        }
133
        return $aHeaders;
134
    }
135
136
    /**
137
     * @return string last error
138
     */
139
    public function getError() : string
140
    {
141
        return $this->strError;
142
    }
143
}
144