From b38f7a675bf66d216b20dbb877f37ec4b07cedc1 Mon Sep 17 00:00:00 2001 From: aliams Date: Sat, 27 Jan 2018 00:05:39 -0800 Subject: [PATCH] Support aes128gcm --- README.md | 20 ++++--- package-lock.json | 6 +-- package.json | 2 +- src/cli.js | 5 ++ src/encryption-helper.js | 10 ++-- src/index.js | 2 + src/vapid-helper.js | 33 ++++++++---- src/web-push-constants.js | 10 ++++ src/web-push-lib.js | 48 +++++++++++++---- test/test-encryption-helper.js | 92 ++++++++++++++++++-------------- test/test-vapid-helper.js | 79 ++++++++++++++++------------ test/testSelenium.js | 61 +++++++++++++++++---- test/testSendNotification.js | 96 ++++++++++++++++++++++++++++++---- 13 files changed, 335 insertions(+), 129 deletions(-) create mode 100644 src/web-push-constants.js diff --git a/README.md b/README.md index 4e1a9f48..884ea8df 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,8 @@ const options = { TTL: , headers: { '< header name >': '< header value >' - } + }, + contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >' } webpush.sendNotification( @@ -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 @@ -237,7 +239,7 @@ None.
-## encrypt(userPublicKey, userAuth, payload) +## encrypt(userPublicKey, userAuth, payload, contentEncoding) ```javascript const pushSubscription = { @@ -250,7 +252,8 @@ const pushSubscription = { webPush.encrypt( pushSubscription.keys.p256dh, pushSubscription.keys.auth, - 'My Payload' + 'My Payload', + 'aes128gcm' ) .then(encryptionDetails => { @@ -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 @@ -282,7 +286,7 @@ encryption.
-## getVapidHeaders(audience, subject, publicKey, privateKey, expiration) +## getVapidHeaders(audience, subject, publicKey, privateKey, contentEncoding, expiration) ```javascript const parsedUrl = url.parse(subscription.endpoint); @@ -293,7 +297,8 @@ const vapidHeaders = vapidHelper.getVapidHeaders( audience, 'mailto: example@web-push-node.org', vapidDetails.publicKey, - vapidDetails.privateKey + vapidDetails.privateKey, + 'aes128gcm' ); ``` @@ -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 @@ -343,7 +349,8 @@ const options = { TTL: , headers: { '< header name >': '< header value >' - } + }, + contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >' } try { @@ -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 diff --git a/package-lock.json b/package-lock.json index 7654365c..f3d30a48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1196,9 +1196,9 @@ } }, "http_ece": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-0.7.2.tgz", - "integrity": "sha1-m3wXfibVINgiwoM3vNX5xGX4gY0=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.0.5.tgz", + "integrity": "sha1-tgZg+q8UIVEC0Uk+pyDc2StTNy8=", "requires": { "urlsafe-base64": "1.0.0" } diff --git a/package.json b/package.json index 3aa93252..43544c3e 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/cli.js b/src/cli.js index 1fb36659..46aabec7 100755 --- a/src/cli.js +++ b/src/cli.js @@ -15,6 +15,7 @@ const printUsageDetails = () => { '[--auth=]', '[--payload=]', '[--ttl=]', + '[--encoding=]', '[--vapid-subject=]', '[--vapid-pubkey=]', '[--vapid-pvtkey=]', @@ -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.'); diff --git a/src/encryption-helper.js b/src/encryption-helper.js index 6c44ab67..3f5d1592 100644 --- a/src/encryption-helper.js +++ b/src/encryption-helper.js @@ -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.'); } @@ -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 { diff --git a/src/index.js b/src/index.js index af511dbb..d3cd06b7 100644 --- a/src/index.js +++ b/src/index.js @@ -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, diff --git a/src/vapid-helper.js b/src/vapid-helper.js index ad2e8084..3825d966 100644 --- a/src/vapid-helper.js +++ b/src/vapid-helper.js @@ -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 */ @@ -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.'); } @@ -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 = { diff --git a/src/web-push-constants.js b/src/web-push-constants.js new file mode 100644 index 00000000..5ba09799 --- /dev/null +++ b/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; diff --git a/src/web-push-lib.js b/src/web-push-lib.js index 26ee00e0..61dbada0 100644 --- a/src/web-push-lib.js +++ b/src/web-push-lib.js @@ -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; @@ -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) { @@ -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') { @@ -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 { @@ -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; @@ -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']; + } } } diff --git a/test/test-encryption-helper.js b/test/test-encryption-helper.js index c5fbf10a..097101b2 100644 --- a/test/test-encryption-helper.js +++ b/test/test-encryption-helper.js @@ -15,61 +15,75 @@ suite('Test Encryption Helpers', function() { assert(webPush.encrypt); }); - function encryptDecrypt(thing) { - const encrypted = webPush.encrypt(VALID_PUBLIC_KEY, VALID_AUTH, thing); - - ece.saveKey('webpushKey', userCurve, 'P-256'); + function encryptDecrypt(thing, contentEncoding) { + const encrypted = webPush.encrypt(VALID_PUBLIC_KEY, VALID_AUTH, thing, contentEncoding); return ece.decrypt(encrypted.cipherText, { - keyid: 'webpushKey', + version: contentEncoding, dh: urlBase64.encode(encrypted.localPublicKey), + privateKey: userCurve, salt: encrypted.salt, - authSecret: VALID_AUTH, - padSize: 2 + authSecret: VALID_AUTH }); } - test('encrypt/decrypt string', function() { - assert(encryptDecrypt('hello').equals(new Buffer('hello'))); + test('encrypt/decrypt string (aesgcm)', function() { + assert(encryptDecrypt('hello', webPush.supportedContentEncodings.AES_GCM).equals(new Buffer('hello'))); + }); + + test('encrypt/decrypt string (aes128gcm)', function() { + assert(encryptDecrypt('hello', webPush.supportedContentEncodings.AES_128_GCM).equals(new Buffer('hello'))); + }); + + test('encrypt/decrypt buffer (aesgcm)', function() { + assert(encryptDecrypt(new Buffer('hello'), webPush.supportedContentEncodings.AES_GCM).equals(new Buffer('hello'))); }); - test('encrypt/decrypt buffer', function() { - assert(encryptDecrypt(new Buffer('hello')).equals(new Buffer('hello'))); + test('encrypt/decrypt buffer (aes128gcm)', function() { + assert(encryptDecrypt(new Buffer('hello'), webPush.supportedContentEncodings.AES_128_GCM).equals(new Buffer('hello'))); }); - test('bad input to encrypt', function() { - // userPublicKey, userAuth, payload - const badInputs = [ - function() { - webPush.encrypt(); - }, - function() { - // Invalid public key - webPush.encrypt(null, VALID_AUTH, 'Example'); - }, - function() { - // Invalid auth - webPush.encrypt(VALID_PUBLIC_KEY, null, 'Example'); - }, - function() { - // No payload - webPush.encrypt(VALID_PUBLIC_KEY, VALID_AUTH, null); - }, - function() { - // Invalid auth size - webPush.encrypt(VALID_PUBLIC_KEY, 'Fake', 'Example'); - }, - function() { - // Invalid auth size - webPush.encrypt(VALID_PUBLIC_KEY, VALID_AUTH, []); - } - ]; + // userPublicKey, userAuth, payload + const badInputs = [ + function(contentEncoding) { + webPush.encrypt(null, null, null, contentEncoding); + }, + function(contentEncoding) { + // Invalid public key + webPush.encrypt(null, VALID_AUTH, 'Example', contentEncoding); + }, + function(contentEncoding) { + // Invalid auth + webPush.encrypt(VALID_PUBLIC_KEY, null, 'Example', contentEncoding); + }, + function(contentEncoding) { + // No payload + webPush.encrypt(VALID_PUBLIC_KEY, VALID_AUTH, null, contentEncoding); + }, + function(contentEncoding) { + // Invalid auth size + webPush.encrypt(VALID_PUBLIC_KEY, 'Fake', 'Example', contentEncoding); + }, + function(contentEncoding) { + // Invalid auth size + webPush.encrypt(VALID_PUBLIC_KEY, VALID_AUTH, [], contentEncoding); + } + ]; + function testBadInput(contentEncoding) { badInputs.forEach(function(badInput, index) { assert.throws(function() { - badInput(); + badInput(contentEncoding); console.log('Encryption input failed to throw: ' + index); }); }); + } + + test('bad input to encrypt (aesgcm)', function() { + testBadInput(webPush.supportedContentEncodings.AES_GCM); + }); + + test('bad input to encrypt (aes128gcm)', function() { + testBadInput(webPush.supportedContentEncodings.AES_128_GCM); }); }); diff --git a/test/test-vapid-helper.js b/test/test-vapid-helper.js index 55f06f5d..4e1c3920 100644 --- a/test/test-vapid-helper.js +++ b/test/test-vapid-helper.js @@ -12,6 +12,7 @@ const VALID_SUBJECT_MAILTO = 'mailto: example@example.com'; const VALID_SUBJECT_URL = 'https://exampe.com/contact'; const VALID_PUBLIC_KEY = urlBase64.encode(new Buffer(65)); const VALID_PRIVATE_KEY = urlBase64.encode(new Buffer(32)); +const VALID_CONTENT_ENCODING = webPush.supportedContentEncodings.AES_GCM; const VALID_EXPIRATION = Math.floor(Date.now() / 1000) + (60 * 60 * 12); suite('Test Vapid Helpers', function() { @@ -118,26 +119,29 @@ suite('Test Vapid Helpers', function() { function() { vapidHelper.getVapidHeaders(VALID_AUDIENCE, { something: 'else' }, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); }, + function() { + vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, 'invalid encoding type'); + }, function () { // String with text, is not accepted as a valid expiration value - vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, 'Not valid expiration: Must be a number, this is a string with text'); + vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_CONTENT_ENCODING, 'Not valid expiration: Must be a number, this is a string with text'); }, function () { // Object is not accepted as a valid expiration value - vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, { message: 'Not valid expiration: Must be a number, this is an object' }); + vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_CONTENT_ENCODING, { message: 'Not valid expiration: Must be a number, this is an object' }); }, function () { // Boolean is not accepted as a valid expiration value - vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, true); + vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_CONTENT_ENCODING, true); }, function () { // String is not accepted as a valid expiration value - vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, '12213'); + vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_CONTENT_ENCODING, '12213'); }, function () { // Invalid `expiration` as it exceeds 24 hours in duration const invalidExpiration = Math.floor(Date.now() / 1000) + (25 * 60 * 60); - vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, invalidExpiration); + vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_CONTENT_ENCODING, invalidExpiration); } ]; @@ -149,41 +153,52 @@ suite('Test Vapid Helpers', function() { }); }); - test('should get valid VAPID headers', function() { - const validInputs = [ - function() { - return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); - }, - function() { - return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY); - }, - function() { - return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_EXPIRATION); - }, - function() { - // 0 is a valid value for `expiration` - // since the the `expiration` value isn't checked for minimum - const secondsFromEpoch = 0; - return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, secondsFromEpoch); - }, - function () { - // Valid value for `secondsFromEpoch` passed in to - // `vapidHelper.getVapidHeaders` function - const secondsFromEpoch = Math.floor(Date.now() / 1000) + (5 * 60 * 60); - return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, secondsFromEpoch); - } - ]; - + const validInputs = [ + function(contentEncoding) { + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding); + }, + function(contentEncoding) { + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding); + }, + function(contentEncoding) { + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding, VALID_EXPIRATION); + }, + function(contentEncoding) { + // 0 is a valid value for `expiration` + // since the the `expiration` value isn't checked for minimum + const secondsFromEpoch = 0; + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding, secondsFromEpoch); + }, + function (contentEncoding) { + // Valid value for `secondsFromEpoch` passed in to + // `vapidHelper.getVapidHeaders` function + const secondsFromEpoch = Math.floor(Date.now() / 1000) + (5 * 60 * 60); + return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, contentEncoding, secondsFromEpoch); + } + ]; + + function testValidInputs(contentEncoding) { validInputs.forEach(function(validInput, index) { try { - const headers = validInput(); + const headers = validInput(contentEncoding); assert(headers.Authorization); - assert(headers['Crypto-Key']); + + if (contentEncoding === webPush.supportedContentEncodings.AES_GCM) { + assert(headers['Crypto-Key']); + } } catch (err) { console.warn('Valid input call for getVapidHeaders() threw an ' + 'error. [' + index + ']'); throw err; } }); + } + + test('should get valid VAPID headers (aesgcm)', function() { + testValidInputs(webPush.supportedContentEncodings.AES_GCM); + }); + + test('should get valid VAPID headers (aes128gcm)', function() { + testValidInputs(webPush.supportedContentEncodings.AES_128_GCM); }); }); diff --git a/test/testSelenium.js b/test/testSelenium.js index c035ed30..416138b1 100644 --- a/test/testSelenium.js +++ b/test/testSelenium.js @@ -150,14 +150,17 @@ function runTest(browser, options) { let promise; let pushPayload = null; let vapid = null; + let contentEncoding = null; if (options) { pushPayload = options.payload; vapid = options.vapid; + contentEncoding = options.contentEncoding; } if (!pushPayload) { promise = webPush.sendNotification(subscription, null, { - vapidDetails: vapid + vapidDetails: vapid, + contentEncoding: contentEncoding }); } else { if (!subscription.keys) { @@ -165,7 +168,8 @@ function runTest(browser, options) { } promise = webPush.sendNotification(subscription, pushPayload, { - vapidDetails: vapid + vapidDetails: vapid, + contentEncoding: contentEncoding }); } @@ -243,30 +247,67 @@ availableBrowsers.forEach(function(browser) { }); }); - test('send/receive notification without payload with ' + browser.getPrettyName(), function() { + test('send/receive notification without payload with ' + browser.getPrettyName() + ' (aesgcm)', function() { this.timeout(PUSH_TEST_TIMEOUT); - return runTest(browser); + return runTest(browser, { + contentEncoding: webPush.supportedContentEncodings.AES_GCM + }); }); - test('send/receive notification with payload with ' + browser.getPrettyName(), function() { + test('send/receive notification without payload with ' + browser.getPrettyName() + ' (aes128gcm)', function() { this.timeout(PUSH_TEST_TIMEOUT); return runTest(browser, { - payload: 'marco' + contentEncoding: webPush.supportedContentEncodings.AES_128_GCM }); }); - test('send/receive notification with vapid with ' + browser.getPrettyName(), function() { + test('send/receive notification with payload with ' + browser.getPrettyName() + ' (aesgcm)', function() { this.timeout(PUSH_TEST_TIMEOUT); return runTest(browser, { - vapid: VAPID_PARAM + payload: 'marco', + contentEncoding: webPush.supportedContentEncodings.AES_GCM + }); + }); + + test('send/receive notification with payload with ' + browser.getPrettyName() + ' (aes128gcm)', function() { + this.timeout(PUSH_TEST_TIMEOUT); + return runTest(browser, { + payload: 'marco', + contentEncoding: webPush.supportedContentEncodings.AES_128_GCM + }); + }); + + test('send/receive notification with vapid with ' + browser.getPrettyName() + ' (aesgcm)', function() { + this.timeout(PUSH_TEST_TIMEOUT); + return runTest(browser, { + vapid: VAPID_PARAM, + contentEncoding: webPush.supportedContentEncodings.AES_GCM + }); + }); + + test('send/receive notification with vapid with ' + browser.getPrettyName() + ' (aes128gcm)', function() { + this.timeout(PUSH_TEST_TIMEOUT); + return runTest(browser, { + vapid: VAPID_PARAM, + contentEncoding: webPush.supportedContentEncodings.AES_128_GCM + }); + }); + + test('send/receive notification with payload & vapid with ' + browser.getPrettyName() + ' (aesgcm)', function() { + this.timeout(PUSH_TEST_TIMEOUT); + return runTest(browser, { + payload: 'marco', + vapid: VAPID_PARAM, + contentEncoding: webPush.supportedContentEncodings.AES_GCM }); }); - test('send/receive notification with payload & vapid with ' + browser.getPrettyName(), function() { + test('send/receive notification with payload & vapid with ' + browser.getPrettyName() + ' (aes128gcm)', function() { this.timeout(PUSH_TEST_TIMEOUT); return runTest(browser, { payload: 'marco', - vapid: VAPID_PARAM + vapid: VAPID_PARAM, + contentEncoding: webPush.supportedContentEncodings.AES_128_GCM }); }); }); diff --git a/test/testSendNotification.js b/test/testSendNotification.js index d037ae44..ac3acba6 100644 --- a/test/testSendNotification.js +++ b/test/testSendNotification.js @@ -9,6 +9,7 @@ const ece = require('http_ece'); const urlBase64 = require('urlsafe-base64'); const portfinder = require('portfinder'); const jws = require('jws'); +const WebPushConstants = require('../src/web-push-constants.js'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; @@ -145,16 +146,14 @@ suite('sendNotification', function() { return key.indexOf('dh=') === 0; }).substring('dh='.length); - assert.equal(requestDetails.headers['content-encoding'], 'aesgcm', 'Check Content-Encoding header'); - - ece.saveKey('webpushKey', userCurve, 'P-256'); + assert.equal(requestDetails.headers['content-encoding'], options.extraOptions.contentEncoding, 'Check Content-Encoding header'); const decrypted = ece.decrypt(requestBody, { - keyid: 'webpushKey', + version: options.extraOptions.contentEncoding, + privateKey: userCurve, dh: appServerPublicKey, salt: salt, - authSecret: urlBase64.encode(userAuth), - padSize: 2 + authSecret: urlBase64.encode(userAuth) }); assert(decrypted.equals(new Buffer(options.message)), 'Check cipher text can be correctly decoded'); @@ -329,7 +328,7 @@ suite('sendNotification', function() { // TODO: Add test for VAPID override validRequests.forEach(function(validRequest) { - test(validRequest.testTitle, function() { + test(validRequest.testTitle + ' (aesgcm)', function() { // Set the default endpoint if it's not already configured if (!validRequest.requestOptions.subscription.endpoint) { validRequest.requestOptions.subscription.endpoint = @@ -341,6 +340,38 @@ suite('sendNotification', function() { validRequest.serverFlags.join('&'); } + validRequest.requestOptions.extraOptions = validRequest.requestOptions.extraOptions || {}; + validRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_GCM; + + const webPush = require('../src/index'); + return webPush.sendNotification( + validRequest.requestOptions.subscription, + validRequest.requestOptions.message, + validRequest.requestOptions.extraOptions + ) + .then(function(response) { + assert.equal(response.body, 'ok'); + }) + .then(function() { + validateRequest(validRequest); + }); + }); + + test(validRequest.testTitle + ' (aes128gcm)', function() { + // Set the default endpoint if it's not already configured + if (!validRequest.requestOptions.subscription.endpoint) { + validRequest.requestOptions.subscription.endpoint = + 'https://127.0.0.1:' + serverPort; + } + + if (validRequest.serverFlags) { + validRequest.requestOptions.subscription.endpoint += '?' + + validRequest.serverFlags.join('&'); + } + + validRequest.requestOptions.extraOptions = validRequest.requestOptions.extraOptions || {}; + validRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_128_GCM; + const webPush = require('../src/index'); return webPush.sendNotification( validRequest.requestOptions.subscription, @@ -383,7 +414,7 @@ suite('sendNotification', function() { } } }, { - testTitle: 'send notification if push service is GCM and you want to use VAPID', + testTitle: 'send notification if push service is GCM and you want to use VAPID (aesgcm)', requestOptions: { subscription: { }, @@ -392,7 +423,22 @@ suite('sendNotification', function() { subject: 'mailto:mozilla@example.org', privateKey: vapidKeys.privateKey, publicKey: vapidKeys.publicKey - } + }, + contentEncoding: WebPushConstants.supportedContentEncodings.AES_GCM + } + } + }, { + testTitle: 'send notification if push service is GCM and you want to use VAPID (aes128gcm)', + requestOptions: { + subscription: { + }, + extraOptions: { + vapidDetails: { + subject: 'mailto:mozilla@example.org', + privateKey: vapidKeys.privateKey, + publicKey: vapidKeys.publicKey + }, + contentEncoding: WebPushConstants.supportedContentEncodings.AES_128_GCM } } } @@ -611,7 +657,34 @@ suite('sendNotification', function() { ]; invalidRequests.forEach(function(invalidRequest) { - test(invalidRequest.testTitle, function() { + test(invalidRequest.testTitle + ' (aesgcm)', function() { + if (invalidRequest.addEndpoint) { + invalidRequest.requestOptions.subscription.endpoint = + 'https://127.0.0.1:' + serverPort; + } + + if (invalidRequest.serverFlags) { + invalidRequest.requestOptions.subscription.endpoint += '?' + + invalidRequest.serverFlags.join('&'); + } + + invalidRequest.requestOptions.extraOptions = invalidRequest.requestOptions.extraOptions || {}; + invalidRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_GCM; + + const webPush = require('../src/index'); + return webPush.sendNotification( + invalidRequest.requestOptions.subscription, + invalidRequest.requestOptions.message, + invalidRequest.requestOptions.extraOptions + ) + .then(function() { + throw new Error('Expected promise to reject'); + }, function() { + // NOOP, this error is expected + }); + }); + + test(invalidRequest.testTitle + ' (aes128gcm)', function() { if (invalidRequest.addEndpoint) { invalidRequest.requestOptions.subscription.endpoint = 'https://127.0.0.1:' + serverPort; @@ -622,6 +695,9 @@ suite('sendNotification', function() { invalidRequest.serverFlags.join('&'); } + invalidRequest.requestOptions.extraOptions = invalidRequest.requestOptions.extraOptions || {}; + invalidRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_128_GCM; + const webPush = require('../src/index'); return webPush.sendNotification( invalidRequest.requestOptions.subscription,