Skip to content

Commit

Permalink
Support aes128gcm
Browse files Browse the repository at this point in the history
  • Loading branch information
aliams committed Jan 27, 2018
1 parent 1a9398d commit b38f7a6
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 129 deletions.
20 changes: 14 additions & 6 deletions README.md
Expand Up @@ -132,7 +132,8 @@ const options = {
TTL: <Number>,
headers: {
'< header name >': '< header value >'
}
},
contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >'
}

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

### Returns

Expand Down Expand Up @@ -237,7 +239,7 @@ None.

<hr />

## encrypt(userPublicKey, userAuth, payload)
## encrypt(userPublicKey, userAuth, payload, contentEncoding)

```javascript
const pushSubscription = {
Expand All @@ -250,7 +252,8 @@ const pushSubscription = {
webPush.encrypt(
pushSubscription.keys.p256dh,
pushSubscription.keys.auth,
'My Payload'
'My Payload',
'aes128gcm'
)
.then(encryptionDetails => {

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

### Returns

Expand All @@ -282,7 +286,7 @@ encryption.

<hr />

## getVapidHeaders(audience, subject, publicKey, privateKey, expiration)
## getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration)

```javascript
const parsedUrl = url.parse(subscription.endpoint);
Expand All @@ -293,7 +297,8 @@ const vapidHeaders = vapidHelper.getVapidHeaders(
audience,
'mailto: example@web-push-node.org',
vapidDetails.publicKey,
vapidDetails.privateKey
vapidDetails.privateKey,
'aes128gcm'
);
```

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

### Returns

Expand Down Expand Up @@ -343,7 +349,8 @@ const options = {
TTL: <Number>,
headers: {
'< header name >': '< header value >'
}
},
contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >'
}

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

### Returns

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -30,7 +30,7 @@
"homepage": "https://github.com/web-push-libs/web-push#readme",
"dependencies": {
"asn1.js": "^5.0.0",
"http_ece": "0.7.2",
"http_ece": "1.0.5",
"jws": "^3.1.3",
"minimist": "^1.2.0",
"urlsafe-base64": "^1.0.0"
Expand Down
5 changes: 5 additions & 0 deletions src/cli.js
Expand Up @@ -15,6 +15,7 @@ const printUsageDetails = () => {
'[--auth=<auth secret>]',
'[--payload=<message>]',
'[--ttl=<seconds>]',
'[--encoding=<encoding type>]',
'[--vapid-subject=<vapid subject>]',
'[--vapid-pubkey=<public key url base64>]',
'[--vapid-pvtkey=<private key url base64>]',
Expand Down Expand Up @@ -90,6 +91,10 @@ const sendNotification = args => {
options.gcmAPIKey = args['gcm-api-key'];
}

if (args.encoding) {
options.encodingType = args.encoding;
}

webPush.sendNotification(subscription, payload, options)
.then(() => {
console.log('Push message sent.');
Expand Down
10 changes: 4 additions & 6 deletions src/encryption-helper.js
Expand Up @@ -4,7 +4,7 @@ const crypto = require('crypto');
const ece = require('http_ece');
const urlBase64 = require('urlsafe-base64');

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

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

ece.saveKey('webpushKey', localCurve, 'P-256');

const cipherText = ece.encrypt(payload, {
keyid: 'webpushKey',
version: contentEncoding,
dh: userPublicKey,
privateKey: localCurve,
salt: salt,
authSecret: userAuth,
padSize: 2
authSecret: userAuth
});

return {
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Expand Up @@ -4,11 +4,13 @@ const vapidHelper = require('./vapid-helper.js');
const encryptionHelper = require('./encryption-helper.js');
const WebPushLib = require('./web-push-lib.js');
const WebPushError = require('./web-push-error.js');
const WebPushConstants = require('./web-push-constants.js');

const webPush = new WebPushLib();

module.exports = {
WebPushError: WebPushError,
supportedContentEncodings: WebPushConstants.supportedContentEncodings,
encrypt: encryptionHelper.encrypt,
getVapidHeaders: vapidHelper.getVapidHeaders,
generateVAPIDKeys: vapidHelper.generateVAPIDKeys,
Expand Down
33 changes: 22 additions & 11 deletions src/vapid-helper.js
Expand Up @@ -6,6 +6,8 @@ const asn1 = require('asn1.js');
const jws = require('jws');
const url = require('url');

const WebPushConstants = require('./web-push-constants.js');

/**
* DEFAULT_EXPIRATION is set to seconds in 12 hours
*/
Expand Down Expand Up @@ -156,16 +158,17 @@ function validateExpiration(expiration) {
/**
* 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
* @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 {Buffer} publicKey The VAPID public key.
* @param {Buffer} privateKey The VAPID private key.
* @param {integer} [expiration] The expiration of the VAPID JWT.
* @return {Object} Returns an Object with the Authorization and
* @param {Buffer} publicKey The VAPID public key.
* @param {Buffer} privateKey The VAPID private key.
* @param {string} contentEncoding The contentEncoding type.
* @param {integer} [expiration] The expiration of the VAPID JWT.
* @return {Object} Returns an Object with the Authorization and
* 'Crypto-Key' values to be used as headers.
*/
function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
function getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration) {
if (!audience) {
throw new Error('No audience could be generated for VAPID.');
}
Expand Down Expand Up @@ -210,10 +213,18 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
privateKey: toPEM(privateKey)
});

return {
Authorization: 'WebPush ' + jwt,
'Crypto-Key': 'p256ecdsa=' + urlBase64.encode(publicKey)
};
if (contentEncoding === WebPushConstants.supportedContentEncodings.AES_128_GCM) {
return {
Authorization: 'vapid t=' + jwt + ', k=' + urlBase64.encode(publicKey)
};
} else if (contentEncoding === WebPushConstants.supportedContentEncodings.AES_GCM) {
return {
Authorization: 'WebPush ' + jwt,
'Crypto-Key': 'p256ecdsa=' + urlBase64.encode(publicKey)
};
}

throw new Error('Unsupported encoding type specified.');
}

module.exports = {
Expand Down
10 changes: 10 additions & 0 deletions src/web-push-constants.js
@@ -0,0 +1,10 @@
'use strict';

const WebPushConstants = {};

WebPushConstants.supportedContentEncodings = {
AES_GCM: 'aesgcm',
AES_128_GCM: 'aes128gcm'
};

module.exports = WebPushConstants;
48 changes: 37 additions & 11 deletions src/web-push-lib.js
Expand Up @@ -7,6 +7,7 @@ const https = require('https');
const WebPushError = require('./web-push-error.js');
const vapidHelper = require('./vapid-helper.js');
const encryptionHelper = require('./encryption-helper.js');
const webPushConstants = require('./web-push-constants.js');

// Default TTL is four weeks.
const DEFAULT_TTL = 2419200;
Expand Down Expand Up @@ -107,13 +108,15 @@ WebPushLib.prototype.generateRequestDetails =
let currentVapidDetails = vapidDetails;
let timeToLive = DEFAULT_TTL;
let extraHeaders = {};
let contentEncoding = webPushConstants.supportedContentEncodings.AES_GCM;

if (options) {
const validOptionKeys = [
'headers',
'gcmAPIKey',
'vapidDetails',
'TTL'
'TTL',
'contentEncoding'
];
const optionKeys = Object.keys(options);
for (let i = 0; i < optionKeys.length; i += 1) {
Expand Down Expand Up @@ -150,6 +153,15 @@ WebPushLib.prototype.generateRequestDetails =
if (options.TTL) {
timeToLive = options.TTL;
}

if (options.contentEncoding) {
if ((options.contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM
|| options.contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM)) {
contentEncoding = options.contentEncoding;
} else {
throw new Error('Unsupported content encoding specified.');
}
}
}

if (typeof timeToLive === 'undefined') {
Expand Down Expand Up @@ -177,13 +189,19 @@ WebPushLib.prototype.generateRequestDetails =
'required encryption keys'));
}

const encrypted = encryptionHelper.encrypt(subscription.keys.p256dh, subscription.keys.auth, payload);
const encrypted = encryptionHelper
.encrypt(subscription.keys.p256dh, subscription.keys.auth, payload, contentEncoding);

requestDetails.headers['Content-Length'] = encrypted.cipherText.length;
requestDetails.headers['Content-Type'] = 'application/octet-stream';
requestDetails.headers['Content-Encoding'] = 'aesgcm';
requestDetails.headers.Encryption = 'salt=' + encrypted.salt;
requestDetails.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey);

if (contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM) {
requestDetails.headers['Content-Encoding'] = webPushConstants.supportedContentEncodings.AES_128_GCM;
} else if (contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM) {
requestDetails.headers['Content-Encoding'] = webPushConstants.supportedContentEncodings.AES_GCM;
requestDetails.headers.Encryption = 'salt=' + encrypted.salt;
requestDetails.headers['Crypto-Key'] = 'dh=' + urlBase64.encode(encrypted.localPublicKey);
}

requestPayload = encrypted.cipherText;
} else {
Expand All @@ -201,6 +219,10 @@ WebPushLib.prototype.generateRequestDetails =
requestDetails.headers.Authorization = 'key=' + currentGCMAPIKey;
}
} else if (currentVapidDetails) {
if (contentEncoding === webPushConstants.supportedContentEncodings.AES_128_GCM
&& subscription.endpoint.indexOf('https://fcm.googleapis.com') === 0) {
subscription.endpoint = subscription.endpoint.replace('fcm/send', 'wp');
}
const parsedUrl = url.parse(subscription.endpoint);
const audience = parsedUrl.protocol + '//' +
parsedUrl.host;
Expand All @@ -209,15 +231,19 @@ WebPushLib.prototype.generateRequestDetails =
audience,
currentVapidDetails.subject,
currentVapidDetails.publicKey,
currentVapidDetails.privateKey
currentVapidDetails.privateKey,
contentEncoding
);

requestDetails.headers.Authorization = vapidHeaders.Authorization;
if (requestDetails.headers['Crypto-Key']) {
requestDetails.headers['Crypto-Key'] += ';' +
vapidHeaders['Crypto-Key'];
} else {
requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];

if (contentEncoding === webPushConstants.supportedContentEncodings.AES_GCM) {
if (requestDetails.headers['Crypto-Key']) {
requestDetails.headers['Crypto-Key'] += ';' +
vapidHeaders['Crypto-Key'];
} else {
requestDetails.headers['Crypto-Key'] = vapidHeaders['Crypto-Key'];
}
}
}

Expand Down

0 comments on commit b38f7a6

Please sign in to comment.