PageRenderTime 80ms CodeModel.GetById 43ms RepoModel.GetById 9ms app.codeStats 0ms

/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
  1. 'use strict';
  2. /**
  3. * This module provides helper functions for the Voluntary Application Server Identification for Web Push (VAPID) protocol.
  4. *
  5. * @module helpers/vapid-helper
  6. */
  7. let Site = require( 'dw/system/Site' );
  8. let Logger = require( 'dw/system/Logger' );
  9. let Bytes = require( 'dw/util/Bytes' );
  10. let Encoding = require( 'dw/crypto/Encoding' );
  11. let Transaction = require( 'dw/system/Transaction' );
  12. let SecureRandom = require( 'dw/crypto/SecureRandom' );
  13. let sjcl = require( '~/cartridge/scripts/sjcl-1.0.6/sjcl-1.0.6.js' );
  14. let encryptionHelper = require( '~/cartridge/scripts/helpers/encryption-helper.js' );
  15. let urlBase64 = require( '~/cartridge/scripts/helpers/urlsafe-base64.js' );
  16. let log = Logger.getLogger( "[vapid-helper.js]" );
  17. module.exports = {
  18. getVAPIDKeys : getVAPIDKeys,
  19. generateVAPIDKeys : generateVAPIDKeys,
  20. getVapidHeaders : getVapidHeaders,
  21. validateSubject : validateSubject,
  22. validatePublicKey : validatePublicKey,
  23. validatePrivateKey : validatePrivateKey,
  24. saveVAPIDKeys : saveVAPIDKeys
  25. };
  26. /**
  27. * Get public and private key for
  28. * Voluntary Application Server Identification for Web Push (VAPID) protocol
  29. *
  30. * @return {VAPIDKeyPair} VAPID key pair object
  31. */
  32. function getVAPIDKeys()
  33. {
  34. return {
  35. publicKey: Site.getCurrent().getCustomPreferenceValue( 'VAPIDPublicKey' ),
  36. privateKey: Site.getCurrent().getCustomPreferenceValue( 'VAPIDPrivateKey' )
  37. };
  38. }
  39. /**
  40. * Generate ECDH public and private key pair for
  41. * Voluntary Application Server Identification for Web Push (VAPID) protocol
  42. *
  43. * @return {VAPIDKeyPair} VAPID key pair object
  44. */
  45. function generateVAPIDKeys()
  46. {
  47. let VAPIDKeyPair = encryptionHelper.generateECDSAKeys();
  48. /**
  49. * @typedef {Object} VAPIDKeyPair
  50. * @property {string} VAPIDPublicKey Base64 encoded EC Public Key
  51. * @property {string} VAPIDPrivateKey Base64 encoded EC Private Key
  52. */
  53. return {
  54. VAPIDPublicKey: VAPIDKeyPair.ECPublicKey,
  55. VAPIDPrivateKey: VAPIDKeyPair.ECPrivateKey
  56. };
  57. }
  58. /**
  59. * Save VAPID ECDH key pair to corresponding site preferences
  60. *
  61. * @param {VAPIDKeyPair} VAPIDKeyPair VAPIDKeyPair.
  62. *
  63. * @return {void}
  64. */
  65. function saveVAPIDKeys( VAPIDKeyPair )
  66. {
  67. try {
  68. Transaction.wrap( function()
  69. {
  70. Site.getCurrent().setCustomPreferenceValue( 'VAPIDPublicKey', VAPIDKeyPair.VAPIDPublicKey );
  71. Site.getCurrent().setCustomPreferenceValue( 'VAPIDPrivateKey', VAPIDKeyPair.VAPIDPrivateKey );
  72. } );
  73. log.debug( 'Saved VAPID Key pair {0} to site preference', JSON.stringify( VAPIDKeyPair ) );
  74. } catch ( e ) {
  75. log.error( "Could not save VAPIDKeyPair.{0}", e.message );
  76. }
  77. }
  78. /**
  79. * Validate VAPID subject
  80. *
  81. * @param {string} subject VAPID subject.
  82. *
  83. * @return {void}
  84. */
  85. function validateSubject( subject )
  86. {
  87. if( !subject )
  88. {
  89. log.error( 'No subject set in vapidDetails.subject.' );
  90. }
  91. if( typeof subject !== 'string' || subject.length === 0 )
  92. {
  93. log.error( 'The subject value must be a string containing a URL or mailto: address. {0} ', subject );
  94. }
  95. }
  96. /**
  97. * Validate VAPID public key
  98. *
  99. * @param {string} publicKey VAPID public key.
  100. *
  101. * @return {void}
  102. */
  103. function validatePublicKey( publicKey )
  104. {
  105. if( !publicKey )
  106. {
  107. log.error( 'No key set vapidDetails.publicKey' );
  108. }
  109. if( typeof publicKey !== 'string' )
  110. {
  111. log.error( 'Vapid public key is must be a URL safe Base 64 encoded string.' );
  112. }
  113. if( urlBase64.decode( publicKey ).length !== 65 )
  114. {
  115. log.error( 'Vapid public key should be 65 bytes long when decoded.' );
  116. }
  117. }
  118. /**
  119. * Validate VAPID private key
  120. *
  121. * @param {string} privateKey VAPID private key.
  122. *
  123. * @return {void}
  124. */
  125. function validatePrivateKey( privateKey )
  126. {
  127. if( !privateKey )
  128. {
  129. log.error( 'No key set in vapidDetails.privateKey' );
  130. }
  131. if( typeof privateKey !== 'string' )
  132. {
  133. log.error( 'Vapid private key must be a URL safe Base 64 encoded string.' );
  134. }
  135. if( urlBase64.decode( privateKey ).length !== 32 )
  136. {
  137. log.error( 'Vapid private key should be 32 bytes long when decoded.' );
  138. }
  139. }
  140. /**
  141. * This method takes the required VAPID parameters and returns the required header to be added to a Web Push Protocol Request.
  142. *
  143. * @param {string} audience This must be the origin of the push service.
  144. * @param {string} subject This should be a URL or a 'mailto:' email address.
  145. * @param {string} publicKey The VAPID public key.
  146. * @param {string} privateKey The VAPID private key.
  147. * @param {integer} [expiration] The expiration of the VAPID JWT.
  148. *
  149. * @return {VAPIDHeaders} Returns an Object with the Authorization and 'Crypto-Key' values to be used as headers.
  150. */
  151. function getVapidHeaders( audience, subject, publicKey, privateKey, expiration )
  152. {
  153. //set expiraton to 12 hours
  154. let exp = Math.floor( Date.now() / 1000 ) + 43200;
  155. validateVAPID( audience, subject, publicKey, privateKey, expiration );
  156. if( expiration && ( typeof expiration === 'number' ) )
  157. {
  158. exp = expiration;
  159. }
  160. let header = _JSONToBase64URLSaveString( {
  161. typ: 'JWT',
  162. alg: 'ES256'
  163. } );
  164. let jwtPayload = _JSONToBase64URLSaveString( {
  165. aud: audience,
  166. exp: exp,
  167. sub: subject
  168. } );
  169. let data = header + "." + jwtPayload;
  170. log.debug( 'header + "." + jwtPayload: {0}', data );
  171. log.debug( 'privateKey: {0}', privateKey );
  172. log.debug( 'publicKey: {0}', publicKey );
  173. //seed the random generator
  174. _seedDRGB();
  175. // Unserialize private key:
  176. let sec = new sjcl.ecc.ecdsa.secretKey(
  177. sjcl.ecc.curves.c256,
  178. sjcl.ecc.curves.c256.field.fromBits( sjcl.codec.base64.toBits( privateKey ) )
  179. );
  180. log.debug( 'succesfully unserialized privateKey' );
  181. //publicKey is 65 bytes, strip the fixed leading 1 Byte in order to use it with SJCL
  182. let publicKeySegment = Encoding.fromBase64( publicKey ).bytesAt( 1,64 );
  183. let publicKeyBase64 = Encoding.toBase64( publicKeySegment );
  184. // Unserialize the public key:
  185. let pub = new sjcl.ecc.ecdsa.publicKey( sjcl.ecc.curves.c256, sjcl.codec.base64.toBits( publicKeyBase64 ) );
  186. log.debug( 'succesfully unserialized publicKey' );
  187. //hash the data
  188. let hashedData = sjcl.hash.sha256.hash( data );
  189. //sign the data using the private key derived above
  190. let jwtBitArray = sec.sign( hashedData );
  191. // converting the bit array to a base64 string
  192. let jwt = sjcl.codec.base64.fromBits( jwtBitArray );
  193. // verify signature
  194. let isValidSignature = pub.verify( hashedData, jwtBitArray );
  195. if( !isValidSignature )
  196. {
  197. log.error( 'Signature verification does not pass.' );
  198. }
  199. log.debug( 'isValidSignature: {0}', isValidSignature );
  200. /**
  201. * @typedef {Object} VAPIDHeaders
  202. * @property {string} Authorization Authorization header
  203. * @property {string} Crypto-Key Crypto-Key header
  204. */
  205. return {
  206. Authorization: 'WebPush ' + data + "." + urlBase64.encode( Encoding.fromBase64( jwt ) ),
  207. 'Crypto-Key': 'p256ecdsa=' + urlBase64.toURLSave( publicKey )
  208. };
  209. }
  210. /**
  211. * This method takes the required VAPID parameters and check for their validity.
  212. *
  213. * @param {string} audience This must be the origin of the push service.
  214. * @param {string} subject This should be a URL or a 'mailto:' email address.
  215. * @param {string} publicKey The VAPID public key.
  216. * @param {string} privateKey The VAPID private key.
  217. * @param {integer} [expiration] The expiration of the VAPID JWT.
  218. *
  219. * @return {void}
  220. */
  221. function validateVAPID( audience, subject, publicKey, privateKey, expiration )
  222. {
  223. if( !audience )
  224. {
  225. log.error( 'No audience could be generated for VAPID.' );
  226. }
  227. if( typeof audience !== 'string' || audience.length === 0 )
  228. {
  229. log.error( 'The audience value must be a string containing the origin of a push service. {0}', audience );
  230. }
  231. let urlPattern = new RegExp( '^(https?:\\/\\/)?'+ // protocol
  232. '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|'+ // domain name
  233. '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
  234. '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
  235. '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
  236. '(\\#[-a-z\\d_]*)?$','i' ); // fragment locator
  237. if( !urlPattern.test( audience ) )
  238. {
  239. log.error( 'VAPID audience is not a url. {0}', audience );
  240. }
  241. validateSubject( subject );
  242. validatePublicKey( publicKey );
  243. validatePrivateKey( privateKey );
  244. }
  245. /**
  246. * This method seeds the random number generator in SJCL.
  247. *
  248. * @return {void}
  249. */
  250. function _seedDRGB()
  251. {
  252. //Generating VAPIDKeys using server side SJCL, quite slow for sandbox
  253. let random = new SecureRandom();
  254. //seeding SJCL pseudo-random number generator
  255. let estimatedEntropy = random.nextBytes( 1024 / 8 ); // 128 bytes
  256. sjcl.random.addEntropy( estimatedEntropy.toString(), 1024, "dw/crypto/SecureRandom" );
  257. }
  258. /**
  259. * This method converts JSON to base64 encoded URL save string.
  260. *
  261. * @param {string} json json string
  262. *
  263. * @return {string} base64 encoded URL save JSON string
  264. */
  265. function _JSONToBase64URLSaveString( json )
  266. {
  267. let JSONstring = JSON.stringify( json, 0 );
  268. let jsonToBytesArray = new Bytes( JSONstring );
  269. return urlBase64.encode( jsonToBytesArray );
  270. }