summaryrefslogtreecommitdiffstats
path: root/dom/push/test/webpush.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/push/test/webpush.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/push/test/webpush.js')
-rw-r--r--dom/push/test/webpush.js186
1 files changed, 186 insertions, 0 deletions
diff --git a/dom/push/test/webpush.js b/dom/push/test/webpush.js
new file mode 100644
index 000000000..6aacc5ae1
--- /dev/null
+++ b/dom/push/test/webpush.js
@@ -0,0 +1,186 @@
+/*
+ * Browser-based Web Push client for the application server piece.
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * Uses the WebCrypto API.
+ * Uses the fetch API. Polyfill: https://github.com/github/fetch
+ */
+
+(function (g) {
+ 'use strict';
+
+ var P256DH = {
+ name: 'ECDH',
+ namedCurve: 'P-256'
+ };
+ var webCrypto = g.crypto.subtle;
+ var ENCRYPT_INFO = new TextEncoder('utf-8').encode("Content-Encoding: aesgcm128");
+ var NONCE_INFO = new TextEncoder('utf-8').encode("Content-Encoding: nonce");
+
+ function chunkArray(array, size) {
+ var start = array.byteOffset || 0;
+ array = array.buffer || array;
+ var index = 0;
+ var result = [];
+ while(index + size <= array.byteLength) {
+ result.push(new Uint8Array(array, start + index, size));
+ index += size;
+ }
+ if (index < array.byteLength) {
+ result.push(new Uint8Array(array, start + index));
+ }
+ return result;
+ }
+
+ /* I can't believe that this is needed here, in this day and age ...
+ * Note: these are not efficient, merely expedient.
+ */
+ var base64url = {
+ _strmap: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
+ encode: function(data) {
+ data = new Uint8Array(data);
+ var len = Math.ceil(data.length * 4 / 3);
+ return chunkArray(data, 3).map(chunk => [
+ chunk[0] >>> 2,
+ ((chunk[0] & 0x3) << 4) | (chunk[1] >>> 4),
+ ((chunk[1] & 0xf) << 2) | (chunk[2] >>> 6),
+ chunk[2] & 0x3f
+ ].map(v => base64url._strmap[v]).join('')).join('').slice(0, len);
+ },
+ _lookup: function(s, i) {
+ return base64url._strmap.indexOf(s.charAt(i));
+ },
+ decode: function(str) {
+ var v = new Uint8Array(Math.floor(str.length * 3 / 4));
+ var vi = 0;
+ for (var si = 0; si < str.length;) {
+ var w = base64url._lookup(str, si++);
+ var x = base64url._lookup(str, si++);
+ var y = base64url._lookup(str, si++);
+ var z = base64url._lookup(str, si++);
+ v[vi++] = w << 2 | x >>> 4;
+ v[vi++] = x << 4 | y >>> 2;
+ v[vi++] = y << 6 | z;
+ }
+ return v;
+ }
+ };
+
+ g.base64url = base64url;
+
+ /* Coerces data into a Uint8Array */
+ function ensureView(data) {
+ if (typeof data === 'string') {
+ return new TextEncoder('utf-8').encode(data);
+ }
+ if (data instanceof ArrayBuffer) {
+ return new Uint8Array(data);
+ }
+ if (ArrayBuffer.isView(data)) {
+ return new Uint8Array(data.buffer);
+ }
+ throw new Error('webpush() needs a string or BufferSource');
+ }
+
+ function bsConcat(arrays) {
+ var size = arrays.reduce((total, a) => total + a.byteLength, 0);
+ var index = 0;
+ return arrays.reduce((result, a) => {
+ result.set(new Uint8Array(a), index);
+ index += a.byteLength;
+ return result;
+ }, new Uint8Array(size));
+ }
+
+ function hmac(key) {
+ this.keyPromise = webCrypto.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' },
+ false, ['sign']);
+ }
+ hmac.prototype.hash = function(input) {
+ return this.keyPromise.then(k => webCrypto.sign('HMAC', k, input));
+ };
+
+ function hkdf(salt, ikm) {
+ this.prkhPromise = new hmac(salt).hash(ikm)
+ .then(prk => new hmac(prk));
+ }
+
+ hkdf.prototype.generate = function(info, len) {
+ var input = bsConcat([info, new Uint8Array([1])]);
+ return this.prkhPromise
+ .then(prkh => prkh.hash(input))
+ .then(h => {
+ if (h.byteLength < len) {
+ throw new Error('Length is too long');
+ }
+ return h.slice(0, len);
+ });
+ };
+
+ /* generate a 96-bit IV for use in GCM, 48-bits of which are populated */
+ function generateNonce(base, index) {
+ var nonce = base.slice(0, 12);
+ for (var i = 0; i < 6; ++i) {
+ nonce[nonce.length - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
+ }
+ return nonce;
+ }
+
+ function encrypt(localKey, remoteShare, salt, data) {
+ return webCrypto.importKey('raw', remoteShare, P256DH, false, ['deriveBits'])
+ .then(remoteKey =>
+ webCrypto.deriveBits({ name: P256DH.name, public: remoteKey },
+ localKey, 256))
+ .then(rawKey => {
+ var kdf = new hkdf(salt, rawKey);
+ return Promise.all([
+ kdf.generate(ENCRYPT_INFO, 16)
+ .then(gcmBits =>
+ webCrypto.importKey('raw', gcmBits, 'AES-GCM', false, ['encrypt'])),
+ kdf.generate(NONCE_INFO, 12)
+ ]);
+ })
+ .then(([key, nonce]) => {
+ if (data.byteLength === 0) {
+ // Send an authentication tag for empty messages.
+ return webCrypto.encrypt({
+ name: 'AES-GCM',
+ iv: generateNonce(nonce, 0)
+ }, key, new Uint8Array([0])).then(value => [value]);
+ }
+ // 4096 is the default size, though we burn 1 for padding
+ return Promise.all(chunkArray(data, 4095).map((slice, index) => {
+ var padded = bsConcat([new Uint8Array([0]), slice]);
+ return webCrypto.encrypt({
+ name: 'AES-GCM',
+ iv: generateNonce(nonce, index)
+ }, key, padded);
+ }));
+ }).then(bsConcat);
+ }
+
+ function webPushEncrypt(subscription, data) {
+ data = ensureView(data);
+
+ var salt = g.crypto.getRandomValues(new Uint8Array(16));
+ return webCrypto.generateKey(P256DH, false, ['deriveBits'])
+ .then(localKey => {
+ return Promise.all([
+ encrypt(localKey.privateKey, subscription.getKey("p256dh"), salt, data),
+ // 1337 p-256 specific haxx to get the raw value out of the spki value
+ webCrypto.exportKey('raw', localKey.publicKey),
+ ]);
+ }).then(([payload, pubkey]) => {
+ return {
+ data: base64url.encode(payload),
+ encryption: 'keyid=p256dh;salt=' + base64url.encode(salt),
+ encryption_key: 'keyid=p256dh;dh=' + base64url.encode(pubkey),
+ encoding: 'aesgcm128'
+ };
+ });
+ }
+
+ g.webPushEncrypt = webPushEncrypt;
+}(this));