Skip to content

Commit e06e908

Browse files
authored
Merge pull request #309 from aliams/master
Support aes128gcm
2 parents 1a9398d + b38f7a6 commit e06e908

13 files changed

+335
-129
lines changed

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ const options = {
132132
TTL: <Number>,
133133
headers: {
134134
'< header name >': '< header value >'
135-
}
135+
},
136+
contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >'
136137
}
137138

138139
webpush.sendNotification(
@@ -179,6 +180,7 @@ request only. This overrides any API key set via `setGCMAPIKey()`.
179180
- **TTL** is a value in seconds that describes how long a push message is
180181
retained by the push service (by default, four weeks).
181182
- **headers** is an object with all the extra headers you want to add to the request.
183+
- **contentEncoding** is the type of push encoding to use (e.g. 'aesgcm', by default, or 'aes128gcm').
182184

183185
### Returns
184186

@@ -237,7 +239,7 @@ None.
237239

238240
<hr />
239241

240-
## encrypt(userPublicKey, userAuth, payload)
242+
## encrypt(userPublicKey, userAuth, payload, contentEncoding)
241243

242244
```javascript
243245
const pushSubscription = {
@@ -250,7 +252,8 @@ const pushSubscription = {
250252
webPush.encrypt(
251253
pushSubscription.keys.p256dh,
252254
pushSubscription.keys.auth,
253-
'My Payload'
255+
'My Payload',
256+
'aes128gcm'
254257
)
255258
.then(encryptionDetails => {
256259

@@ -270,6 +273,7 @@ The `encrypt()` method expects the following input:
270273
- *userPublicKey*: the public key of the receiver (from the browser).
271274
- *userAuth*: the auth secret of the receiver (from the browser).
272275
- *payload*: the message to attach to the notification.
276+
- *contentEncoding*: the type of content encoding to use (e.g. aesgcm or aes128gcm).
273277

274278
### Returns
275279

@@ -282,7 +286,7 @@ encryption.
282286

283287
<hr />
284288

285-
## getVapidHeaders(audience, subject, publicKey, privateKey, expiration)
289+
## getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration)
286290

287291
```javascript
288292
const parsedUrl = url.parse(subscription.endpoint);
@@ -293,7 +297,8 @@ const vapidHeaders = vapidHelper.getVapidHeaders(
293297
audience,
294298
'mailto: [email protected]',
295299
vapidDetails.publicKey,
296-
vapidDetails.privateKey
300+
vapidDetails.privateKey,
301+
'aes128gcm'
297302
);
298303
```
299304

@@ -308,6 +313,7 @@ The `getVapidHeaders()` method expects the following input:
308313
- *subject*: the mailto or URL for your application.
309314
- *publicKey*: the VAPID public key.
310315
- *privateKey*: the VAPID private key.
316+
- *contentEncoding*: the type of content encoding to use (e.g. aesgcm or aes128gcm).
311317

312318
### Returns
313319

@@ -343,7 +349,8 @@ const options = {
343349
TTL: <Number>,
344350
headers: {
345351
'< header name >': '< header value >'
346-
}
352+
},
353+
contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >'
347354
}
348355

349356
try {
@@ -396,6 +403,7 @@ request only. This overrides any API key set via `setGCMAPIKey()`.
396403
- **TTL** is a value in seconds that describes how long a push message is
397404
retained by the push service (by default, four weeks).
398405
- **headers** is an object with all the extra headers you want to add to the request.
406+
- **contentEncoding** is the type of push encoding to use (e.g. 'aesgcm', by default, or 'aes128gcm').
399407

400408
### Returns
401409

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"homepage": "https://github.com/web-push-libs/web-push#readme",
3131
"dependencies": {
3232
"asn1.js": "^5.0.0",
33-
"http_ece": "0.7.2",
33+
"http_ece": "1.0.5",
3434
"jws": "^3.1.3",
3535
"minimist": "^1.2.0",
3636
"urlsafe-base64": "^1.0.0"

src/cli.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const printUsageDetails = () => {
1515
'[--auth=<auth secret>]',
1616
'[--payload=<message>]',
1717
'[--ttl=<seconds>]',
18+
'[--encoding=<encoding type>]',
1819
'[--vapid-subject=<vapid subject>]',
1920
'[--vapid-pubkey=<public key url base64>]',
2021
'[--vapid-pvtkey=<private key url base64>]',
@@ -90,6 +91,10 @@ const sendNotification = args => {
9091
options.gcmAPIKey = args['gcm-api-key'];
9192
}
9293

94+
if (args.encoding) {
95+
options.encodingType = args.encoding;
96+
}
97+
9398
webPush.sendNotification(subscription, payload, options)
9499
.then(() => {
95100
console.log('Push message sent.');

src/encryption-helper.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const crypto = require('crypto');
44
const ece = require('http_ece');
55
const urlBase64 = require('urlsafe-base64');
66

7-
const encrypt = function(userPublicKey, userAuth, payload) {
7+
const encrypt = function(userPublicKey, userAuth, payload, contentEncoding) {
88
if (!userPublicKey) {
99
throw new Error('No user public key provided for encryption.');
1010
}
@@ -43,14 +43,12 @@ const encrypt = function(userPublicKey, userAuth, payload) {
4343

4444
const salt = urlBase64.encode(crypto.randomBytes(16));
4545

46-
ece.saveKey('webpushKey', localCurve, 'P-256');
47-
4846
const cipherText = ece.encrypt(payload, {
49-
keyid: 'webpushKey',
47+
version: contentEncoding,
5048
dh: userPublicKey,
49+
privateKey: localCurve,
5150
salt: salt,
52-
authSecret: userAuth,
53-
padSize: 2
51+
authSecret: userAuth
5452
});
5553

5654
return {

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ const vapidHelper = require('./vapid-helper.js');
44
const encryptionHelper = require('./encryption-helper.js');
55
const WebPushLib = require('./web-push-lib.js');
66
const WebPushError = require('./web-push-error.js');
7+
const WebPushConstants = require('./web-push-constants.js');
78

89
const webPush = new WebPushLib();
910

1011
module.exports = {
1112
WebPushError: WebPushError,
13+
supportedContentEncodings: WebPushConstants.supportedContentEncodings,
1214
encrypt: encryptionHelper.encrypt,
1315
getVapidHeaders: vapidHelper.getVapidHeaders,
1416
generateVAPIDKeys: vapidHelper.generateVAPIDKeys,

src/vapid-helper.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const asn1 = require('asn1.js');
66
const jws = require('jws');
77
const url = require('url');
88

9+
const WebPushConstants = require('./web-push-constants.js');
10+
911
/**
1012
* DEFAULT_EXPIRATION is set to seconds in 12 hours
1113
*/
@@ -156,16 +158,17 @@ function validateExpiration(expiration) {
156158
/**
157159
* This method takes the required VAPID parameters and returns the required
158160
* header to be added to a Web Push Protocol Request.
159-
* @param {string} audience This must be the origin of the push service.
160-
* @param {string} subject This should be a URL or a 'mailto:' email
161+
* @param {string} audience This must be the origin of the push service.
162+
* @param {string} subject This should be a URL or a 'mailto:' email
161163
* address.
162-
* @param {Buffer} publicKey The VAPID public key.
163-
* @param {Buffer} privateKey The VAPID private key.
164-
* @param {integer} [expiration] The expiration of the VAPID JWT.
165-
* @return {Object} Returns an Object with the Authorization and
164+
* @param {Buffer} publicKey The VAPID public key.
165+
* @param {Buffer} privateKey The VAPID private key.
166+
* @param {string} contentEncoding The contentEncoding type.
167+
* @param {integer} [expiration] The expiration of the VAPID JWT.
168+
* @return {Object} Returns an Object with the Authorization and
166169
* 'Crypto-Key' values to be used as headers.
167170
*/
168-
function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
171+
function getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration) {
169172
if (!audience) {
170173
throw new Error('No audience could be generated for VAPID.');
171174
}
@@ -210,10 +213,18 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
210213
privateKey: toPEM(privateKey)
211214
});
212215

213-
return {
214-
Authorization: 'WebPush ' + jwt,
215-
'Crypto-Key': 'p256ecdsa=' + urlBase64.encode(publicKey)
216-
};
216+
if (contentEncoding === WebPushConstants.supportedContentEncodings.AES_128_GCM) {
217+
return {
218+
Authorization: 'vapid t=' + jwt + ', k=' + urlBase64.encode(publicKey)
219+
};
220+
} else if (contentEncoding === WebPushConstants.supportedContentEncodings.AES_GCM) {
221+
return {
222+
Authorization: 'WebPush ' + jwt,
223+
'Crypto-Key': 'p256ecdsa=' + urlBase64.encode(publicKey)
224+
};
225+
}
226+
227+
throw new Error('Unsupported encoding type specified.');
217228
}
218229

219230
module.exports = {

src/web-push-constants.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
const WebPushConstants = {};
4+
5+
WebPushConstants.supportedContentEncodings = {
6+
AES_GCM: 'aesgcm',
7+
AES_128_GCM: 'aes128gcm'
8+
};
9+
10+
module.exports = WebPushConstants;

src/web-push-lib.js

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const https = require('https');
77
const WebPushError = require('./web-push-error.js');
88
const vapidHelper = require('./vapid-helper.js');
99
const encryptionHelper = require('./encryption-helper.js');
10+
const webPushConstants = require('./web-push-constants.js');
1011

1112
// Default TTL is four weeks.
1213
const DEFAULT_TTL = 2419200;
@@ -107,13 +108,15 @@ WebPushLib.prototype.generateRequestDetails =
107108
let currentVapidDetails = vapidDetails;
108109
let timeToLive = DEFAULT_TTL;
109110
let extraHeaders = {};
111+
let contentEncoding = webPushConstants.supportedContentEncodings.AES_GCM;
110112

111113
if (options) {
112114
const validOptionKeys = [
113115
'headers',
114116
'gcmAPIKey',
115117
'vapidDetails',
116-
'TTL'
118+
'TTL',
119+
'contentEncoding'
117120
];
118121
const optionKeys = Object.keys(options);
119122
for (let i = 0; i < optionKeys.length; i += 1) {
@@ -150,6 +153,15 @@ WebPushLib.prototype.generateRequestDetails =
150153
if (options.TTL) {
151154
timeToLive = options.TTL;
152155
}
156+
157+
if (options.contentEncoding) {
158+
if ((options.contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM
159+
|| options.contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM)) {
160+
contentEncoding = options.contentEncoding;
161+
} else {
162+
throw new Error('Unsupported content encoding specified.');
163+
}
164+
}
153165
}
154166

155167
if (typeof timeToLive === 'undefined') {
@@ -177,13 +189,19 @@ WebPushLib.prototype.generateRequestDetails =
177189
'required encryption keys'));
178190
}
179191

180-
const encrypted = encryptionHelper.encrypt(subscription.keys.p256dh, subscription.keys.auth, payload);
192+
const encrypted = encryptionHelper
193+
.encrypt(subscription.keys.p256dh, subscription.keys.auth, payload, contentEncoding);
181194

182195
requestDetails.headers['Content-Length'] = encrypted.cipherText.length;
183196
requestDetails.headers['Content-Type'] = 'application/octet-stream';
184-
requestDetails.headers['Content-Encoding'] = 'aesgcm';
185-
requestDetails.headers.Encryption = 'salt=' + encrypted.salt;
186-
requestDetails.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey);
197+
198+
if (contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM) {
199+
requestDetails.headers['Content-Encoding'] = webPushConstants.supportedContentEncodings.AES_128_GCM;
200+
} else if (contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM) {
201+
requestDetails.headers['Content-Encoding'] = webPushConstants.supportedContentEncodings.AES_GCM;
202+
requestDetails.headers.Encryption = 'salt=' + encrypted.salt;
203+
requestDetails.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey);
204+
}
187205

188206
requestPayload = encrypted.cipherText;
189207
} else {
@@ -201,6 +219,10 @@ WebPushLib.prototype.generateRequestDetails =
201219
requestDetails.headers.Authorization = 'key=' + currentGCMAPIKey;
202220
}
203221
} else if (currentVapidDetails) {
222+
if (contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM
223+
&& subscription.endpoint.indexOf('https://fcm.googleapis.com') === 0) {
224+
subscription.endpoint = subscription.endpoint.replace('fcm/send', 'wp');
225+
}
204226
const parsedUrl = url.parse(subscription.endpoint);
205227
const audience = parsedUrl.protocol + '//' +
206228
parsedUrl.host;
@@ -209,15 +231,19 @@ WebPushLib.prototype.generateRequestDetails =
209231
audience,
210232
currentVapidDetails.subject,
211233
currentVapidDetails.publicKey,
212-
currentVapidDetails.privateKey
234+
currentVapidDetails.privateKey,
235+
contentEncoding
213236
);
214237

215238
requestDetails.headers.Authorization = vapidHeaders.Authorization;
216-
if (requestDetails.headers['Crypto-Key']) {
217-
requestDetails.headers['Crypto-Key'] += ';' +
218-
vapidHeaders['Crypto-Key'];
219-
} else {
220-
requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
239+
240+
if (contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM) {
241+
if (requestDetails.headers['Crypto-Key']) {
242+
requestDetails.headers['Crypto-Key'] += ';' +
243+
vapidHeaders['Crypto-Key'];
244+
} else {
245+
requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
246+
}
221247
}
222248
}
223249

0 commit comments

Comments
 (0)