From cc26d8a07fa9c0cd348195f74c5a72bea8758647 Mon Sep 17 00:00:00 2001 From: Niklas Mollenhauer Date: Wed, 21 Jun 2023 10:26:56 +0200 Subject: [PATCH] Use builtin encoding `base64url` (#819) Node 16+ supports base64url --- src/encryption-helper.js | 7 +++--- src/urlsafe-base64-helper.js | 33 ++++++--------------------- src/vapid-helper.js | 10 ++++---- src/web-push-lib.js | 2 +- test/test-cli.js | 9 ++++---- test/test-encryption-helper.js | 7 +++--- test/test-generate-request-details.js | 25 ++++++++++---------- test/test-set-vapid-details.js | 9 ++++---- test/test-vapid-helper.js | 13 +++++------ test/testSendNotification.js | 27 +++++++++++----------- 10 files changed, 58 insertions(+), 84 deletions(-) diff --git a/src/encryption-helper.js b/src/encryption-helper.js index 3c11571a..779e2ad0 100644 --- a/src/encryption-helper.js +++ b/src/encryption-helper.js @@ -2,7 +2,6 @@ const crypto = require('crypto'); const ece = require('http_ece'); -const urlBase64Helper = require('./urlsafe-base64-helper'); const encrypt = function(userPublicKey, userAuth, payload, contentEncoding) { if (!userPublicKey) { @@ -13,7 +12,7 @@ const encrypt = function(userPublicKey, userAuth, payload, contentEncoding) { throw new Error('The subscription p256dh value must be a string.'); } - if (urlBase64Helper.decode(userPublicKey).length !== 65) { + if (Buffer.from(userPublicKey, 'base64url').length !== 65) { throw new Error('The subscription p256dh value should be 65 bytes long.'); } @@ -25,7 +24,7 @@ const encrypt = function(userPublicKey, userAuth, payload, contentEncoding) { throw new Error('The subscription auth key must be a string.'); } - if (urlBase64Helper.decode(userAuth).length < 16) { + if (Buffer.from(userAuth, 'base64url').length < 16) { throw new Error('The subscription auth key should be at least 16 ' + 'bytes long'); } @@ -41,7 +40,7 @@ const encrypt = function(userPublicKey, userAuth, payload, contentEncoding) { const localCurve = crypto.createECDH('prime256v1'); const localPublicKey = localCurve.generateKeys(); - const salt = urlBase64Helper.encode(crypto.randomBytes(16)); + const salt = crypto.randomBytes(16).toString('base64url'); const cipherText = ece.encrypt(payload, { version: contentEncoding, diff --git a/src/urlsafe-base64-helper.js b/src/urlsafe-base64-helper.js index 6a7a6c2e..30650fa2 100644 --- a/src/urlsafe-base64-helper.js +++ b/src/urlsafe-base64-helper.js @@ -1,32 +1,13 @@ -// Largely ported from https://github.com/RGBboy/urlsafe-base64 - 'use strict'; -function encode(buffer) { - return buffer.toString('base64') - .replace(/\+/g, '-') // Convert '+' to '-' - .replace(/\//g, '_') // Convert '/' to '_' - .replace(/=+$/, ''); // Remove ending '=' -} - -function decode(base64) { - // Add removed at end '=' - base64 += Array(5 - (base64.length % 4)).join('='); - - base64 = base64 - .replace(/-/g, '+') // Convert '-' to '+' - .replace(/_/g, '/'); // Convert '_' to '/' - - // change from urlsafe-base64 since new Buffer() is deprecated - return Buffer.from(base64, 'base64'); -} - +/** + * @param {string} base64 + * @returns {boolean} + */ function validate(base64) { - return /^[A-Za-z0-9\-_]+$/.test(base64); - } + return /^[A-Za-z0-9\-_]+$/.test(base64); +} module.exports = { - encode: encode, - decode: decode, - validate: validate + validate: validate }; diff --git a/src/vapid-helper.js b/src/vapid-helper.js index 9329f048..0302593d 100644 --- a/src/vapid-helper.js +++ b/src/vapid-helper.js @@ -60,8 +60,8 @@ function generateVAPIDKeys() { } return { - publicKey: urlBase64Helper.encode(publicKeyBuffer), - privateKey: urlBase64Helper.encode(privateKeyBuffer) + publicKey: publicKeyBuffer.toString('base64url'), + privateKey: privateKeyBuffer.toString('base64url') }; } @@ -104,7 +104,7 @@ function validatePublicKey(publicKey) { throw new Error('Vapid public key must be a URL safe Base 64 (without "=")'); } - publicKey = urlBase64Helper.decode(publicKey); + publicKey = Buffer.from(publicKey, 'base64url'); if (publicKey.length !== 65) { throw new Error('Vapid public key should be 65 bytes long when decoded.'); @@ -125,7 +125,7 @@ function validatePrivateKey(privateKey) { throw new Error('Vapid private key must be a URL safe Base 64 (without "=")'); } - privateKey = urlBase64Helper.decode(privateKey); + privateKey = Buffer.from(privateKey, 'base64url'); if (privateKey.length !== 32) { throw new Error('Vapid private key should be 32 bytes long when decoded.'); @@ -203,7 +203,7 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, contentEncodi validatePublicKey(publicKey); validatePrivateKey(privateKey); - privateKey = urlBase64Helper.decode(privateKey); + privateKey = Buffer.from(privateKey, 'base64url'); if (expiration) { validateExpiration(expiration); diff --git a/src/web-push-lib.js b/src/web-push-lib.js index 1fdb73d3..4cf82f8f 100644 --- a/src/web-push-lib.js +++ b/src/web-push-lib.js @@ -251,7 +251,7 @@ WebPushLib.prototype.generateRequestDetails = function(subscription, payload, op } 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=' + urlBase64Helper.encode(encrypted.localPublicKey); + requestDetails.headers['Crypto-Key'] = 'dh=' + encrypted.localPublicKey.toString('base64url'); } requestPayload = encrypted.cipherText; diff --git a/test/test-cli.js b/test/test-cli.js index 7174bbd1..471b13af 100644 --- a/test/test-cli.js +++ b/test/test-cli.js @@ -9,7 +9,6 @@ const assert = require('assert'); const spawn = require('child_process').spawn; - const urlBase64Helper = require('../src/urlsafe-base64-helper'); const cliPath = 'src/cli.js'; @@ -147,13 +146,13 @@ return line.indexOf('Public Key:') !== -1; }); const publicKey = lines[publicKeyTitleIndex + 1].trim(); - assert.equal(urlBase64Helper.decode(publicKey).length, 65); + assert.equal(Buffer.from(publicKey, 'base64url').length, 65); const privateKeyTitleIndex = lines.findIndex(function(line) { return line.indexOf('Private Key:') !== -1; }); const privateKey = lines[privateKeyTitleIndex + 1].trim(); - assert.equal(urlBase64Helper.decode(privateKey).length, 32); + assert.equal(Buffer.from(privateKey, 'base64url').length, 32); resolve(); }); }); @@ -185,8 +184,8 @@ assert(vapidKeys.publicKey); assert(vapidKeys.privateKey); - assert.equal(urlBase64Helper.decode(vapidKeys.privateKey).length, 32); - assert.equal(urlBase64Helper.decode(vapidKeys.publicKey).length, 65); + assert.equal(Buffer.from(vapidKeys.privateKey, 'base64url').length, 32); + assert.equal(Buffer.from(vapidKeys.publicKey, 'base64url').length, 65); resolve(); }); diff --git a/test/test-encryption-helper.js b/test/test-encryption-helper.js index 5476bf54..364d3b5f 100644 --- a/test/test-encryption-helper.js +++ b/test/test-encryption-helper.js @@ -3,12 +3,11 @@ const assert = require('assert'); const crypto = require('crypto'); const webPush = require('../src/index'); -const urlBase64Helper = require('../src/urlsafe-base64-helper'); const ece = require('http_ece'); const userCurve = crypto.createECDH('prime256v1'); -const VALID_PUBLIC_KEY = urlBase64Helper.encode(userCurve.generateKeys()); -const VALID_AUTH = urlBase64Helper.encode(crypto.randomBytes(16)); +const VALID_PUBLIC_KEY = userCurve.generateKeys().toString('base64url'); +const VALID_AUTH = crypto.randomBytes(16).toString('base64url'); suite('Test Encryption Helpers', function() { test('is defined', function() { @@ -20,7 +19,7 @@ suite('Test Encryption Helpers', function() { return ece.decrypt(encrypted.cipherText, { version: contentEncoding, - dh: urlBase64Helper.encode(encrypted.localPublicKey), + dh: encrypted.localPublicKey.toString('base64url'), privateKey: userCurve, salt: encrypted.salt, authSecret: VALID_AUTH diff --git a/test/test-generate-request-details.js b/test/test-generate-request-details.js index 4361bad1..d4501e25 100644 --- a/test/test-generate-request-details.js +++ b/test/test-generate-request-details.js @@ -2,7 +2,6 @@ const assert = require('assert'); const webPush = require('../src/index'); -const urlBase64Helper = require('../src/urlsafe-base64-helper'); const crypto = require('crypto'); const jws = require('jws'); const urlParse = require('url').parse; @@ -19,8 +18,8 @@ suite('Test Generate Request Details', function() { const vapidKeys = require('../src/vapid-helper').generateVAPIDKeys(); const VALID_KEYS = { - p256dh: urlBase64Helper.encode(userPublicKey), - auth: urlBase64Helper.encode(userAuth) + p256dh: userPublicKey.toString('base64url'), + auth: userAuth.toString('base64url') }; const invalidRequests = [ @@ -85,7 +84,7 @@ suite('Test Generate Request Details', function() { subscription: { endpoint: true, keys: { - p256dh: urlBase64Helper.encode(userPublicKey) + p256dh: userPublicKey.toString('base64url') } }, message: 'hello' @@ -96,7 +95,7 @@ suite('Test Generate Request Details', function() { subscription: { endpoint: true, keys: { - auth: urlBase64Helper.encode(userAuth) + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -107,7 +106,7 @@ suite('Test Generate Request Details', function() { subscription: { keys: { p256dh: userPublicKey, - auth: urlBase64Helper.encode(userAuth) + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -118,7 +117,7 @@ suite('Test Generate Request Details', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(userPublicKey), + p256dh: userPublicKey.toString('base64url'), auth: userAuth } }, @@ -130,8 +129,8 @@ suite('Test Generate Request Details', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(Buffer.concat([userPublicKey, Buffer.alloc(1)])), - auth: urlBase64Helper.encode(userAuth) + p256dh: Buffer.concat([userPublicKey, Buffer.alloc(1)]).toString('base64url'), + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -142,8 +141,8 @@ suite('Test Generate Request Details', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(userPublicKey.slice(1)), - auth: urlBase64Helper.encode(userAuth) + p256dh: userPublicKey.slice(1).toString('base64url'), + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -154,8 +153,8 @@ suite('Test Generate Request Details', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(userPublicKey), - auth: urlBase64Helper.encode(userAuth.slice(1)) + p256dh: userPublicKey.toString('base64url'), + auth: userAuth.slice(1).toString('base64url') } }, message: 'hello' diff --git a/test/test-set-vapid-details.js b/test/test-set-vapid-details.js index 36cd956c..fe9b3ac2 100644 --- a/test/test-set-vapid-details.js +++ b/test/test-set-vapid-details.js @@ -2,12 +2,11 @@ const assert = require('assert'); const webPush = require('../src/index'); -const urlBase64Helper = require('../src/urlsafe-base64-helper'); const VALID_SUBJECT_MAILTO = 'mailto: example@example.com'; const VALID_SUBJECT_URL = 'https://exampe.com/contact'; -const VALID_PUBLIC_KEY = urlBase64Helper.encode(Buffer.alloc(65)); -const VALID_PRIVATE_KEY = urlBase64Helper.encode(Buffer.alloc(32)); +const VALID_PUBLIC_KEY = Buffer.alloc(65).toString('base64url'); +const VALID_PRIVATE_KEY = Buffer.alloc(32).toString('base64url'); suite('setVapidDetails()', function() { test('is defined', function() { @@ -55,7 +54,7 @@ suite('setVapidDetails()', function() { }, { subject: VALID_SUBJECT_URL, - publicKey: urlBase64Helper.encode(Buffer.alloc(60)), + publicKey: Buffer.alloc(60).toString('base64url'), privateKey: VALID_PRIVATE_KEY }, { @@ -81,7 +80,7 @@ suite('setVapidDetails()', function() { { subject: VALID_SUBJECT_URL, publicKey: VALID_PUBLIC_KEY, - privateKey: urlBase64Helper.encode(Buffer.alloc(60)) + privateKey: Buffer.alloc(60).toString('base64url') }, { subject: VALID_SUBJECT_URL, diff --git a/test/test-vapid-helper.js b/test/test-vapid-helper.js index a3624877..949a352d 100644 --- a/test/test-vapid-helper.js +++ b/test/test-vapid-helper.js @@ -6,7 +6,6 @@ const crypto = require('crypto'); const mocha = require('mocha'); const webPush = require('../src/index'); const vapidHelper = require('../src/vapid-helper'); -const urlBase64Helper = require('../src/urlsafe-base64-helper'); const VALID_AUDIENCE = 'https://example.com'; const VALID_SUBJECT_MAILTO = 'mailto:example@example.com'; @@ -15,9 +14,9 @@ const VALID_SUBJECT_URL = 'https://example.com/contact'; const WARN_SUBJECT_LOCALHOST_URL = 'https://localhost'; const INVALID_SUBJECT_URL_1 = 'http://example.gov'; const INVALID_SUBJECT_URL_2 = 'ftp://example.net'; -const VALID_PUBLIC_KEY = urlBase64Helper.encode(Buffer.alloc(65)); +const VALID_PUBLIC_KEY = Buffer.alloc(65).toString('base64url'); const VALID_UNSAFE_BASE64_PUBLIC_KEY = Buffer.alloc(65).toString('base64'); -const VALID_PRIVATE_KEY = urlBase64Helper.encode(Buffer.alloc(32)); +const VALID_PRIVATE_KEY = Buffer.alloc(32).toString('base64url'); const VALID_UNSAFE_BASE64_PRIVATE_KEY = Buffer.alloc(32).toString('base64'); const VALID_CONTENT_ENCODING = webPush.supportedContentEncodings.AES_GCM; const VALID_EXPIRATION = Math.floor(Date.now() / 1000) + (60 * 60 * 12); @@ -46,8 +45,8 @@ suite('Test Vapid Helpers', function() { assert.equal(typeof keys.privateKey, 'string'); assert.equal(typeof keys.publicKey, 'string'); - assert.equal(urlBase64Helper.decode(keys.privateKey).length, 32); - assert.equal(urlBase64Helper.decode(keys.publicKey).length, 65); + assert.equal(Buffer.from(keys.privateKey, 'base64url').length, 32); + assert.equal(Buffer.from(keys.publicKey, 'base64url').length, 65); }); test('generate vapid keys with padding', function() { @@ -66,8 +65,8 @@ suite('Test Vapid Helpers', function() { assert.equal(typeof keys.privateKey, 'string'); assert.equal(typeof keys.publicKey, 'string'); - assert.equal(urlBase64Helper.decode(keys.privateKey).length, 32); - assert.equal(urlBase64Helper.decode(keys.publicKey).length, 65); + assert.equal(Buffer.from(keys.privateKey, 'base64url').length, 32); + assert.equal(Buffer.from(keys.publicKey, 'base64url').length, 65); }); test('generate new vapid keys between calls', function() { diff --git a/test/testSendNotification.js b/test/testSendNotification.js index b55310b5..484c0aa5 100644 --- a/test/testSendNotification.js +++ b/test/testSendNotification.js @@ -10,7 +10,6 @@ const portfinder = require('portfinder'); const jws = require('jws'); const mocha = require('mocha'); const WebPushConstants = require('../src/web-push-constants.js'); -const urlBase64Helper = require('../src/urlsafe-base64-helper'); suite('sendNotification', function() { test('is defined', function() { @@ -62,8 +61,8 @@ suite('sendNotification', function() { const userAuth = crypto.randomBytes(16); const VALID_KEYS = { - p256dh: urlBase64Helper.encode(userPublicKey), - auth: urlBase64Helper.encode(userAuth) + p256dh: userPublicKey.toString('base64url'), + auth: userAuth.toString('base64url') }; const vapidKeys = require('../src/vapid-helper').generateVAPIDKeys(); @@ -166,7 +165,7 @@ suite('sendNotification', function() { privateKey: userCurve, dh: appServerPublicKey, salt: salt, - authSecret: urlBase64Helper.encode(userAuth) + authSecret: userAuth.toString('base64url') }); assert(decrypted.equals(Buffer.from(options.message)), 'Check cipher text can be correctly decoded'); @@ -671,7 +670,7 @@ suite('sendNotification', function() { subscription: { endpoint: true, keys: { - p256dh: urlBase64Helper.encode(userPublicKey) + p256dh: userPublicKey.toString('base64url') } }, message: 'hello' @@ -682,7 +681,7 @@ suite('sendNotification', function() { subscription: { endpoint: true, keys: { - auth: urlBase64Helper.encode(userAuth) + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -693,7 +692,7 @@ suite('sendNotification', function() { subscription: { keys: { p256dh: userPublicKey, - auth: urlBase64Helper.encode(userAuth) + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -704,7 +703,7 @@ suite('sendNotification', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(userPublicKey), + p256dh: userPublicKey.toString('base64url'), auth: userAuth } }, @@ -716,8 +715,8 @@ suite('sendNotification', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(Buffer.concat([userPublicKey, Buffer.alloc(1)])), - auth: urlBase64Helper.encode(userAuth) + p256dh: Buffer.concat([userPublicKey, Buffer.alloc(1)]).toString('base64url'), + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -728,8 +727,8 @@ suite('sendNotification', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(userPublicKey.slice(1)), - auth: urlBase64Helper.encode(userAuth) + p256dh: userPublicKey.slice(1).toString('base64url'), + auth: userAuth.toString('base64url') } }, message: 'hello' @@ -740,8 +739,8 @@ suite('sendNotification', function() { requestOptions: { subscription: { keys: { - p256dh: urlBase64Helper.encode(userPublicKey), - auth: urlBase64Helper.encode(userAuth.slice(1)) + p256dh: userPublicKey.toString('base64url'), + auth: userAuth.slice(1).toString('base64url') } }, message: 'hello'