├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── gulp-tasks ├── lint.js └── test.js ├── gulpfile.js ├── jsdoc.json ├── package.json ├── project └── copy-bundle-files.sh ├── src ├── encrypt.js ├── index.js └── push.js └── test ├── .eslintrc └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "google", 3 | "rules": { 4 | # We current have a TODO comment that causes this error 5 | "no-warning-comments": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | tagged-release 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - 'stable' 6 | env: 7 | global: 8 | - GH_REF: github.com/googlechrome/web-push-encryption.git 9 | - secure: "PccQoVoj6hQS3jndVrWQRJ2OMOUSAFDDNMwfBDwqBiRfE0zc5hjFWadfpBNwzaiEGXZcmn6vMjlg1MmtCqyO3ytyk2CK/fMAu18xhbZGrQYdY9wEKzRwxdrACsdq7pjDzJrDlcT0iY/Fxc+/SjnJ3t8ppWot14unUbWb33EJXzZ/nxKcz9G8hn0RdHeg30J/8ExC5uwYIK+Kq1wGfzcGJ/Q8RhopEVYlUYqVY0LMnqAuQCdLJXYd+6XtNZJFJRTzlPI1ehVLJ3NdfY7CK0EE3TPTuwEIIICYhp7J2wxdBFjITlTE//LEgGdBPuGvGlRxTnloZOQH1YdQf+WCDtbg088tUl5t4l2qkOdNS7U+lI45srAPK2Sg0Yl8fW9kGFpvRYiSifnSVGpzx7KOhT5g9UPK3Deu/738dJWQhjVaXaQYW7ZHvab8krRH3AsMd8wJhXw6WHsgEoxboowQMNn4CmlrjCvL9Z+SdmxVsXbUhHocFtmqGONZRn0Pbu1H2Oj0OPd7XdyGewH04l+AdAHbDVOEI0ilGTgqIpQUVfH7ie0vVaIDrWdNrInsWICphTutD3f2qKytf2qVkxQb4wLFy+0brAmXA3STazDKddbjczIkMV0cegFFNleKRbWY0+w+0MER4kLf4V96o1B2PiFw3Uk0Rs6dbloFFoXGpZuJlR4=" 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm test 15 | - if [[ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_OS_NAME" = "linux" && "$TRAVIS_PULL_REQUEST" = "false" ]]; then 16 | ./node_modules/sw-testing-helpers/project/publish-docs.sh master; 17 | fi 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement 9 | (CLA). 10 | 11 | * If you are an individual writing original source code and you're sure you 12 | own the intellectual property, then you'll need to sign an [individual CLA] 13 | (https://developers.google.com/open-source/cla/individual). 14 | * If you work for a company that wants to allow you to contribute your work, 15 | then you'll need to sign a [corporate CLA] 16 | (https://developers.google.com/open-source/cla/corporate). 17 | 18 | Follow either of the two links above to access the appropriate CLA and 19 | instructions for how to sign and return it. Once we receive it, we'll be able to 20 | accept your pull requests. 21 | 22 | ## Contributing A Patch 23 | 24 | 1. Submit an issue describing your proposed change to the repo in question. 25 | 1. The repo owner will respond to your issue promptly. 26 | 1. If your proposed change is accepted, and you haven't already done so, sign a 27 | Contributor License Agreement (see details above). 28 | 1. Fork the desired repo, develop and test your code changes. 29 | 1. Ensure that your code adheres to the existing style in the sample to which 30 | you are contributing. Refer to the 31 | [Google Cloud Platform Samples Style Guide] 32 | (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the 33 | recommended coding standards for this organization. 34 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 35 | 1. Submit a pull request. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION NOTICE 2 | 3 | This library is now deprecated in favor of: 4 | [web-push](https://github.com/web-push-libs/web-push) 5 | 6 | ### Migration from `web-push-encryption` to `web-push` 7 | 8 | To move from this library to `web-push` perform the following steps: 9 | 10 | Install the new module and delete `web-push-encryption` from your dependencies. 11 | 12 | npm install --save web-push 13 | 14 | Swap the required module from `web-push-encryption` to `web-push` in your code. 15 | 16 | var webpush = require('web-push'); 17 | 18 | Replace the `sendWebPush(, )` call with 19 | the following: 20 | 21 | const params = { 22 | payload: 23 | }; 24 | if (subscription.keys) { 25 | params.userPublicKey = subscription.keys.p256dh; 26 | params.userAuth = subscription.keys.auth; 27 | } 28 | webpush.sendNotification(subscription.endpoint, params); 29 | 30 | `setGCMAPIKey` is the same for both libraries, just make sure it's called 31 | before `sendNotificaiton`. 32 | 33 | webpush.setGCMAPIKey(MY_GCM_KEY); 34 | 35 | License 36 | ------- 37 | 38 | Copyright 2016 Google, Inc. 39 | 40 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 41 | license agreements. See the NOTICE file distributed with this work for 42 | additional information regarding copyright ownership. The ASF licenses this 43 | file to you under the Apache License, Version 2.0 (the "License"); you may not 44 | use this file except in compliance with the License. You may obtain a copy of 45 | the License at 46 | 47 | http://www.apache.org/licenses/LICENSE-2.0 48 | 49 | Unless required by applicable law or agreed to in writing, software 50 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 51 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 52 | License for the specific language governing permissions and limitations under 53 | the License. 54 | -------------------------------------------------------------------------------- /gulp-tasks/lint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const gulp = require('gulp'); 20 | const eslint = require('gulp-eslint'); 21 | 22 | gulp.task('lint', function() { 23 | return gulp.src(GLOBAL.config.src + '/**/*.js') 24 | .pipe(eslint()) 25 | .pipe(eslint.format()) 26 | .pipe(eslint.failOnError()); 27 | }); 28 | -------------------------------------------------------------------------------- /gulp-tasks/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 Google Inc. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | /* eslint-env node */ 17 | 18 | 'use strict'; 19 | 20 | const gulp = require('gulp'); 21 | const mocha = require('gulp-mocha'); 22 | 23 | gulp.task('test:manual', function() { 24 | return gulp.src('./test/*.js', {read: false}) 25 | .pipe(mocha()) 26 | .on('error', () => { 27 | process.exit(1); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | GLOBAL.config = { 20 | src: './src' 21 | }; 22 | 23 | const gulp = require('gulp'); 24 | 25 | // Import tasks from the gulp-tasks directory 26 | require('require-dir')('./gulp-tasks'); 27 | 28 | gulp.task('default', ['lint']); 29 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": "./src", 4 | "includePattern": ".+\\.js(doc)?$", 5 | "excludePattern": "(^|\\/|\\\\)_" 6 | }, 7 | "opts": { 8 | "destination": "./docs/", 9 | "readme": "./README.md" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-push-encryption", 3 | "version": "2.0.0", 4 | "description": "A library that handles payload encryption for the Web Push protocol", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/GoogleChrome/web-push-encryption.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/GoogleChrome/web-push-encryption/issues" 11 | }, 12 | "homepage": "https://github.com/GoogleChrome/web-push-encryption#readme", 13 | "main": "src/index.js", 14 | "scripts": { 15 | "publish-release": "./node_modules/sw-testing-helpers/project/publish-release.sh", 16 | "build": "echo \"Skipping build step\"", 17 | "build-docs": "jsdoc -c jsdoc.json", 18 | "test": "gulp lint test:manual", 19 | "bundle": "./project/copy-bundle-files.sh" 20 | }, 21 | "dependencies": { 22 | "request": "^2.67.0" 23 | }, 24 | "devDependencies": { 25 | "chai": "^3.5.0", 26 | "eslint-config-google": "^0.3.0", 27 | "gulp": "^3.9.0", 28 | "gulp-eslint": "^1.1.1", 29 | "gulp-mocha": "^2.2.0", 30 | "jsdoc": "^3.4.0", 31 | "proxyquire": "^1.7.4", 32 | "require-dir": "^0.3.0", 33 | "sinon": "^1.17.3", 34 | "sw-testing-helpers": "0.0.14" 35 | }, 36 | "keywords": [ 37 | "web", 38 | "push", 39 | "messaging", 40 | "encryption" 41 | ], 42 | "author": "Mat Scales", 43 | "license": "Apache-2.0" 44 | } 45 | -------------------------------------------------------------------------------- /project/copy-bundle-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ######################################################################### 5 | # 6 | # GUIDE TO USE OF THIS SCRIPT 7 | # 8 | ######################################################################### 9 | # 10 | # - This script is used by npm run bundle in this probject and serves 11 | # solely as an example 12 | # 13 | ######################################################################### 14 | 15 | if [ "$BASH_VERSION" = '' ]; then 16 | echo " Please run this script via this command: './project/copy-bundle-files.sh'" 17 | exit 1; 18 | fi 19 | 20 | if [ -z "$1" ]; then 21 | echo " Bad input: Expected a directory as the first argument for the path to put the final bundle files into (i.e. ./tagged-release)"; 22 | exit 1; 23 | fi 24 | 25 | # This isn't needed unless run outside of publish-release script 26 | mkdir -p $1 27 | 28 | cp -r ./docs $1 29 | cp -r ./src $1 30 | cp LICENSE $1 31 | cp package.json $1 32 | cp README.md $1 33 | -------------------------------------------------------------------------------- /src/encrypt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const crypto = require('crypto'); 20 | 21 | const ONE_BUFFER = new Buffer(1).fill(1); 22 | const AUTH_INFO = new Buffer('Content-Encoding: auth\0', 'utf8'); 23 | const MAX_PAYLOAD_LENGTH = 4078; 24 | 25 | /** 26 | * Encrypts a message such that it can be sent using the Web Push protocol. 27 | * 28 | * You can find out more about the various pieces: 29 | * 30 | *
    31 | *
  • {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding}
  • 32 | *
  • {@link https://en.wikipedia.org/wiki/Elliptic_curve_Diffie%E2%80%93Hellman}
  • 33 | *
  • {@link https://tools.ietf.org/html/draft-ietf-webpush-encryption}
  • 34 | *
35 | * 36 | * @memberof web-push-encryption 37 | * @param {String|Buffer} message The message to be sent 38 | * @param {Object} subscription A JavaScript Object representing a 39 | * [PushSubscription]{@link https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription}. 40 | * The easiest way to get this from the browser to your backend is by running 41 | * JSON.stringify(subscription) in a web pages JavaScript. 42 | * @param {number} paddingLength Number of bytes of padding to use 43 | * @return {Object} An Object containing the encrypted payload and 44 | * the other encryption information needed to 45 | * send the message. 46 | */ 47 | function encrypt(message, subscription, paddingLength) { 48 | paddingLength = paddingLength || 0; 49 | 50 | // Create Buffers for all of the inputs 51 | const paddingBuffer = makePadding(paddingLength); 52 | let messageBuffer; 53 | 54 | if (typeof message === 'string') { 55 | messageBuffer = new Buffer(message, 'utf8'); 56 | } else if (message instanceof Buffer) { 57 | messageBuffer = message; 58 | } else { 59 | throw new Error('Message must be a String or a Buffer'); 60 | } 61 | 62 | // The maximum size of the message + padding is 4078 bytes 63 | if ((messageBuffer.length + paddingLength) > MAX_PAYLOAD_LENGTH) { 64 | throw new Error(`Payload is too large. The max number of ` + 65 | `bytes is ${MAX_PAYLOAD_LENGTH}, input is ${messageBuffer.length} ` + 66 | `bytes plus ${paddingLength} bytes of padding.`); 67 | } 68 | 69 | const plaintext = Buffer.concat([paddingBuffer, messageBuffer]); 70 | 71 | if (!subscription || !subscription.keys) { 72 | throw new Error('Subscription has no encryption details.'); 73 | } 74 | 75 | if (!subscription.keys.p256dh || 76 | !subscription.keys.auth) { 77 | throw new Error('Subscription is missing some encryption details.'); 78 | } 79 | 80 | const clientPublicKey = new Buffer(subscription.keys.p256dh, 'base64'); 81 | const clientAuthToken = new Buffer(subscription.keys.auth, 'base64'); 82 | 83 | if (clientAuthToken.length !== 16) { 84 | throw new Error('Subscription\'s Auth token is not 16 bytes.'); 85 | } 86 | 87 | if (clientPublicKey.length !== 65) { 88 | throw new Error('Subscription\'s client key (p256dh) is invalid.'); 89 | } 90 | 91 | // Create a random 16-byte salt 92 | const salt = crypto.randomBytes(16); 93 | 94 | // Use ECDH to derive a shared secret between us and the client. We generate 95 | // a fresh private/public key pair at random every time we encrypt. 96 | const serverECDH = crypto.createECDH('prime256v1'); 97 | const serverPublicKey = serverECDH.generateKeys(); 98 | const sharedSecret = serverECDH.computeSecret(clientPublicKey); 99 | 100 | // Derive a Pseudo-Random Key (prk) that can be used to further derive our 101 | // other encryption parameters. These derivations are described in 102 | // https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00 103 | const prk = hkdf(clientAuthToken, sharedSecret, AUTH_INFO, 32); 104 | 105 | const context = createContext(clientPublicKey, serverPublicKey); 106 | 107 | // Derive the Content Encryption Key 108 | const contentEncryptionKeyInfo = createInfo('aesgcm', context); 109 | const contentEncryptionKey = hkdf(salt, prk, contentEncryptionKeyInfo, 16); 110 | 111 | // Derive the Nonce 112 | const nonceInfo = createInfo('nonce', context); 113 | const nonce = hkdf(salt, prk, nonceInfo, 12); 114 | 115 | // Do the actual encryption 116 | const ciphertext = encryptPayload(plaintext, contentEncryptionKey, nonce); 117 | 118 | // Return all of the values needed to construct a Web Push HTTP request. 119 | return { 120 | ciphertext: ciphertext, 121 | salt: salt, 122 | serverPublicKey: serverPublicKey 123 | }; 124 | } 125 | 126 | /** 127 | * Creates a context for deriving encyption parameters. 128 | * See section 4.2 of 129 | * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} 130 | * 131 | * @private 132 | * @param {Buffer} clientPublicKey The client's public key 133 | * @param {Buffer} serverPublicKey Our public key 134 | * @return {Buffer} context 135 | */ 136 | function createContext(clientPublicKey, serverPublicKey) { 137 | // The context format is: 138 | // 0x00 || length(clientPublicKey) || clientPublicKey || 139 | // length(serverPublicKey) || serverPublicKey 140 | // The lengths are 16-bit, Big Endian, unsigned integers so take 2 bytes each. 141 | 142 | // The keys should always be 65 bytes each. The format of the keys is 143 | // described in section 4.3.6 of the (sadly not freely linkable) ANSI X9.62 144 | // specification. 145 | if (clientPublicKey.length !== 65) { 146 | throw new Error('Invalid client public key length'); 147 | } 148 | 149 | // This one should never happen, because it's our code that generates the key 150 | if (serverPublicKey.length !== 65) { 151 | throw new Error('Invalid server public key length'); 152 | } 153 | 154 | const context = new Buffer(1 + 2 + 65 + 2 + 65); 155 | context.write('\0', 0); 156 | context.writeUInt16BE(clientPublicKey.length, 1); 157 | clientPublicKey.copy(context, 3); 158 | context.writeUInt16BE(serverPublicKey.length, 68); 159 | serverPublicKey.copy(context, 70); 160 | return context; 161 | } 162 | 163 | /** 164 | * Returns an info record. See sections 3.2 and 3.3 of 165 | * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} 166 | * 167 | * @private 168 | * @param {String} type The type of the info record 169 | * @param {Buffer} context The context for the record 170 | * @return {Buffer} info 171 | */ 172 | function createInfo(type, context) { 173 | if (context.length !== 135) { 174 | throw new Error('Context argument has invalid size'); 175 | } 176 | 177 | const l = type.length; 178 | const info = new Buffer(18 + l + 1 + 5 + 135); 179 | 180 | // The start index for each element within the buffer is: 181 | // value | length | start | 182 | // --------------------------------------- 183 | // 'Content-Encoding: '| 18 | 0 | 184 | // type | l | 18 | 185 | // nul byte | 1 | 18 + l | 186 | // 'P-256' | 5 | 19 + l | 187 | // info | 135 | 24 + l | 188 | info.write('Content-Encoding: '); 189 | info.write(type, 18); 190 | info.write('\0', 18 + l); 191 | info.write('P-256', 19 + l); 192 | context.copy(info, 24 + l); 193 | 194 | return info; 195 | } 196 | 197 | /** 198 | * HMAC-based Extract-and-Expand Key Derivation Function (HKDF) 199 | * 200 | * This is used to derive a secure encryption key from a mostly-secure shared 201 | * secret. 202 | * 203 | * This is a partial implementation of HKDF tailored to our specific purposes. 204 | * In particular, for us the value of N will always be 1, and thus T always 205 | * equals HMAC-Hash(PRK, info | 0x01). 206 | * 207 | * See {@link https://www.rfc-editor.org/rfc/rfc5869.txt} 208 | * 209 | * @private 210 | * @param {Buffer} salt A non-secret random value 211 | * @param {Buffer} ikm Input keying material 212 | * @param {Buffer} info Application-specfic context 213 | * @param {Number} length The length (in bytes) of the required output key 214 | * @return {Buffer} Key 215 | */ 216 | function hkdf(salt, ikm, info, length) { 217 | // Extract 218 | const prkHmac = crypto.createHmac('sha256', salt); 219 | prkHmac.update(ikm); 220 | const prk = prkHmac.digest(); 221 | 222 | // Expand 223 | const infoHmac = crypto.createHmac('sha256', prk); 224 | infoHmac.update(info); 225 | infoHmac.update(ONE_BUFFER); 226 | return infoHmac.digest().slice(0, length); 227 | } 228 | 229 | /** 230 | * Creates a buffer of padding bytes. The first two bytes hold the length of the 231 | * rest of the buffer, encoded as a 16-bit integer. 232 | * @private 233 | * @param {number} length How long the padding should be 234 | * @return {Buffer} The new buffer 235 | */ 236 | function makePadding(length) { 237 | const buffer = new Buffer(2 + length); 238 | buffer.fill(0); 239 | buffer.writeUInt16BE(length, 0); 240 | return buffer; 241 | } 242 | 243 | /** 244 | * Encrypt the plaintext message using AES128/GCM 245 | * @private 246 | * @param {Buffer} plaintext The message to be encrypted, including 247 | * padding. 248 | * @param {Buffer} contentEncryptionKey The private key to use 249 | * @param {Buffer} nonce The iv 250 | * @return {Buffer} The encrypted payload 251 | */ 252 | function encryptPayload(plaintext, contentEncryptionKey, nonce) { 253 | const cipher = crypto.createCipheriv('id-aes128-GCM', contentEncryptionKey, 254 | nonce); 255 | const result = cipher.update(plaintext); 256 | cipher.final(); 257 | 258 | return Buffer.concat([result, cipher.getAuthTag()]); 259 | } 260 | 261 | module.exports = encrypt; 262 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const encrypt = require('./encrypt'); 20 | const push = require('./push'); 21 | 22 | /** 23 | * web-push-encryption is a module that helps encrypt and / or send push 24 | * messages over the web push protocol. 25 | * @namespace web-push-encryption 26 | */ 27 | module.exports = { 28 | encrypt: encrypt, 29 | sendWebPush: push.sendWebPush, 30 | setGCMAPIKey: push.setGCMAPIKey 31 | }; 32 | -------------------------------------------------------------------------------- /src/push.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const request = require('request'); 20 | const encrypt = require('./encrypt'); 21 | 22 | const GCM_URL = 'https://android.googleapis.com/gcm/send'; 23 | const TEMP_GCM_URL = 'https://gcm-http.googleapis.com/gcm'; 24 | 25 | let gcmAuthToken; 26 | 27 | /** 28 | * Set the key to use in the Authentication header for GCM requests 29 | * @param {String} key The API key to use 30 | * @throws {Error} If the key is invalid 31 | */ 32 | function setGCMAPIKey(key) { 33 | if (!key.startsWith('AIza') || key.length !== 40) { 34 | throw new Error('expected Server API Key in the form AIza..., 40 characters long'); 35 | } 36 | gcmAuthToken = key; 37 | } 38 | 39 | /** 40 | * URL safe Base64 encoder 41 | * 42 | * @private 43 | * @param {Buffer} buffer The data to encode 44 | * @return {String} URL safe base 64 encoded string 45 | */ 46 | function ub64(buffer) { 47 | return buffer.toString('base64').replace(/\+/g, '-') 48 | .replace(/\//g, '_') 49 | .replace(/\=/g, ''); 50 | } 51 | 52 | /** 53 | * Sends a message using the Web Push protocol 54 | * 55 | * @memberof web-push-encryption 56 | * @param {String|Buffer} message The message to send 57 | * @param {Object} subscription The subscription details for the client we 58 | * are sending to 59 | * @param {Number} paddingLength The number of bytes of padding to add to the 60 | * message before encrypting it. 61 | * @return {Promise} A promise that resolves if the push was sent successfully 62 | * with status and body. 63 | */ 64 | function sendWebPush(message, subscription, paddingLength) { 65 | if (!subscription || !subscription.endpoint) { 66 | throw new Error('sendWebPush() expects a subscription endpoint with ' + 67 | 'an endpoint parameter.'); 68 | } 69 | 70 | // If the endpoint is GCM then we temporarily need to rewrite it, as not all 71 | // GCM servers support the Web Push protocol. This should go away in the 72 | // future. 73 | const endpoint = subscription.endpoint.replace(GCM_URL, TEMP_GCM_URL); 74 | const headers = { 75 | // TODO: Make TTL variable 76 | TTL: '0' 77 | }; 78 | let body; 79 | 80 | if (message) { 81 | if (typeof message != 'string' && !(message instanceof Buffer)) { 82 | throw new Error('Message must be a String or a Buffer'); 83 | } 84 | if (message.length > 0) { 85 | const payload = encrypt(message, subscription, paddingLength); 86 | headers['Content-Encoding'] = 'aesgcm'; 87 | headers.Encryption = `salt=${ub64(payload.salt)}`; 88 | headers['Crypto-Key'] = `dh=${ub64(payload.serverPublicKey)}`; 89 | body = payload.ciphertext; 90 | } 91 | } 92 | 93 | if (endpoint.indexOf(TEMP_GCM_URL) !== -1) { 94 | if (gcmAuthToken) { 95 | headers.Authorization = `key=${gcmAuthToken}`; 96 | } else { 97 | throw new Error('GCM requires an Auth Token parameter'); 98 | } 99 | } 100 | 101 | return new Promise(function(resolve, reject) { 102 | request.post(endpoint, { 103 | body: body, 104 | headers: headers 105 | }, function(error, response, body) { 106 | if (error) { 107 | reject(error); 108 | } else { 109 | if (response.statusCode >= 400 && response.statusCode < 500) { 110 | // Subscription is invalid: 111 | // https://tools.ietf.org/html/draft-ietf-webpush-protocol-04#section-8.3 112 | return reject({ 113 | code: 'expired-subscription', 114 | statusCode: response.statusCode, 115 | statusMessage: response.statusMessage, 116 | body: body 117 | }); 118 | } 119 | 120 | resolve({ 121 | statusCode: response.statusCode, 122 | statusMessage: response.statusMessage, 123 | body: body 124 | }); 125 | } 126 | }); 127 | }); 128 | } 129 | 130 | module.exports = {sendWebPush, setGCMAPIKey}; 131 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "max-len": 0, 8 | "quote-props": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | require('chai').should(); 20 | const expect = require('chai').expect; 21 | const proxyquire = require('proxyquire'); 22 | const sinon = require('sinon'); 23 | 24 | const EXAMPLE_SERVER_KEYS = { 25 | public: 'BOg5KfYiBdDDRF12Ri17y3v+POPr8X0nVP2jDjowPVI/DMKU1aQ3OLdPH1iaakvR9/PHq6tNCzJH35v/JUz2crY=', 26 | private: 'uDNsfsz91y2ywQeOHljVoiUg3j5RGrDVAswRqjP3v90=' 27 | }; 28 | const EXAMPLE_SALT = 'AAAAAAAAAAAAAAAAAAAAAA=='; 29 | 30 | const EXAMPLE_INPUT = 'Hello, World.'; 31 | const EXAMPLE_OUTPUT = 'CE2OS6BxfXsC2YbTdfkeWLlt4AKWbHZ3Fe53n5/4Yg=='; 32 | 33 | const VALID_SUBSCRIPTION = { 34 | endpoint: 'https://example-endpoint.com/example/1234', 35 | keys: { 36 | auth: '8eDyX_uCN0XRhSbY5hs7Hg==', 37 | p256dh: 'BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=' 38 | } 39 | }; 40 | 41 | const INVALID_SUBSCRIPTION = { 42 | endpoint: 'https://example-endpoint.com/example/6666', 43 | keys: { 44 | auth: '8eDyX_uCN0XRhSbY5hs7Hg==', 45 | p256dh: 'BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=' 46 | } 47 | }; 48 | 49 | const INVALID_AUTH_SUBSCRIPTION = { 50 | endpoint: 'https://example-endpoint.com/example/1234', 51 | keys: { 52 | auth: 'uCN0XRhSbY5hs7Hg==', 53 | p256dh: 'BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=' 54 | } 55 | }; 56 | 57 | const INVALID_P256DH_SUBSCRIPTION = { 58 | endpoint: 'https://example-endpoint.com/example/1234', 59 | keys: { 60 | auth: '8eDyX_uCN0XRhSbY5hs7Hg==', 61 | p256dh: '6ZFK3ol2ohgn_-0yP7QQA=' 62 | } 63 | }; 64 | 65 | const SUBSCRIPTION_NO_KEYS = { 66 | endpoint: 'https://example-endpoint.com/example/1234' 67 | }; 68 | 69 | const GCM_SUBSCRIPTION_EXAMPLE = { 70 | original: 'https://android.googleapis.com/gcm/send/AAAAAAAAAAA:AAA91AAAA2_A7AAAAAAAAAAAAAAAAAAAAAAAAAAA9AAAAA9AAA_AAAA8AAAAAA5-AAAAAA2AAAA_AAAAA4A51A_A3AAA1AAAAAAAAAAAAAAA3AAAAAAAAA6AA2AAAAAAAA80AAAAAA', 71 | webpush: 'https://gcm-http.googleapis.com/gcm/AAAAAAAAAAA:AAA91AAAA2_A7AAAAAAAAAAAAAAAAAAAAAAAAAAA9AAAAA9AAA_AAAA8AAAAAA5-AAAAAA2AAAA_AAAAA4A51A_A3AAA1AAAAAAAAAAAAAAA3AAAAAAAAA6AA2AAAAAAAA80AAAAAA' 72 | }; 73 | 74 | const SALT_LENGTH = 16; 75 | const SERVER_PUBLIC_KEY_LENGTH = 65; 76 | 77 | let testStubs = []; 78 | 79 | describe('Test the Libraries Top Level API', function() { 80 | const restoreStubs = () => { 81 | testStubs.forEach(stub => { 82 | stub.restore(); 83 | }); 84 | testStubs = []; 85 | }; 86 | 87 | beforeEach(() => restoreStubs()); 88 | 89 | after(() => restoreStubs()); 90 | 91 | describe('Test encrypt() method', function() { 92 | it('should encrypt the message with a valid subscription', function() { 93 | const library = require('../src/index.js'); 94 | const response = library.encrypt('Hello, World', VALID_SUBSCRIPTION); 95 | Buffer.isBuffer(response.ciphertext).should.equal(true); 96 | Buffer.isBuffer(response.salt).should.equal(true); 97 | response.salt.should.have.length(SALT_LENGTH); 98 | Buffer.isBuffer(response.serverPublicKey).should.equal(true); 99 | response.serverPublicKey.should.have.length(SERVER_PUBLIC_KEY_LENGTH); 100 | }); 101 | 102 | it('should throw an error due to subscription with no keys being passed in', function() { 103 | const library = require('../src/index.js'); 104 | expect( 105 | () => library.encrypt('Hello, World', SUBSCRIPTION_NO_KEYS) 106 | ).to.throw('Subscription has no encryption details'); 107 | }); 108 | 109 | it('should not throw an error when no endpoint is passed in', function() { 110 | const library = require('../src/index.js'); 111 | let subscription = { 112 | keys: VALID_SUBSCRIPTION.keys 113 | }; 114 | const response = library.encrypt('Hello, World', subscription); 115 | Buffer.isBuffer(response.ciphertext).should.equal(true); 116 | Buffer.isBuffer(response.salt).should.equal(true); 117 | response.salt.should.have.length(SALT_LENGTH); 118 | Buffer.isBuffer(response.serverPublicKey).should.equal(true); 119 | response.serverPublicKey.should.have.length(SERVER_PUBLIC_KEY_LENGTH); 120 | }); 121 | 122 | it('should throw an error due to an invalid auth token', function() { 123 | const library = require('../src/index.js'); 124 | expect( 125 | () => library.encrypt('Hello, World', INVALID_AUTH_SUBSCRIPTION) 126 | ).to.throw('Subscription\'s Auth token is not 16 bytes.'); 127 | }); 128 | 129 | it('should not throw an error when no auth token is passed in', function() { 130 | const library = require('../src/index.js'); 131 | let subscription = { 132 | endpoint: VALID_SUBSCRIPTION.endpoint, 133 | keys: { 134 | p256dh: VALID_SUBSCRIPTION.keys.p256dh 135 | } 136 | }; 137 | 138 | expect( 139 | () => library.encrypt('Hello, World', subscription) 140 | ).to.throw('Subscription is missing some encryption details'); 141 | }); 142 | 143 | it('should throw an error due to an invalid client public key', function() { 144 | const library = require('../src/index.js'); 145 | expect( 146 | () => library.encrypt('Hello, World', INVALID_P256DH_SUBSCRIPTION) 147 | ).to.throw('Subscription\'s client key (p256dh) is invalid.'); 148 | }); 149 | 150 | it('should throw an error when no p256dh key is passed in', function() { 151 | const library = require('../src/index.js'); 152 | let subscription = { 153 | endpoint: VALID_SUBSCRIPTION.endpoint, 154 | keys: { 155 | auth: VALID_SUBSCRIPTION.keys.auth 156 | } 157 | }; 158 | 159 | expect( 160 | () => library.encrypt('Hello, World', subscription) 161 | ).to.throw('Subscription is missing some encryption details'); 162 | }); 163 | 164 | it('should return the correct encryption values', function() { 165 | let crypto = require('crypto'); 166 | 167 | // This is for the salt 168 | let saltStub = sinon.stub(crypto, 'randomBytes'); 169 | saltStub.withArgs(16).returns(new Buffer(EXAMPLE_SALT, 'base64')); 170 | testStubs.push(saltStub); 171 | 172 | // Server key generation 173 | const exampleECDH = crypto.createECDH('prime256v1'); 174 | exampleECDH.generateKeys(); 175 | exampleECDH.setPrivateKey(EXAMPLE_SERVER_KEYS.private, 'base64'); 176 | exampleECDH.setPublicKey(EXAMPLE_SERVER_KEYS.public, 'base64'); 177 | // Make this a NOOP 178 | exampleECDH.generateKeys = () => { 179 | return exampleECDH.getPublicKey(); 180 | }; 181 | let ecdhStub = sinon.stub(crypto, 'createECDH'); 182 | ecdhStub.withArgs('prime256v1').returns(exampleECDH); 183 | testStubs.push(ecdhStub); 184 | 185 | const library = proxyquire('../src/index.js', { 186 | 'crypto': crypto 187 | }); 188 | 189 | const response = library.encrypt(EXAMPLE_INPUT, VALID_SUBSCRIPTION); 190 | Buffer.isBuffer(response.salt).should.equal(true); 191 | response.salt.should.have.length(SALT_LENGTH); 192 | response.salt.toString('base64').should.equal(EXAMPLE_SALT); 193 | 194 | Buffer.isBuffer(response.serverPublicKey).should.equal(true); 195 | response.serverPublicKey.should.have.length(SERVER_PUBLIC_KEY_LENGTH); 196 | response.serverPublicKey.toString('base64').should.equal(EXAMPLE_SERVER_KEYS.public); 197 | 198 | response.ciphertext.toString('base64').should.equal(EXAMPLE_OUTPUT); 199 | }); 200 | 201 | it('should throw an error when the input is too large', function() { 202 | const library = require('../src/index.js'); 203 | 204 | let largeInput = new Buffer(4081); 205 | largeInput.fill(0); 206 | 207 | expect( 208 | () => library.encrypt(largeInput.toString('utf8'), VALID_SUBSCRIPTION) 209 | ).to.throw('Payload is too large. The max number of bytes is 4078, input is 4081 bytes plus 0 bytes of padding.'); 210 | 211 | largeInput = new Buffer(5000); 212 | largeInput.fill(0); 213 | 214 | expect( 215 | () => library.encrypt(largeInput.toString('utf8'), VALID_SUBSCRIPTION) 216 | ).to.throw('Payload is too large. The max number of bytes is 4078, input is 5000 bytes plus 0 bytes of padding.'); 217 | 218 | expect(() => library.encrypt(EXAMPLE_INPUT, VALID_SUBSCRIPTION, 4080)) 219 | .to.throw('Payload is too large. The max number of bytes is 4078, input is 13 bytes plus 4080 bytes of padding.'); 220 | }); 221 | }); 222 | 223 | describe('Test sendWebPush() method', function() { 224 | it('should throw an error when no input provided', function() { 225 | const library = require('../src/index.js'); 226 | 227 | expect( 228 | () => library.sendWebPush() 229 | ).to.throw('sendWebPush() expects a subscription endpoint with ' + 230 | 'an endpoint parameter.'); 231 | }); 232 | 233 | it('should throw an error when the subscription object has no endpoint', function() { 234 | const library = require('../src/index.js'); 235 | 236 | expect( 237 | () => library.sendWebPush({}) 238 | ).to.throw('sendWebPush() expects a subscription endpoint with ' + 239 | 'an endpoint parameter.'); 240 | }); 241 | 242 | it('should throw an error when a message is passed in with no subscription', function() { 243 | const library = require('../src/index.js'); 244 | 245 | expect( 246 | () => library.sendWebPush('Message') 247 | ).to.throw('sendWebPush() expects a subscription endpoint with ' + 248 | 'an endpoint parameter.'); 249 | }); 250 | 251 | it('should throw an error when a subscription is passed in with array as payload data', function() { 252 | const library = require('../src/index.js'); 253 | 254 | expect( 255 | () => library.sendWebPush([ 256 | { 257 | hello: 'world' 258 | }, 259 | 'This is a test', 260 | Promise.resolve('Promise Resolve'), 261 | Promise.reject('Promise Reject') 262 | ], VALID_SUBSCRIPTION) 263 | ).to.throw('Message must be a String or a Buffer'); 264 | }); 265 | 266 | it('should throw an error when a subscription is passed in with a random object as payload data', function() { 267 | const library = require('../src/index.js'); 268 | 269 | expect( 270 | () => library.sendWebPush({random: 1}, VALID_SUBSCRIPTION) 271 | ).to.throw('Message must be a String or a Buffer'); 272 | }); 273 | 274 | it('should throw an error when a subscription with no encryption details is passed in with string as payload data', function() { 275 | const library = require('../src/index.js'); 276 | 277 | expect( 278 | () => library.sendWebPush('Hello, World!', { 279 | endpoint: 'http://fakendpoint' 280 | }) 281 | ).to.throw('Subscription has no encryption details.'); 282 | }); 283 | 284 | it('should attempt a web push protocol request', function() { 285 | const requestReplacement = { 286 | post: (endpoint, data, cb) => { 287 | endpoint.should.equal(VALID_SUBSCRIPTION.endpoint); 288 | 289 | Buffer.isBuffer(data.body).should.equal(true); 290 | data.headers.Encryption.should.have.length(27); 291 | data.headers['Crypto-Key'].should.have.length(90); 292 | 293 | cb( 294 | null, 295 | { 296 | statusCode: 200, 297 | statusMessage: 'Status message' 298 | }, 299 | 'Response body' 300 | ); 301 | } 302 | }; 303 | const pushProxy = proxyquire('../src/push.js', { 304 | 'request': requestReplacement 305 | }); 306 | const library = proxyquire('../src/index.js', { 307 | './push': pushProxy 308 | }); 309 | return library.sendWebPush(new Buffer('Hello, World!', 'utf8'), VALID_SUBSCRIPTION) 310 | .then(response => { 311 | response.statusCode.should.equal(200); 312 | response.statusMessage.should.equal('Status message'); 313 | response.body.should.equal('Response body'); 314 | }); 315 | }); 316 | 317 | it('should attempt a web push protocol request for GCM', function() { 318 | const API_KEY = 'AIza not a real key but must be 40 chars'; 319 | const gcmSubscription = { 320 | endpoint: GCM_SUBSCRIPTION_EXAMPLE.original, 321 | keys: VALID_SUBSCRIPTION.keys 322 | }; 323 | const requestReplacement = { 324 | post: (endpoint, data, cb) => { 325 | endpoint.should.equal(GCM_SUBSCRIPTION_EXAMPLE.webpush); 326 | 327 | Buffer.isBuffer(data.body).should.equal(true); 328 | data.headers.Encryption.should.have.length(27); 329 | data.headers['Crypto-Key'].should.have.length(90); 330 | data.headers.Authorization.should.equal('key=' + API_KEY); 331 | 332 | cb( 333 | null, 334 | { 335 | statusCode: 200, 336 | statusMessage: 'Status message' 337 | }, 338 | 'Response body' 339 | ); 340 | } 341 | }; 342 | 343 | const pushProxy = proxyquire('../src/push.js', { 344 | 'request': requestReplacement 345 | }); 346 | const library = proxyquire('../src/index.js', { 347 | './push': pushProxy 348 | }); 349 | library.setGCMAPIKey(API_KEY); 350 | return library.sendWebPush('Hello, World!', gcmSubscription) 351 | .then(response => { 352 | response.statusCode.should.equal(200); 353 | response.statusMessage.should.equal('Status message'); 354 | response.body.should.equal('Response body'); 355 | }); 356 | }); 357 | 358 | it('should throw an error when not providing an AuthToken for a GCM subscription', function() { 359 | const gcmSubscription = { 360 | endpoint: GCM_SUBSCRIPTION_EXAMPLE.original, 361 | keys: VALID_SUBSCRIPTION.keys 362 | }; 363 | 364 | const library = require('../src/index.js'); 365 | expect( 366 | () => library.sendWebPush('Hello, World!', gcmSubscription) 367 | ).to.throw('GCM requires an Auth Token parameter'); 368 | }); 369 | 370 | it('should handle errors from request', function() { 371 | const EXAMPLE_ERROR = 'Example Error'; 372 | 373 | const requestReplacement = { 374 | post: (endpoint, data, cb) => { 375 | endpoint.should.equal(VALID_SUBSCRIPTION.endpoint); 376 | 377 | Buffer.isBuffer(data.body).should.equal(true); 378 | data.headers.Encryption.should.have.length(27); 379 | data.headers['Crypto-Key'].should.have.length(90); 380 | 381 | cb(EXAMPLE_ERROR); 382 | } 383 | }; 384 | const pushProxy = proxyquire('../src/push.js', { 385 | 'request': requestReplacement 386 | }); 387 | const library = proxyquire('../src/index.js', { 388 | './push': pushProxy 389 | }); 390 | return library.sendWebPush('Hello, World!', VALID_SUBSCRIPTION) 391 | .then(() => { 392 | throw new Error('The promise was expected to reject.'); 393 | }) 394 | .catch(err => { 395 | err.should.equal(EXAMPLE_ERROR); 396 | }); 397 | }); 398 | 399 | it('should reject when the subscription is no longer valid (via 404 status code)', function(done) { 400 | const requestReplacement = { 401 | post: (endpoint, data, cb) => { 402 | endpoint.should.equal(INVALID_SUBSCRIPTION.endpoint); 403 | 404 | Buffer.isBuffer(data.body).should.equal(true); 405 | data.headers.Encryption.should.have.length(27); 406 | data.headers['Crypto-Key'].should.have.length(90); 407 | 408 | cb( 409 | null, 410 | { 411 | statusCode: 404, 412 | statusMessage: 'Not Found' 413 | }, 414 | '' 415 | ); 416 | } 417 | }; 418 | const pushProxy = proxyquire('../src/push.js', { 419 | 'request': requestReplacement 420 | }); 421 | const library = proxyquire('../src/index.js', { 422 | './push': pushProxy 423 | }); 424 | return library.sendWebPush('Hello, World!', INVALID_SUBSCRIPTION) 425 | .then(() => { 426 | done(new Error('This should have rejected')); 427 | }) 428 | .catch(err => { 429 | err.code.should.equal('expired-subscription'); 430 | done(); 431 | }); 432 | }); 433 | 434 | it('should reject when the subscription is no longer valid (via 410 status code)', function(done) { 435 | const requestReplacement = { 436 | post: (endpoint, data, cb) => { 437 | endpoint.should.equal(INVALID_SUBSCRIPTION.endpoint); 438 | 439 | Buffer.isBuffer(data.body).should.equal(true); 440 | data.headers.Encryption.should.have.length(27); 441 | data.headers['Crypto-Key'].should.have.length(90); 442 | 443 | cb( 444 | null, 445 | { 446 | statusCode: 410, 447 | statusMessage: 'Gone' 448 | }, 449 | '' 450 | ); 451 | } 452 | }; 453 | const pushProxy = proxyquire('../src/push.js', { 454 | 'request': requestReplacement 455 | }); 456 | const library = proxyquire('../src/index.js', { 457 | './push': pushProxy 458 | }); 459 | return library.sendWebPush('Hello, World!', INVALID_SUBSCRIPTION) 460 | .then(() => { 461 | done(new Error('This should have rejected')); 462 | }) 463 | .catch(err => { 464 | err.code.should.equal('expired-subscription'); 465 | done(); 466 | }); 467 | }); 468 | 469 | it('should throw on invalid API key', function() { 470 | const library = require('../src/index.js'); 471 | 472 | expect( 473 | () => { 474 | library.setGCMAPIKey('invalid'); 475 | } 476 | ).to.throw('expected Server API Key in the form AIza..., 40 characters long'); 477 | }); 478 | }); 479 | }); 480 | --------------------------------------------------------------------------------