Skip to content

Commit

Permalink
Merge pull request #220 from gauntface/vapid-string
Browse files Browse the repository at this point in the history
Changing vapid input / output to be base64url encoded string
  • Loading branch information
marco-c authored Sep 23, 2016
2 parents 251c28d + 616c2b9 commit ab8d884
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 46 deletions.
95 changes: 59 additions & 36 deletions src/vapid-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,38 +32,12 @@ function generateVAPIDKeys() {
curve.generateKeys();

return {
publicKey: curve.getPublicKey(),
privateKey: curve.getPrivateKey()
publicKey: urlBase64.encode(curve.getPublicKey()),
privateKey: urlBase64.encode(curve.getPrivateKey())
};
}

/**
* 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
* 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
* 'Crypto-Key' values to be used as headers.
*/
function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
if (!audience) {
throw new Error('No audience set in vapid.audience.');
}

if (typeof audience !== 'string' || audience.length === 0) {
throw new Error('The audience value must be a string containing the ' +
'origin of a push service. ' + audience);
}

const audienceParseResult = url.parse(audience);
if (!audienceParseResult.hostname) {
throw new Error('VAPID audience is not a url. ' + audience);
}

function validateSubject(subject) {
if (!subject) {
throw new Error('No subject set in vapid.subject.');
}
Expand All @@ -79,30 +53,76 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
throw new Error('Vapid subject is not a url or mailto url. ' + subject);
}
}
}

