@@ -26,36 +26,66 @@ class Encryption
26
26
27
27
/**
28
28
* @param string $payload
29
- * @param int $maxLengthToPad
30
- *
29
+ * @param int $maxLengthToPad
30
+ * @param string $contentEncoding
31
31
* @return string padded payload (plaintext)
32
+ * @throws \ErrorException
32
33
*/
33
- public static function padPayload (string $ payload , int $ maxLengthToPad ): string
34
+ public static function padPayload (string $ payload , int $ maxLengthToPad, string $ contentEncoding ): string
34
35
{
35
36
$ payloadLen = Utils::safeStrlen ($ payload );
36
37
$ padLen = $ maxLengthToPad ? $ maxLengthToPad - $ payloadLen : 0 ;
37
38
38
- return pack ('n* ' , $ padLen ).str_pad ($ payload , $ padLen + $ payloadLen , chr (0 ), STR_PAD_LEFT );
39
+ if ($ contentEncoding === "aesgcm " ) {
40
+ return pack ('n* ' , $ padLen ).str_pad ($ payload , $ padLen + $ payloadLen , chr (0 ), STR_PAD_LEFT );
41
+ } else if ($ contentEncoding === "aes128gcm " ) {
42
+ return str_pad ($ payload .chr (2 ), $ padLen + $ payloadLen , chr (0 ), STR_PAD_RIGHT );
43
+ } else {
44
+ throw new \ErrorException ("This content encoding is not supported " );
45
+ }
39
46
}
40
47
41
48
/**
42
- * @param string $payload With padding
49
+ * @param string $payload With padding
43
50
* @param string $userPublicKey Base 64 encoded (MIME or URL-safe)
44
51
* @param string $userAuthToken Base 64 encoded (MIME or URL-safe)
52
+ * @param string $contentEncoding
53
+ * @return array
45
54
*
55
+ * @throws \ErrorException
56
+ */
57
+ public static function encrypt (string $ payload , string $ userPublicKey , string $ userAuthToken , string $ contentEncoding ): array
58
+ {
59
+ return self ::deterministicEncrypt (
60
+ $ payload ,
61
+ $ userPublicKey ,
62
+ $ userAuthToken ,
63
+ $ contentEncoding ,
64
+ self ::createLocalKeyObject (),
65
+ random_bytes (16 )
66
+ );
67
+ }
68
+
69
+ /**
70
+ * @param string $payload
71
+ * @param string $userPublicKey
72
+ * @param string $userAuthToken
73
+ * @param string $contentEncoding
74
+ * @param array $localKeyObject
75
+ * @param string $salt
46
76
* @return array
47
77
*
48
78
* @throws \ErrorException
49
79
*/
50
- public static function encrypt (string $ payload , string $ userPublicKey , string $ userAuthToken ): array
80
+ public static function deterministicEncrypt (string $ payload , string $ userPublicKey , string $ userAuthToken, string $ contentEncoding , array $ localKeyObject , string $ salt ): array
51
81
{
52
82
$ userPublicKey = Base64Url::decode ($ userPublicKey );
53
83
$ userAuthToken = Base64Url::decode ($ userAuthToken );
54
84
55
85
$ curve = NistCurve::curve256 ();
56
86
57
87
// get local key pair
58
- list ($ localPublicKeyObject , $ localPrivateKeyObject ) = self :: createLocalKeyObject () ;
88
+ list ($ localPublicKeyObject , $ localPrivateKeyObject ) = $ localKeyObject ;
59
89
$ localPublicKey = hex2bin (Utils::serializePublicKey ($ localPublicKeyObject ));
60
90
61
91
// get user public key object
@@ -69,23 +99,18 @@ public static function encrypt(string $payload, string $userPublicKey, string $u
69
99
$ sharedSecret = $ curve ->mul ($ userPublicKeyObject ->getPoint (), $ localPrivateKeyObject ->getSecret ())->getX ();
70
100
$ sharedSecret = hex2bin (str_pad (gmp_strval ($ sharedSecret , 16 ), 64 , '0 ' , STR_PAD_LEFT ));
71
101
72
- // generate salt
73
- $ salt = random_bytes (16 );
74
-
75
102
// section 4.3
76
- $ ikm = !empty ($ userAuthToken ) ?
77
- self ::hkdf ($ userAuthToken , $ sharedSecret , 'Content-Encoding: auth ' .chr (0 ), 32 ) :
78
- $ sharedSecret ;
103
+ $ ikm = self ::getIKM ($ userAuthToken , $ userPublicKey , $ localPublicKey , $ sharedSecret , $ contentEncoding );
79
104
80
105
// section 4.2
81
- $ context = self ::createContext ($ userPublicKey , $ localPublicKey );
106
+ $ context = self ::createContext ($ userPublicKey , $ localPublicKey, $ contentEncoding );
82
107
83
108
// derive the Content Encryption Key
84
- $ contentEncryptionKeyInfo = self ::createInfo (' aesgcm ' , $ context );
109
+ $ contentEncryptionKeyInfo = self ::createInfo ($ contentEncoding , $ context, $ contentEncoding );
85
110
$ contentEncryptionKey = self ::hkdf ($ salt , $ ikm , $ contentEncryptionKeyInfo , 16 );
86
111
87
112
// section 3.3, derive the nonce
88
- $ nonceInfo = self ::createInfo ('nonce ' , $ context );
113
+ $ nonceInfo = self ::createInfo ('nonce ' , $ context, $ contentEncoding );
89
114
$ nonce = self ::hkdf ($ salt , $ ikm , $ nonceInfo , 12 );
90
115
91
116
// encrypt
@@ -94,12 +119,24 @@ public static function encrypt(string $payload, string $userPublicKey, string $u
94
119
95
120
// return values in url safe base64
96
121
return [
97
- 'localPublicKey ' => Base64Url:: encode ( $ localPublicKey) ,
98
- 'salt ' => Base64Url:: encode ( $ salt) ,
122
+ 'localPublicKey ' => $ localPublicKey ,
123
+ 'salt ' => $ salt ,
99
124
'cipherText ' => $ encryptedText .$ tag ,
100
125
];
101
126
}
102
127
128
+ public static function getContentCodingHeader ($ salt , $ localPublicKey , $ contentEncoding ): string
129
+ {
130
+ if ($ contentEncoding === "aes128gcm " ) {
131
+ return $ salt
132
+ .pack ('N* ' , 4096 )
133
+ .pack ('C* ' , Utils::safeStrlen ($ localPublicKey ))
134
+ .$ localPublicKey ;
135
+ }
136
+
137
+ return "" ;
138
+ }
139
+
103
140
/**
104
141
* HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
105
142
*
@@ -138,12 +175,16 @@ private static function hkdf(string $salt, string $ikm, string $info, int $lengt
138
175
* @param string $clientPublicKey The client's public key
139
176
* @param string $serverPublicKey Our public key
140
177
*
141
- * @return string
178
+ * @return null| string
142
179
*
143
180
* @throws \ErrorException
144
181
*/
145
- private static function createContext (string $ clientPublicKey , string $ serverPublicKey ): string
182
+ private static function createContext (string $ clientPublicKey , string $ serverPublicKey, $ contentEncoding ): ? string
146
183
{
184
+ if ($ contentEncoding === "aes128gcm " ) {
185
+ return null ;
186
+ }
187
+
147
188
if (Utils::safeStrlen ($ clientPublicKey ) !== 65 ) {
148
189
throw new \ErrorException ('Invalid client public key length ' );
149
190
}
@@ -163,20 +204,30 @@ private static function createContext(string $clientPublicKey, string $serverPub
163
204
* {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
164
205
* From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
165
206
*
166
- * @param string $type The type of the info record
167
- * @param string $context The context for the record
168
- *
207
+ * @param string $type The type of the info record
208
+ * @param string|null $context The context for the record
209
+ * @param string $contentEncoding
169
210
* @return string
170
211
*
171
212
* @throws \ErrorException
172
213
*/
173
- private static function createInfo (string $ type , string $ context ): string
214
+ private static function createInfo (string $ type , ? string $ context, string $ contentEncoding ): string
174
215
{
175
- if (Utils::safeStrlen ($ context ) !== 135 ) {
176
- throw new \ErrorException ('Context argument has invalid size ' );
216
+ if ($ contentEncoding === "aesgcm " ) {
217
+ if (!$ context ) {
218
+ throw new \ErrorException ('Context must exist ' );
219
+ }
220
+
221
+ if (Utils::safeStrlen ($ context ) !== 135 ) {
222
+ throw new \ErrorException ('Context argument has invalid size ' );
223
+ }
224
+
225
+ return 'Content-Encoding: ' .$ type .chr (0 ).'P-256 ' .$ context ;
226
+ } else if ($ contentEncoding === "aes128gcm " ) {
227
+ return 'Content-Encoding: ' .$ type .chr (0 );
177
228
}
178
229
179
- return ' Content-Encoding: ' . $ type . chr ( 0 ). ' P-256 ' . $ context ;
230
+ throw new \ ErrorException ( ' This content encoding is not supported. ' ) ;
180
231
}
181
232
182
233
/**
@@ -234,4 +285,30 @@ private static function createLocalKeyObjectUsingOpenSSL(): array
234
285
PrivateKey::create (gmp_init (bin2hex ($ details ['ec ' ]['d ' ]), 16 ))
235
286
];
236
287
}
288
+
289
+ /**
290
+ * @param string $userAuthToken
291
+ * @param string $userPublicKey
292
+ * @param string $localPublicKey
293
+ * @param string $sharedSecret
294
+ * @param string $contentEncoding
295
+ * @return string
296
+ * @throws \ErrorException
297
+ */
298
+ private static function getIKM (string $ userAuthToken , string $ userPublicKey , string $ localPublicKey , string $ sharedSecret , string $ contentEncoding ): string
299
+ {
300
+ if (!empty ($ userAuthToken )) {
301
+ if ($ contentEncoding === "aesgcm " ) {
302
+ $ info = 'Content-Encoding: auth ' .chr (0 );
303
+ } else if ($ contentEncoding === "aes128gcm " ) {
304
+ $ info = "WebPush: info " .chr (0 ).$ userPublicKey .$ localPublicKey ;
305
+ } else {
306
+ throw new \ErrorException ("This content encoding is not supported " );
307
+ }
308
+
309
+ return self ::hkdf ($ userAuthToken , $ sharedSecret , $ info , 32 );
310
+ }
311
+
312
+ return $ sharedSecret ;
313
+ }
237
314
}
0 commit comments