/cartridges/bm_push/cartridge/scripts/helpers/vapid-helper.js
https://gitlab.com/zautre/dw-push · JavaScript · 301 lines · 170 code · 31 blank · 100 comment · 17 complexity · 50ac7a510b58b97808940dc134598109 MD5 · raw file
- 'use strict';
- /**
- * This module provides helper functions for the Voluntary Application Server Identification for Web Push (VAPID) protocol.
- *
- * @module helpers/vapid-helper
- */
- let Site = require( 'dw/system/Site' );
- let Logger = require( 'dw/system/Logger' );
- let Bytes = require( 'dw/util/Bytes' );
- let Encoding = require( 'dw/crypto/Encoding' );
- let Transaction = require( 'dw/system/Transaction' );
- let SecureRandom = require( 'dw/crypto/SecureRandom' );
- let sjcl = require( '~/cartridge/scripts/sjcl-1.0.6/sjcl-1.0.6.js' );
- let encryptionHelper = require( '~/cartridge/scripts/helpers/encryption-helper.js' );
- let urlBase64 = require( '~/cartridge/scripts/helpers/urlsafe-base64.js' );
- let log = Logger.getLogger( "[vapid-helper.js]" );
- module.exports = {
- getVAPIDKeys : getVAPIDKeys,
- generateVAPIDKeys : generateVAPIDKeys,
- getVapidHeaders : getVapidHeaders,
- validateSubject : validateSubject,
- validatePublicKey : validatePublicKey,
- validatePrivateKey : validatePrivateKey,
- saveVAPIDKeys : saveVAPIDKeys
- };
- /**
- * Get public and private key for
- * Voluntary Application Server Identification for Web Push (VAPID) protocol
- *
- * @return {VAPIDKeyPair} VAPID key pair object
- */
- function getVAPIDKeys()
- {
- return {
- publicKey: Site.getCurrent().getCustomPreferenceValue( 'VAPIDPublicKey' ),
- privateKey: Site.getCurrent().getCustomPreferenceValue( 'VAPIDPrivateKey' )
- };
- }
- /**
- * Generate ECDH public and private key pair for
- * Voluntary Application Server Identification for Web Push (VAPID) protocol
- *
- * @return {VAPIDKeyPair} VAPID key pair object
- */
- function generateVAPIDKeys()
- {
- let VAPIDKeyPair = encryptionHelper.generateECDSAKeys();
-
- /**
- * @typedef {Object} VAPIDKeyPair
- * @property {string} VAPIDPublicKey Base64 encoded EC Public Key
- * @property {string} VAPIDPrivateKey Base64 encoded EC Private Key
- */
- return {
- VAPIDPublicKey: VAPIDKeyPair.ECPublicKey,
- VAPIDPrivateKey: VAPIDKeyPair.ECPrivateKey
- };
- }
- /**
- * Save VAPID ECDH key pair to corresponding site preferences
- *
- * @param {VAPIDKeyPair} VAPIDKeyPair VAPIDKeyPair.
- *
- * @return {void}
- */
- function saveVAPIDKeys( VAPIDKeyPair )
- {
- try {
- Transaction.wrap( function()
- {
- Site.getCurrent().setCustomPreferenceValue( 'VAPIDPublicKey', VAPIDKeyPair.VAPIDPublicKey );
- Site.getCurrent().setCustomPreferenceValue( 'VAPIDPrivateKey', VAPIDKeyPair.VAPIDPrivateKey );
- } );
- log.debug( 'Saved VAPID Key pair {0} to site preference', JSON.stringify( VAPIDKeyPair ) );
- } catch ( e ) {
- log.error( "Could not save VAPIDKeyPair.{0}", e.message );
- }
- }
- /**
- * Validate VAPID subject
- *
- * @param {string} subject VAPID subject.
- *
- * @return {void}
- */
- function validateSubject( subject )
- {
- if( !subject )
- {
- log.error( 'No subject set in vapidDetails.subject.' );
- }
- if( typeof subject !== 'string' || subject.length === 0 )
- {
- log.error( 'The subject value must be a string containing a URL or mailto: address. {0} ', subject );
- }
- }
- /**
- * Validate VAPID public key
- *
- * @param {string} publicKey VAPID public key.
- *
- * @return {void}
- */
- function validatePublicKey( publicKey )
- {
- if( !publicKey )
- {
- log.error( 'No key set vapidDetails.publicKey' );
- }
- if( typeof publicKey !== 'string' )
- {
- log.error( 'Vapid public key is must be a URL safe Base 64 encoded string.' );
- }
- if( urlBase64.decode( publicKey ).length !== 65 )
- {
- log.error( 'Vapid public key should be 65 bytes long when decoded.' );
- }
- }
- /**
- * Validate VAPID private key
- *
- * @param {string} privateKey VAPID private key.
- *
- * @return {void}
- */
- function validatePrivateKey( privateKey )
- {
- if( !privateKey )
- {
- log.error( 'No key set in vapidDetails.privateKey' );
- }
- if( typeof privateKey !== 'string' )
- {
- log.error( 'Vapid private key must be a URL safe Base 64 encoded string.' );
- }
- if( urlBase64.decode( privateKey ).length !== 32 )
- {
- log.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 {string} publicKey The VAPID public key.
- * @param {string} privateKey The VAPID private key.
- * @param {integer} [expiration] The expiration of the VAPID JWT.
- *
- * @return {VAPIDHeaders} Returns an Object with the Authorization and 'Crypto-Key' values to be used as headers.
- */
- function getVapidHeaders( audience, subject, publicKey, privateKey, expiration )
- {
- //set expiraton to 12 hours
- let exp = Math.floor( Date.now() / 1000 ) + 43200;
-
- validateVAPID( audience, subject, publicKey, privateKey, expiration );
-
- if( expiration && ( typeof expiration === 'number' ) )
- {
- exp = expiration;
- }
-
- let header = _JSONToBase64URLSaveString( {
- typ: 'JWT',
- alg: 'ES256'
- } );
- let jwtPayload = _JSONToBase64URLSaveString( {
- aud: audience,
- exp: exp,
- sub: subject
- } );
-
- let data = header + "." + jwtPayload;
- log.debug( 'header + "." + jwtPayload: {0}', data );
- log.debug( 'privateKey: {0}', privateKey );
- log.debug( 'publicKey: {0}', publicKey );
-
- //seed the random generator
- _seedDRGB();
-
- // Unserialize private key:
- let sec = new sjcl.ecc.ecdsa.secretKey(
- sjcl.ecc.curves.c256,
- sjcl.ecc.curves.c256.field.fromBits( sjcl.codec.base64.toBits( privateKey ) )
- );
- log.debug( 'succesfully unserialized privateKey' );
-
- //publicKey is 65 bytes, strip the fixed leading 1 Byte in order to use it with SJCL
- let publicKeySegment = Encoding.fromBase64( publicKey ).bytesAt( 1,64 );
- let publicKeyBase64 = Encoding.toBase64( publicKeySegment );
-
- // Unserialize the public key:
- let pub = new sjcl.ecc.ecdsa.publicKey( sjcl.ecc.curves.c256, sjcl.codec.base64.toBits( publicKeyBase64 ) );
- log.debug( 'succesfully unserialized publicKey' );
-
- //hash the data
- let hashedData = sjcl.hash.sha256.hash( data );
-
- //sign the data using the private key derived above
- let jwtBitArray = sec.sign( hashedData );
-
- // converting the bit array to a base64 string
- let jwt = sjcl.codec.base64.fromBits( jwtBitArray );
-
- // verify signature
- let isValidSignature = pub.verify( hashedData, jwtBitArray );
- if( !isValidSignature )
- {
- log.error( 'Signature verification does not pass.' );
- }
- log.debug( 'isValidSignature: {0}', isValidSignature );
-
- /**
- * @typedef {Object} VAPIDHeaders
- * @property {string} Authorization Authorization header
- * @property {string} Crypto-Key Crypto-Key header
- */
- return {
- Authorization: 'WebPush ' + data + "." + urlBase64.encode( Encoding.fromBase64( jwt ) ),
- 'Crypto-Key': 'p256ecdsa=' + urlBase64.toURLSave( publicKey )
- };
- }
- /**
- * This method takes the required VAPID parameters and check for their validity.
- *
- * @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 {string} publicKey The VAPID public key.
- * @param {string} privateKey The VAPID private key.
- * @param {integer} [expiration] The expiration of the VAPID JWT.
- *
- * @return {void}
- */
- function validateVAPID( audience, subject, publicKey, privateKey, expiration )
- {
- if( !audience )
- {
- log.error( 'No audience could be generated for VAPID.' );
- }
- if( typeof audience !== 'string' || audience.length === 0 )
- {
- log.error( 'The audience value must be a string containing the origin of a push service. {0}', audience );
- }
-
- let urlPattern = new RegExp( '^(https?:\\/\\/)?'+ // protocol
- '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|'+ // domain name
- '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
- '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
- '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
- '(\\#[-a-z\\d_]*)?$','i' ); // fragment locator
- if( !urlPattern.test( audience ) )
- {
- log.error( 'VAPID audience is not a url. {0}', audience );
- }
- validateSubject( subject );
- validatePublicKey( publicKey );
- validatePrivateKey( privateKey );
- }
- /**
- * This method seeds the random number generator in SJCL.
- *
- * @return {void}
- */
- function _seedDRGB()
- {
- //Generating VAPIDKeys using server side SJCL, quite slow for sandbox
- let random = new SecureRandom();
- //seeding SJCL pseudo-random number generator
- let estimatedEntropy = random.nextBytes( 1024 / 8 ); // 128 bytes
- sjcl.random.addEntropy( estimatedEntropy.toString(), 1024, "dw/crypto/SecureRandom" );
- }
- /**
- * This method converts JSON to base64 encoded URL save string.
- *
- * @param {string} json json string
- *
- * @return {string} base64 encoded URL save JSON string
- */
- function _JSONToBase64URLSaveString( json )
- {
- let JSONstring = JSON.stringify( json, 0 );
- let jsonToBytesArray = new Bytes( JSONstring );
-
- return urlBase64.encode( jsonToBytesArray );
- }