function validatePublicKey(publicKey) {
if (!publicKey) {
throw new Error('No key set vapid.publicKey');
}

if (!(publicKey instanceof Buffer)) {
throw new Error('Vapid public key is not a buffer.');
if (typeof publicKey !== 'string') {
throw new Error('Vapid public key is must be a URL safe Base 64 ' +
'encoded string.');
}

publicKey = urlBase64.decode(publicKey);

if (publicKey.length !== 65) {
throw new Error('Vapid public key should be 65 bytes long');
throw new Error('Vapid public key should be 65 bytes long when decoded.');
}
}

function validatePrivateKey(privateKey) {
if (!privateKey) {
throw new Error('No key set in vapid.privateKey');
}

if (!(privateKey instanceof Buffer)) {
throw new Error('Vapid private key is not a buffer');
if (typeof privateKey !== 'string') {
throw new Error('Vapid private key must be a URL safe Base 64 ' +
'encoded string.');
}

privateKey = urlBase64.decode(privateKey);

if (privateKey.length !== 32) {
throw new Error('Vapid private key should be 32 bytes long');
throw new Error('Vapid private key should be 32 bytes long when decoded.');
}
}

/**
* 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
* 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
* 'Crypto-Key' values to be used as headers.
*/
function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
if (!audience) {
throw new Error('No audience set in vapid.audience.');
}

if (typeof audience !== 'string' || audience.length === 0) {
throw new Error('The audience value must be a string containing the ' +
'origin of a push service. ' + audience);
}

const audienceParseResult = url.parse(audience);
if (!audienceParseResult.hostname) {
throw new Error('VAPID audience is not a url. ' + audience);
}

validateSubject(subject);
validatePublicKey(publicKey);
validatePrivateKey(privateKey);

publicKey = urlBase64.decode(publicKey);
privateKey = urlBase64.decode(privateKey);


if (expiration) {
// TODO: Check if expiration is valid and use it in place of the hard coded
Expand Down Expand Up @@ -134,5 +154,8 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {

module.exports = {
generateVAPIDKeys: generateVAPIDKeys,
getVapidHeaders: getVapidHeaders
getVapidHeaders: getVapidHeaders,
validateSubject: validateSubject,
validatePublicKey: validatePublicKey,
validatePrivateKey: validatePrivateKey
};
4 changes: 4 additions & 0 deletions src/web-push-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ WebPushLib.prototype.setVapidDetails =
return;
}

vapidHelper.validateSubject(subject);
vapidHelper.validatePublicKey(publicKey);
vapidHelper.validatePrivateKey(privateKey);

vapidDetails = {
subject: subject,
publicKey: publicKey,
Expand Down
119 changes: 119 additions & 0 deletions test/test-set-vapid-details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use strict';

const assert = require('assert');
const urlBase64 = require('urlsafe-base64');
const webPush = require('../src/index');

const VALID_SUBJECT_MAILTO = 'mailto: [email protected]';
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));

suite('setVapidDetails()', function() {
test('is defined', function() {
assert(webPush.setVapidDetails);
});

test('Valid URL input', function() {
assert.doesNotThrow(function() {
webPush.setVapidDetails(VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
});
});

test('Valid mailto: input', function() {
assert.doesNotThrow(function() {
webPush.setVapidDetails(VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
});
});

test('reset Vapid Details with null', function() {
assert.doesNotThrow(function() {
webPush.setVapidDetails(null);
});
});

const invalidInputs = [
{
subject: '',
publicKey: VALID_PUBLIC_KEY,
privateKey: VALID_PRIVATE_KEY
},
{
subject: 'This is not a valid subject',
publicKey: VALID_PUBLIC_KEY,
privateKey: VALID_PRIVATE_KEY
},
{
subject: {},
publicKey: VALID_PUBLIC_KEY,
privateKey: VALID_PRIVATE_KEY
},
{
subject: true,
publicKey: VALID_PUBLIC_KEY,
privateKey: VALID_PRIVATE_KEY
},
{
subject: VALID_SUBJECT_URL,
publicKey: urlBase64.encode(new Buffer(60)),
privateKey: VALID_PRIVATE_KEY
},
{
subject: VALID_SUBJECT_URL,
publicKey: 'This is invalid',
privateKey: VALID_PRIVATE_KEY
},
{
subject: VALID_SUBJECT_URL,
publicKey: '',
privateKey: VALID_PRIVATE_KEY
},
{
subject: VALID_SUBJECT_URL,
publicKey: {},
privateKey: VALID_PRIVATE_KEY
},
{
subject: VALID_SUBJECT_URL,
publicKey: true,
privateKey: VALID_PRIVATE_KEY
},
{
subject: VALID_SUBJECT_URL,
publicKey: VALID_PUBLIC_KEY,
privateKey: urlBase64.encode(new Buffer(60))
},
{
subject: VALID_SUBJECT_URL,
publicKey: VALID_PUBLIC_KEY,
privateKey: 'This is invalid'
},
{
subject: VALID_SUBJECT_URL,
publicKey: VALID_PUBLIC_KEY,
privateKey: ''
},
{
subject: VALID_SUBJECT_URL,
publicKey: VALID_PUBLIC_KEY,
privateKey: {}
},
{
subject: VALID_SUBJECT_URL,
publicKey: VALID_PUBLIC_KEY,
privateKey: true
}
];

test('Invalid input should throw an error', function() {
invalidInputs.forEach(function(invalidInput, index) {
assert.throws(function() {
webPush.setVapidDetails(
invalidInput.subject,
invalidInput.publicKey,
invalidInput.privateKey
);
}, 'Error not thrown on input index: ' + index);
});
});
});
13 changes: 7 additions & 6 deletions test/test-vapid-helper.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use strict';

const assert = require('assert');
const urlBase64 = require('urlsafe-base64');
const webPush = require('../src/index');
const vapidHelper = require('../src/vapid-helper');

const VALID_AUDIENCE = 'https://example.com';
const VALID_SUBJECT_MAILTO = 'mailto: [email protected]';
const VALID_SUBJECT_URL = 'https://exampe.com/contact';
const VALID_PUBLIC_KEY = new Buffer(65);
const VALID_PRIVATE_KEY = new Buffer(32);
const VALID_PUBLIC_KEY = urlBase64.encode(new Buffer(65));
const VALID_PRIVATE_KEY = urlBase64.encode(new Buffer(32));
const VALID_EXPIRATION = Math.floor(Date.now() / 1000) + (60 * 60 * 12);

suite('Test Vapid Helpers', function() {
Expand All @@ -21,11 +22,11 @@ suite('Test Vapid Helpers', function() {
assert(keys.privateKey);
assert(keys.publicKey);

assert.equal(keys.privateKey instanceof Buffer, true);
assert.equal(keys.publicKey instanceof Buffer, true);
assert.equal(typeof keys.privateKey, 'string');
assert.equal(typeof keys.publicKey, 'string');

assert.equal(keys.privateKey.length, 32);
assert.equal(keys.publicKey.length, 65);
assert.equal(urlBase64.decode(keys.privateKey).length, 32);
assert.equal(urlBase64.decode(keys.publicKey).length, 65);
});

test('generate new vapid keys between calls', function() {
Expand Down
9 changes: 5 additions & 4 deletions test/testSelenium.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
}

/* eslint-disable global-require */
const urlBase64 = require('urlsafe-base64');
const seleniumAssistant = require('selenium-assistant');
const webdriver = require('selenium-webdriver');
const seleniumFirefox = require('selenium-webdriver/firefox');
Expand All @@ -26,11 +25,13 @@
require('chromedriver');
/* eslint-enable global-require */

const vapidKeys = webPush.generateVAPIDKeys();

const PUSH_TEST_TIMEOUT = 120 * 1000;
const VAPID_PARAM = {
subject: 'mailto:[email protected]',
privateKey: new Buffer('H6tqEMswzHOFlPHFi2JPfDQRiKN32ZJIwvSPWZl1VTA=', 'base64'),
publicKey: new Buffer('BIx6khu9Z/5lBwNEXYNEOQiL70IKYDpDxsTyoiCb82puQ/V4c/NFdyrBFpWdsz3mikmV6sWARNuhRbbbLTMOmB0=', 'base64')
privateKey: vapidKeys.privateKey,
publicKey: vapidKeys.publicKey
};
const testDirectory = './test/output/';

Expand Down Expand Up @@ -119,7 +120,7 @@
globalDriver = driver;

if (options.vapid) {
testServerURL += '?vapid=' + urlBase64.encode(options.vapid.publicKey);
testServerURL += '?vapid=' + options.vapid.publicKey;
}

return globalDriver.get(testServerURL)
Expand Down

0 comments on commit ab8d884

Please sign in to comment.