@@ -26,36 +26,66 @@ class Encryption
2626
2727 /**
2828 * @param string $payload
29- * @param int $maxLengthToPad
30- *
29+ * @param int $maxLengthToPad
30+ * @param string $contentEncoding
3131 * @return string padded payload (plaintext)
32+ * @throws \ErrorException
3233 */
33- public static function padPayload (string $ payload , int $ maxLengthToPad ): string
34+ public static function padPayload (string $ payload , int $ maxLengthToPad, string $ contentEncoding ): string
3435 {
3536 $ payloadLen = Utils::safeStrlen ($ payload );
3637 $ padLen = $ maxLengthToPad ? $ maxLengthToPad - $ payloadLen : 0 ;
3738
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+ }
3946 }
4047
4148 /**
42- * @param string $payload With padding
49+ * @param string $payload With padding
4350 * @param string $userPublicKey Base 64 encoded (MIME or URL-safe)
4451 * @param string $userAuthToken Base 64 encoded (MIME or URL-safe)
52+ * @param string $contentEncoding
53+ * @return array
4554 *
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
4676 * @return array
4777 *
4878 * @throws \ErrorException
4979 */
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
5181 {
5282 $ userPublicKey = Base64Url::decode ($ userPublicKey );
5383 $ userAuthToken = Base64Url::decode ($ userAuthToken );
5484
5585 $ curve = NistCurve::curve256 ();
5686
5787 // get local key pair
58- list ($ localPublicKeyObject , $ localPrivateKeyObject ) = self :: createLocalKeyObject () ;
88+ list ($ localPublicKeyObject , $ localPrivateKeyObject ) = $ localKeyObject ;
5989 $ localPublicKey = hex2bin (Utils::serializePublicKey ($ localPublicKeyObject ));
6090
6191 // get user public key object
@@ -69,23 +99,18 @@ public static function encrypt(string $payload, string $userPublicKey, string $u
6999 $ sharedSecret = $ curve ->mul ($ userPublicKeyObject ->getPoint (), $ localPrivateKeyObject ->getSecret ())->getX ();
70100 $ sharedSecret = hex2bin (str_pad (gmp_strval ($ sharedSecret , 16 ), 64 , '0 ' , STR_PAD_LEFT ));
71101
72- // generate salt
73- $ salt = random_bytes (16 );
74-
75102 // 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 );
79104
80105 // section 4.2
81- $ context = self ::createContext ($ userPublicKey , $ localPublicKey );
106+ $ context = self ::createContext ($ userPublicKey , $ localPublicKey, $ contentEncoding );
82107
83108 // derive the Content Encryption Key
84- $ contentEncryptionKeyInfo = self ::createInfo (' aesgcm ' , $ context );
109+ $ contentEncryptionKeyInfo = self ::createInfo ($ contentEncoding , $ context, $ contentEncoding );
85110 $ contentEncryptionKey = self ::hkdf ($ salt , $ ikm , $ contentEncryptionKeyInfo , 16 );
86111
87112 // section 3.3, derive the nonce
88- $ nonceInfo = self ::createInfo ('nonce ' , $ context );
113+ $ nonceInfo = self ::createInfo ('nonce ' , $ context, $ contentEncoding );
89114 $ nonce = self ::hkdf ($ salt , $ ikm , $ nonceInfo , 12 );
90115
91116 // encrypt
@@ -94,12 +119,24 @@ public static function encrypt(string $payload, string $userPublicKey, string $u
94119
95120 // return values in url safe base64
96121 return [
97- 'localPublicKey ' => Base64Url:: encode ( $ localPublicKey) ,
98- 'salt ' => Base64Url:: encode ( $ salt) ,
122+ 'localPublicKey ' => $ localPublicKey ,
123+ 'salt ' => $ salt ,
99124 'cipherText ' => $ encryptedText .$ tag ,
100125 ];
101126 }
102127
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+
103140 /**
104141 * HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
105142 *
@@ -138,12 +175,16 @@ private static function hkdf(string $salt, string $ikm, string $info, int $lengt
138175 * @param string $clientPublicKey The client's public key
139176 * @param string $serverPublicKey Our public key
140177 *
141- * @return string
178+ * @return null| string
142179 *
143180 * @throws \ErrorException
144181 */
145- private static function createContext (string $ clientPublicKey , string $ serverPublicKey ): string
182+ private static function createContext (string $ clientPublicKey , string $ serverPublicKey, $ contentEncoding ): ? string
146183 {
184+ if ($ contentEncoding === "aes128gcm " ) {
185+ return null ;
186+ }
187+
147188 if (Utils::safeStrlen ($ clientPublicKey ) !== 65 ) {
148189 throw new \ErrorException ('Invalid client public key length ' );
149190 }
@@ -163,20 +204,30 @@ private static function createContext(string $clientPublicKey, string $serverPub
163204 * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
164205 * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
165206 *
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
169210 * @return string
170211 *
171212 * @throws \ErrorException
172213 */
173- private static function createInfo (string $ type , string $ context ): string
214+ private static function createInfo (string $ type , ? string $ context, string $ contentEncoding ): string
174215 {
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 );
177228 }
178229
179- return ' Content-Encoding: ' . $ type . chr ( 0 ). ' P-256 ' . $ context ;
230+ throw new \ ErrorException ( ' This content encoding is not supported. ' ) ;
180231 }
181232
182233 /**
@@ -234,4 +285,30 @@ private static function createLocalKeyObjectUsingOpenSSL(): array
234285 PrivateKey::create (gmp_init (bin2hex ($ details ['ec ' ]['d ' ]), 16 ))
235286 ];
236287 }
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+ }
237314}
0 commit comments