From fbb63b31bdb80358625365d5f55776ae762f9930 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Thu, 11 Oct 2018 22:32:25 +0100 Subject: [PATCH 01/20] End-to-end encryption --- lib/auth.js | 7 +++++- lib/config.js | 10 +++++++++ lib/events.js | 57 +++++++++++++++++++++++++++++++++++++++-------- lib/pusher.js | 12 +++++++++- package-lock.json | 28 +++++++++++++++++------ package.json | 3 ++- 6 files changed, 98 insertions(+), 19 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index d6600c1..3326161 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,4 +1,4 @@ -function getSocketSignature(token, channel, socketID, data) { +function getSocketSignature(pusher, token, channel, socketID, data) { var result = {}; var signatureData = [socketID, channel]; @@ -9,6 +9,11 @@ function getSocketSignature(token, channel, socketID, data) { } result.auth = token.key + ':' + token.sign(signatureData.join(":")); + + if (pusher.isEncryptedChannel(channel) && pusher.config.encryptionMasterKey !== undefined) { + result.shared_secret = Buffer(pusher.channelSharedSecret(channel)).toString('base64'); + } + return result; } diff --git a/lib/config.js b/lib/config.js index d09fbe5..a33f738 100644 --- a/lib/config.js +++ b/lib/config.js @@ -22,6 +22,16 @@ function Config(options) { this.proxy = options.proxy; this.timeout = options.timeout; this.keepAlive = options.keepAlive; + + if (options.encryptionMasterKey !== undefined) { + if (typeof(options.encryptionMasterKey) !== 'string') { + throw new Error("encryptionMasterKey must be a string"); + } + if (options.encryptionMasterKey.length !== 32) { + throw new Error("encryptionMasterKey must be 32 characters long"); + } + this.encryptionMasterKey = options.encryptionMasterKey; + } } Config.prototype.prefixPath = function(subPath) { diff --git a/lib/events.js b/lib/events.js index 3c50a57..3bd1c51 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,18 +1,57 @@ +const nacl = require("tweetnacl"); + +function encrypt(pusher, channel, data) { + if (pusher.config.encryptionMasterKey === undefined) { + throw new Error("Set encryptionMasterKey before triggering events on encrypted channels"); + } + + const nonceBytes = nacl.randomBytes(24); + + const ciphertextBytes = nacl.secretbox( + Buffer.from(JSON.stringify(data), 'utf8'), + nonceBytes, + pusher.channelSharedSecret(channel)); + + return JSON.stringify({ + nonce: Buffer(nonceBytes).toString('base64'), + ciphertext: Buffer(ciphertextBytes).toString('base64') + }); +} + exports.trigger = function(pusher, channels, eventName, data, socketId, callback) { - var event = { - "name": eventName, - "data": ensureJSON(data), - "channels": channels - }; - if (socketId) { - event.socket_id = socketId; + const encryptedBatch = []; + const plaintextChannels = []; + + for (var i = 0; i < channels.length; i++) { + if (pusher.isEncryptedChannel(channels[i])) { + encryptedBatch.push({ channel: channels[i], name: eventName, data: data, socket_id: socketId }); + } else { + plaintextChannels.push(channels[i]); + } + } + + if (encryptedBatch.length > 0 && plaintextChannels.length > 0) { + throw new Error("Don't mix encrypted channels with non-encrypted channels in the same call to trigger(). Use triggerBatch() instead"); + } else if (encryptedBatch.length > 0) { + exports.triggerBatch(pusher, encryptedBatch, callback); + } else { + var event = { + "name": eventName, + "data": ensureJSON(data), + "channels": plaintextChannels + }; + if (socketId) { + event.socket_id = socketId; + } + pusher.post({ path: '/events', body: event }, callback); } - pusher.post({ path: '/events', body: event }, callback); } exports.triggerBatch = function(pusher, batch, callback) { for (var i = 0; i < batch.length; i++) { - batch[i].data = ensureJSON(batch[i].data); + batch[i].data = pusher.isEncryptedChannel(batch[i].channel) ? + encrypt(pusher, batch[i].channel, batch[i].data) : + ensureJSON(batch[i].data); } pusher.post({ path: '/batch_events', body: { batch: batch } }, callback); } diff --git a/lib/pusher.js b/lib/pusher.js index 1cc1462..df35d20 100644 --- a/lib/pusher.js +++ b/lib/pusher.js @@ -11,6 +11,8 @@ var Token = require('./token'); var WebHook = require('./webhook'); var NotificationClient = require('./notification_client'); +const crypto = require('crypto'); + var validateChannel = function(channel) { if (typeof channel !== "string" || channel === "" || channel.match(/[^A-Za-z0-9_\-=@,.;]/)) { throw new Error("Invalid channel name: '" + channel + "'"); @@ -106,7 +108,7 @@ Pusher.prototype.authenticate = function(socketId, channel, data) { validateSocketId(socketId); validateChannel(channel); - return auth.getSocketSignature(this.config.token, channel, socketId, data); + return auth.getSocketSignature(this, this.config.token, channel, socketId, data); }; /** Triggers an event. @@ -230,6 +232,14 @@ Pusher.prototype.createSignedQueryString = function(options) { return requests.createSignedQueryString(this.config.token, options); }; +Pusher.prototype.channelSharedSecret = function(channel) { + return crypto.createHash('sha256').update(channel + this.config.encryptionMasterKey).digest(); +} + +Pusher.prototype.isEncryptedChannel = function(channel) { + return channel.startsWith("private-encrypted-"); +} + /** Exported {@link Token} constructor. */ Pusher.Token = Token; /** Exported {@link RequestError} constructor. */ diff --git a/package-lock.json b/package-lock.json index bff4b82..b2a1e7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -204,6 +204,13 @@ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { "tweetnacl": "^0.14.3" + }, + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + } } }, "big.js": { @@ -1473,7 +1480,7 @@ }, "graceful-fs": { "version": "2.0.3", - "resolved": "http://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", "dev": true }, @@ -1672,13 +1679,13 @@ "dependencies": { "commander": { "version": "0.6.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", "dev": true }, "mkdirp": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", "dev": true } @@ -1768,7 +1775,7 @@ }, "lru-cache": { "version": "2.7.3", - "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, @@ -2619,6 +2626,13 @@ "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" + }, + "dependencies": { + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + } } }, "stream-browserify": { @@ -2777,9 +2791,9 @@ } }, "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", + "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" }, "type-detect": { "version": "4.0.8", diff --git a/package.json b/package.json index 97bd4ae..2d00089 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ ], "dependencies": { "@types/request": "^2.47.1", - "request": "2.88.0" + "request": "2.88.0", + "tweetnacl": "^1.0.0" }, "devDependencies": { "expect.js": "=0.3.1", From ea1050410f2f2b13d9c13a3d4546be48ec58314c Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 15:57:45 +0000 Subject: [PATCH 02/20] start tests --- tests/integration/pusher/trigger.js | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index d39a2ea..6646233 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -298,6 +298,57 @@ describe("Pusher", function() { }); }); + describe("Pusher with encryptionMasterKey", function() { + var pusher; + + beforeEach(function() { + pusher = new Pusher({ appId: 1234, key: "f00d", secret: "beef", encryptionMasterKey: "01234567890123456789012345678901" }); + nock.disableNetConnect(); + }); + + afterEach(function() { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + describe("#trigger", function() { + it("should not encrypt the body of an event triggered on a single channel", function(done) { + var mock = nock("http://api.pusherapp.com") + .filteringPath(function(path) { + return path + .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") + .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y"); + }) + .post( + "/apps/1234/events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=e95168baf497b2e54b2c6cadd41a6a3f&auth_signature=Y", + { name: "my_event", data: "{\"some\":\"data \"}", channels: ["one"] } + ) + .reply(200, "{}"); + + pusher.trigger("one", "my_event", { some: "data "}, done); + }); + + it("should encrypt the body of an event triggered on a private-encrypted- channel", function(done) { + var plaintext = "Hello!"; + + var mock = nock("http://api.pusherapp.com") + .filteringPath(function(path) { + return path + .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") + .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y") + .replace(/body_md5=[0-9a-f]{32}/, "body_md5=Z"); + }) + .post( + "/apps/1234/batch_events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=Z&auth_signature=Y", + { batch: [ { name: "event", data: /.*/, channel: "private-encrypted-bla" } ] } + ) + .reply(200, "{}"); + + pusher.trigger("private-encrypted-bla", "event", plaintext, done); + }); + }); + }); + describe("#triggerBatch", function(){ it("should trigger multiple events in a single call", function(done) { var mock = nock("http://api.pusherapp.com") From 4fe82dd11838904f7d85ed24d1c5295fc72a592b Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 15:58:00 +0000 Subject: [PATCH 03/20] more helpful error --- lib/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config.js b/lib/config.js index a33f738..c32ff09 100644 --- a/lib/config.js +++ b/lib/config.js @@ -28,7 +28,7 @@ function Config(options) { throw new Error("encryptionMasterKey must be a string"); } if (options.encryptionMasterKey.length !== 32) { - throw new Error("encryptionMasterKey must be 32 characters long"); + throw new Error("encryptionMasterKey must be 32 characters long, but the string '" + options.encryptionMasterKey + "' is " + options.encryptionMasterKey.length + " characters long"); } this.encryptionMasterKey = options.encryptionMasterKey; } From d1da6342425f4a9c4f92212ba53a7b837465cf0e Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 16:38:37 +0000 Subject: [PATCH 04/20] change policy to match pusher-http-go when triggering to multiple channels --- lib/events.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/events.js b/lib/events.js index 3bd1c51..fcc0832 100644 --- a/lib/events.js +++ b/lib/events.js @@ -19,26 +19,28 @@ function encrypt(pusher, channel, data) { } exports.trigger = function(pusher, channels, eventName, data, socketId, callback) { - const encryptedBatch = []; - const plaintextChannels = []; - - for (var i = 0; i < channels.length; i++) { - if (pusher.isEncryptedChannel(channels[i])) { - encryptedBatch.push({ channel: channels[i], name: eventName, data: data, socket_id: socketId }); - } else { - plaintextChannels.push(channels[i]); + if (channels.length === 1 && pusher.isEncryptedChannel(channels[0])) { + var channel = channels[0]; + var event = { + "name": eventName, + "data": encrypt(pusher, channel, data), + "channel": channel + }; + if (socketId) { + event.socket_id = socketId; } - } - - if (encryptedBatch.length > 0 && plaintextChannels.length > 0) { - throw new Error("Don't mix encrypted channels with non-encrypted channels in the same call to trigger(). Use triggerBatch() instead"); - } else if (encryptedBatch.length > 0) { - exports.triggerBatch(pusher, encryptedBatch, callback); + pusher.post({ path: '/events', body: event }, callback); } else { + for (var i = 0; i < channels.length; i++) { + if (pusher.isEncryptedChannel(channels[i])) { + throw new Error("You cannot trigger to multiple channels when using encrypted channels"); + } + } + var event = { "name": eventName, "data": ensureJSON(data), - "channels": plaintextChannels + "channels": channels }; if (socketId) { event.socket_id = socketId; From 3a3c93351e479b8acad70bd4d4a7d32b093fad8a Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 17:22:53 +0000 Subject: [PATCH 05/20] test by decrypting --- lib/events.js | 2 +- tests/integration/pusher/trigger.js | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/events.js b/lib/events.js index fcc0832..d4ac62f 100644 --- a/lib/events.js +++ b/lib/events.js @@ -24,7 +24,7 @@ exports.trigger = function(pusher, channels, eventName, data, socketId, callback var event = { "name": eventName, "data": encrypt(pusher, channel, data), - "channel": channel + "channels": [channel] }; if (socketId) { event.socket_id = socketId; diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 6646233..a0271f8 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -1,5 +1,6 @@ var expect = require("expect.js"); var nock = require("nock"); +const nacl = require("tweetnacl"); var Pusher = require("../../../lib/pusher"); @@ -300,9 +301,11 @@ describe("Pusher", function() { describe("Pusher with encryptionMasterKey", function() { var pusher; + + var testMasterKey = "01234567890123456789012345678901"; beforeEach(function() { - pusher = new Pusher({ appId: 1234, key: "f00d", secret: "beef", encryptionMasterKey: "01234567890123456789012345678901" }); + pusher = new Pusher({ appId: 1234, key: "f00d", secret: "beef", encryptionMasterKey: testMasterKey }); nock.disableNetConnect(); }); @@ -329,7 +332,7 @@ describe("Pusher", function() { }); it("should encrypt the body of an event triggered on a private-encrypted- channel", function(done) { - var plaintext = "Hello!"; + var sentPlaintext = "Hello!"; var mock = nock("http://api.pusherapp.com") .filteringPath(function(path) { @@ -339,12 +342,25 @@ describe("Pusher", function() { .replace(/body_md5=[0-9a-f]{32}/, "body_md5=Z"); }) .post( - "/apps/1234/batch_events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=Z&auth_signature=Y", - { batch: [ { name: "event", data: /.*/, channel: "private-encrypted-bla" } ] } + "/apps/1234/events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=Z&auth_signature=Y", + function (body) { + if (body.name !== "test_event") return false; + if (body.channels.length !== 1) return false; + var channel = body.channels[0]; + if (channel !== "private-encrypted-bla") return false; + var encrypted = JSON.parse(body.data); + var nonce = Buffer.from(encrypted.nonce, 'base64'); + var ciphertext = Buffer.from(encrypted.ciphertext, 'base64'); + var channelSharedSecret = pusher.channelSharedSecret(channel); + var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); + var receivedPlaintextJson = new TextDecoder("utf-8").decode(receivedPlaintextBytes); + var receivedPlaintext = JSON.parse(receivedPlaintextJson); + return receivedPlaintext === sentPlaintext; + } ) .reply(200, "{}"); - pusher.trigger("private-encrypted-bla", "event", plaintext, done); + pusher.trigger("private-encrypted-bla", "test_event", sentPlaintext, done); }); }); }); From f79a138105c55680703c89ac808469626ee6b769 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 17:30:50 +0000 Subject: [PATCH 06/20] move isEncryptedChannel to util --- lib/auth.js | 4 +++- lib/events.js | 7 ++++--- lib/pusher.js | 4 ---- lib/util.js | 5 +++++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index 3326161..b30e5ea 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,3 +1,5 @@ +var util = require('./util'); + function getSocketSignature(pusher, token, channel, socketID, data) { var result = {}; @@ -10,7 +12,7 @@ function getSocketSignature(pusher, token, channel, socketID, data) { result.auth = token.key + ':' + token.sign(signatureData.join(":")); - if (pusher.isEncryptedChannel(channel) && pusher.config.encryptionMasterKey !== undefined) { + if (util.isEncryptedChannel(channel) && pusher.config.encryptionMasterKey !== undefined) { result.shared_secret = Buffer(pusher.channelSharedSecret(channel)).toString('base64'); } diff --git a/lib/events.js b/lib/events.js index d4ac62f..a82429a 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,3 +1,4 @@ +var util = require('./util'); const nacl = require("tweetnacl"); function encrypt(pusher, channel, data) { @@ -19,7 +20,7 @@ function encrypt(pusher, channel, data) { } exports.trigger = function(pusher, channels, eventName, data, socketId, callback) { - if (channels.length === 1 && pusher.isEncryptedChannel(channels[0])) { + if (channels.length === 1 && util.isEncryptedChannel(channels[0])) { var channel = channels[0]; var event = { "name": eventName, @@ -32,7 +33,7 @@ exports.trigger = function(pusher, channels, eventName, data, socketId, callback pusher.post({ path: '/events', body: event }, callback); } else { for (var i = 0; i < channels.length; i++) { - if (pusher.isEncryptedChannel(channels[i])) { + if (util.isEncryptedChannel(channels[i])) { throw new Error("You cannot trigger to multiple channels when using encrypted channels"); } } @@ -51,7 +52,7 @@ exports.trigger = function(pusher, channels, eventName, data, socketId, callback exports.triggerBatch = function(pusher, batch, callback) { for (var i = 0; i < batch.length; i++) { - batch[i].data = pusher.isEncryptedChannel(batch[i].channel) ? + batch[i].data = util.isEncryptedChannel(batch[i].channel) ? encrypt(pusher, batch[i].channel, batch[i].data) : ensureJSON(batch[i].data); } diff --git a/lib/pusher.js b/lib/pusher.js index df35d20..34f7a05 100644 --- a/lib/pusher.js +++ b/lib/pusher.js @@ -236,10 +236,6 @@ Pusher.prototype.channelSharedSecret = function(channel) { return crypto.createHash('sha256').update(channel + this.config.encryptionMasterKey).digest(); } -Pusher.prototype.isEncryptedChannel = function(channel) { - return channel.startsWith("private-encrypted-"); -} - /** Exported {@link Token} constructor. */ Pusher.Token = Token; /** Exported {@link RequestError} constructor. */ diff --git a/lib/util.js b/lib/util.js index 098fc8d..f8bd965 100644 --- a/lib/util.js +++ b/lib/util.js @@ -38,7 +38,12 @@ function secureCompare(a, b) { return result === 0; } +function isEncryptedChannel(channel) { + return channel.startsWith("private-encrypted-"); +} + exports.toOrderedArray = toOrderedArray; exports.mergeObjects = mergeObjects; exports.getMD5 = getMD5; exports.secureCompare = secureCompare; +exports.isEncryptedChannel = isEncryptedChannel; \ No newline at end of file From 48104cba0147511bf4adceabc391c8fc359c83b7 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 17:34:30 +0000 Subject: [PATCH 07/20] use var, not const --- lib/events.js | 2 +- tests/integration/pusher/trigger.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events.js b/lib/events.js index a82429a..ab48b35 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,5 +1,5 @@ var util = require('./util'); -const nacl = require("tweetnacl"); +var nacl = require('tweetnacl'); function encrypt(pusher, channel, data) { if (pusher.config.encryptionMasterKey === undefined) { diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index a0271f8..9af7211 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -1,6 +1,6 @@ var expect = require("expect.js"); var nock = require("nock"); -const nacl = require("tweetnacl"); +var nacl = require('tweetnacl'); var Pusher = require("../../../lib/pusher"); From 1c53ade142a619ca47f7c970d6c7e8c364c05d49 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 17:41:37 +0000 Subject: [PATCH 08/20] throw error instead of generating auth response, if client asks for auth for encrypted channel but we don't have the master key --- lib/auth.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/auth.js b/lib/auth.js index b30e5ea..4fdd8e9 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -12,7 +12,10 @@ function getSocketSignature(pusher, token, channel, socketID, data) { result.auth = token.key + ':' + token.sign(signatureData.join(":")); - if (util.isEncryptedChannel(channel) && pusher.config.encryptionMasterKey !== undefined) { + if (util.isEncryptedChannel(channel)) { + if (pusher.config.encryptionMasterKey === undefined) { + throw new Error("Cannot generate shared_secret because encryptionMasterKey is not set"); + } result.shared_secret = Buffer(pusher.channelSharedSecret(channel)).toString('base64'); } From 5651fb280ba49f7494577e1c64d05a9da9d9e981 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 17:55:20 +0000 Subject: [PATCH 09/20] some tests for authenticate function --- tests/integration/pusher/authenticate.js | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/integration/pusher/authenticate.js b/tests/integration/pusher/authenticate.js index af8b8ff..1250af5 100644 --- a/tests/integration/pusher/authenticate.js +++ b/tests/integration/pusher/authenticate.js @@ -124,5 +124,37 @@ describe("Pusher", function() { pusher.authenticate("111.222", "") }).to.throwException(/^Invalid channel name: ''$/); }); + + it("should throw an error for private-encrypted- channels", function() { + expect(function() { + pusher.authenticate("123.456", "private-encrypted-bla", "foo"); + }).to.throwException('Cannot generate shared_secret because encryptionMasterKey is not set'); + }); }); }); + +describe("Pusher with encryptionMasterKey", function() { + var pusher; + + var testMasterKey = "01234567890123456789012345678901"; + + beforeEach(function() { + pusher = new Pusher({ appId: 1234, key: "f00d", secret: "beef", encryptionMasterKey: testMasterKey }); + }); + + describe("#auth", function() { + it("should return a shared_secret for private-encrypted- channels", function() { + expect(pusher.authenticate("123.456", "private-encrypted-bla", "foo")).to.eql({ + auth: "f00d:d8df1e524cf38fbde4f1dc38e6eaa4943e60412122801eed1f0e89c8a1268784", + channel_data: "\"foo\"", + shared_secret: "BYBsePpRCQkGPvbWu/5j8x+MmUF5sgPH5DmNBwkTzYs=" + }); + }); + it("should not return a shared_secret for non-encrypted channels", function() { + expect(pusher.authenticate("123.456", "bla", "foo")).to.eql({ + auth: "f00d:4c48fa1cb34537501eb3291b28c0b04de270008ae418bc3141f4f11680abe312", + channel_data: "\"foo\"", + }); + }); + }); +}); \ No newline at end of file From 5c3307c4522bc5c5fde48bc5f4c27babd1d3cce1 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 18:18:23 +0000 Subject: [PATCH 10/20] use StringDecoder instead of TextDecoder to support Node.JS version 4+ --- tests/integration/pusher/trigger.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 9af7211..8ee65bd 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -1,6 +1,7 @@ var expect = require("expect.js"); var nock = require("nock"); var nacl = require('tweetnacl'); +var StringDecoder = require('string_decoder').StringDecoder; var Pusher = require("../../../lib/pusher"); @@ -353,7 +354,9 @@ describe("Pusher", function() { var ciphertext = Buffer.from(encrypted.ciphertext, 'base64'); var channelSharedSecret = pusher.channelSharedSecret(channel); var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); - var receivedPlaintextJson = new TextDecoder("utf-8").decode(receivedPlaintextBytes); + var decoder = new StringDecoder('utf8'); + var receivedPlaintextJson = decoder.write(receivedPlaintextBytes); + receivedPlaintextJson += decoder.end(); var receivedPlaintext = JSON.parse(receivedPlaintextJson); return receivedPlaintext === sentPlaintext; } From 66dd9a8477b4b588a8b35dd91b5e0135932219b4 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 18:23:55 +0000 Subject: [PATCH 11/20] debug travisci --- tests/integration/pusher/trigger.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 8ee65bd..9952afe 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -357,6 +357,7 @@ describe("Pusher", function() { var decoder = new StringDecoder('utf8'); var receivedPlaintextJson = decoder.write(receivedPlaintextBytes); receivedPlaintextJson += decoder.end(); + console.log("received plaintext json", JSON.stringify(receivedPlaintextJson));// debug travisci var receivedPlaintext = JSON.parse(receivedPlaintextJson); return receivedPlaintext === sentPlaintext; } From ab08f29d1ab0848eb125ab95436f147252f54843 Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 18:34:32 +0000 Subject: [PATCH 12/20] debug --- tests/integration/pusher/trigger.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 9952afe..636cd45 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -356,8 +356,9 @@ describe("Pusher", function() { var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); var decoder = new StringDecoder('utf8'); var receivedPlaintextJson = decoder.write(receivedPlaintextBytes); + console.log("1 received plaintext json", typeofs(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci receivedPlaintextJson += decoder.end(); - console.log("received plaintext json", JSON.stringify(receivedPlaintextJson));// debug travisci + console.log("2 received plaintext json", typeofs(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci var receivedPlaintext = JSON.parse(receivedPlaintextJson); return receivedPlaintext === sentPlaintext; } From c16bbea48b6ed2be9bb1deb980b654dded75250b Mon Sep 17 00:00:00 2001 From: Jim Fisher Date: Tue, 20 Nov 2018 18:36:17 +0000 Subject: [PATCH 13/20] debug --- tests/integration/pusher/trigger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 636cd45..63338b6 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -356,9 +356,9 @@ describe("Pusher", function() { var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); var decoder = new StringDecoder('utf8'); var receivedPlaintextJson = decoder.write(receivedPlaintextBytes); - console.log("1 received plaintext json", typeofs(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci + console.log("1 received plaintext json", typeof(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci receivedPlaintextJson += decoder.end(); - console.log("2 received plaintext json", typeofs(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci + console.log("2 received plaintext json", typeof(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci var receivedPlaintext = JSON.parse(receivedPlaintextJson); return receivedPlaintext === sentPlaintext; } From 5456780afc6871b54475f2505ee2cc9f31f2c6fc Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Wed, 21 Nov 2018 17:34:43 +0000 Subject: [PATCH 14/20] Use tweetnacl-util as base64/utf8 codec (same as pusher-js --- lib/events.js | 7 ++++--- package-lock.json | 5 +++++ package.json | 3 ++- tests/integration/pusher/trigger.js | 12 ++++-------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/events.js b/lib/events.js index ab48b35..64be66c 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1,5 +1,6 @@ var util = require('./util'); var nacl = require('tweetnacl'); +var naclUtil = require('tweetnacl-util'); function encrypt(pusher, channel, data) { if (pusher.config.encryptionMasterKey === undefined) { @@ -9,13 +10,13 @@ function encrypt(pusher, channel, data) { const nonceBytes = nacl.randomBytes(24); const ciphertextBytes = nacl.secretbox( - Buffer.from(JSON.stringify(data), 'utf8'), + naclUtil.decodeUTF8(JSON.stringify(data)), nonceBytes, pusher.channelSharedSecret(channel)); return JSON.stringify({ - nonce: Buffer(nonceBytes).toString('base64'), - ciphertext: Buffer(ciphertextBytes).toString('base64') + nonce: naclUtil.encodeBase64(nonceBytes), + ciphertext: naclUtil.encodeBase64(ciphertextBytes) }); } diff --git a/package-lock.json b/package-lock.json index b2a1e7b..0d09095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2795,6 +2795,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" }, + "tweetnacl-util": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz", + "integrity": "sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU=" + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/package.json b/package.json index 2d00089..6b766f6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "dependencies": { "@types/request": "^2.47.1", "request": "2.88.0", - "tweetnacl": "^1.0.0" + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" }, "devDependencies": { "expect.js": "=0.3.1", diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 63338b6..85035a7 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -1,7 +1,7 @@ var expect = require("expect.js"); var nock = require("nock"); var nacl = require('tweetnacl'); -var StringDecoder = require('string_decoder').StringDecoder; +var naclUtil = require('tweetnacl-util'); var Pusher = require("../../../lib/pusher"); @@ -350,15 +350,11 @@ describe("Pusher", function() { var channel = body.channels[0]; if (channel !== "private-encrypted-bla") return false; var encrypted = JSON.parse(body.data); - var nonce = Buffer.from(encrypted.nonce, 'base64'); - var ciphertext = Buffer.from(encrypted.ciphertext, 'base64'); + var nonce = naclUtil.decodeBase64(encrypted.nonce); + var ciphertext = naclUtil.decodeBase64(encrypted.ciphertext); var channelSharedSecret = pusher.channelSharedSecret(channel); var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); - var decoder = new StringDecoder('utf8'); - var receivedPlaintextJson = decoder.write(receivedPlaintextBytes); - console.log("1 received plaintext json", typeof(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci - receivedPlaintextJson += decoder.end(); - console.log("2 received plaintext json", typeof(receivedPlaintextJson), JSON.stringify(receivedPlaintextJson));// debug travisci + var receivedPlaintextJson = naclUtil.encodeUTF8(receivedPlaintextBytes); var receivedPlaintext = JSON.parse(receivedPlaintextJson); return receivedPlaintext === sentPlaintext; } From c7efba76cf0660f6583fb8dd46317c9e3ba8d992 Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Wed, 21 Nov 2018 17:41:13 +0000 Subject: [PATCH 15/20] const -> var --- lib/events.js | 4 ++-- lib/pusher.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/events.js b/lib/events.js index 64be66c..0097f4a 100644 --- a/lib/events.js +++ b/lib/events.js @@ -7,9 +7,9 @@ function encrypt(pusher, channel, data) { throw new Error("Set encryptionMasterKey before triggering events on encrypted channels"); } - const nonceBytes = nacl.randomBytes(24); + var nonceBytes = nacl.randomBytes(24); - const ciphertextBytes = nacl.secretbox( + var ciphertextBytes = nacl.secretbox( naclUtil.decodeUTF8(JSON.stringify(data)), nonceBytes, pusher.channelSharedSecret(channel)); diff --git a/lib/pusher.js b/lib/pusher.js index 34f7a05..2a40f10 100644 --- a/lib/pusher.js +++ b/lib/pusher.js @@ -1,3 +1,4 @@ +var crypto = require('crypto'); var url = require('url'); var auth = require('./auth'); @@ -11,8 +12,6 @@ var Token = require('./token'); var WebHook = require('./webhook'); var NotificationClient = require('./notification_client'); -const crypto = require('crypto'); - var validateChannel = function(channel) { if (typeof channel !== "string" || channel === "" || channel.match(/[^A-Za-z0-9_\-=@,.;]/)) { throw new Error("Invalid channel name: '" + channel + "'"); From ef83452d52179bfcc688e9cf8a47116b1c5ef1fc Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Wed, 21 Nov 2018 17:41:42 +0000 Subject: [PATCH 16/20] Add missing trailing newline --- tests/integration/pusher/authenticate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/pusher/authenticate.js b/tests/integration/pusher/authenticate.js index 1250af5..cc833cb 100644 --- a/tests/integration/pusher/authenticate.js +++ b/tests/integration/pusher/authenticate.js @@ -157,4 +157,4 @@ describe("Pusher with encryptionMasterKey", function() { }); }); }); -}); \ No newline at end of file +}); From c6168d84bbb23e920ea4718277ad0c37fbf89ccd Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Wed, 21 Nov 2018 17:48:12 +0000 Subject: [PATCH 17/20] Put crypto tests at the top level --- tests/integration/pusher/trigger.js | 132 ++++++++++++++-------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 85035a7..c405518 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -300,72 +300,6 @@ describe("Pusher", function() { }); }); - describe("Pusher with encryptionMasterKey", function() { - var pusher; - - var testMasterKey = "01234567890123456789012345678901"; - - beforeEach(function() { - pusher = new Pusher({ appId: 1234, key: "f00d", secret: "beef", encryptionMasterKey: testMasterKey }); - nock.disableNetConnect(); - }); - - afterEach(function() { - nock.cleanAll(); - nock.enableNetConnect(); - }); - - describe("#trigger", function() { - it("should not encrypt the body of an event triggered on a single channel", function(done) { - var mock = nock("http://api.pusherapp.com") - .filteringPath(function(path) { - return path - .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") - .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y"); - }) - .post( - "/apps/1234/events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=e95168baf497b2e54b2c6cadd41a6a3f&auth_signature=Y", - { name: "my_event", data: "{\"some\":\"data \"}", channels: ["one"] } - ) - .reply(200, "{}"); - - pusher.trigger("one", "my_event", { some: "data "}, done); - }); - - it("should encrypt the body of an event triggered on a private-encrypted- channel", function(done) { - var sentPlaintext = "Hello!"; - - var mock = nock("http://api.pusherapp.com") - .filteringPath(function(path) { - return path - .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") - .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y") - .replace(/body_md5=[0-9a-f]{32}/, "body_md5=Z"); - }) - .post( - "/apps/1234/events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=Z&auth_signature=Y", - function (body) { - if (body.name !== "test_event") return false; - if (body.channels.length !== 1) return false; - var channel = body.channels[0]; - if (channel !== "private-encrypted-bla") return false; - var encrypted = JSON.parse(body.data); - var nonce = naclUtil.decodeBase64(encrypted.nonce); - var ciphertext = naclUtil.decodeBase64(encrypted.ciphertext); - var channelSharedSecret = pusher.channelSharedSecret(channel); - var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); - var receivedPlaintextJson = naclUtil.encodeUTF8(receivedPlaintextBytes); - var receivedPlaintext = JSON.parse(receivedPlaintextJson); - return receivedPlaintext === sentPlaintext; - } - ) - .reply(200, "{}"); - - pusher.trigger("private-encrypted-bla", "test_event", sentPlaintext, done); - }); - }); - }); - describe("#triggerBatch", function(){ it("should trigger multiple events in a single call", function(done) { var mock = nock("http://api.pusherapp.com") @@ -422,3 +356,69 @@ describe("Pusher", function() { }); }) }); + +describe("Pusher with encryptionMasterKey", function() { + var pusher; + + var testMasterKey = "01234567890123456789012345678901"; + + beforeEach(function() { + pusher = new Pusher({ appId: 1234, key: "f00d", secret: "beef", encryptionMasterKey: testMasterKey }); + nock.disableNetConnect(); + }); + + afterEach(function() { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + describe("#trigger", function() { + it("should not encrypt the body of an event triggered on a single channel", function(done) { + var mock = nock("http://api.pusherapp.com") + .filteringPath(function(path) { + return path + .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") + .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y"); + }) + .post( + "/apps/1234/events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=e95168baf497b2e54b2c6cadd41a6a3f&auth_signature=Y", + { name: "my_event", data: "{\"some\":\"data \"}", channels: ["one"] } + ) + .reply(200, "{}"); + + pusher.trigger("one", "my_event", { some: "data "}, done); + }); + + it("should encrypt the body of an event triggered on a private-encrypted- channel", function(done) { + var sentPlaintext = "Hello!"; + + var mock = nock("http://api.pusherapp.com") + .filteringPath(function(path) { + return path + .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") + .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y") + .replace(/body_md5=[0-9a-f]{32}/, "body_md5=Z"); + }) + .post( + "/apps/1234/events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=Z&auth_signature=Y", + function (body) { + if (body.name !== "test_event") return false; + if (body.channels.length !== 1) return false; + var channel = body.channels[0]; + if (channel !== "private-encrypted-bla") return false; + var encrypted = JSON.parse(body.data); + var nonce = naclUtil.decodeBase64(encrypted.nonce); + var ciphertext = naclUtil.decodeBase64(encrypted.ciphertext); + var channelSharedSecret = pusher.channelSharedSecret(channel); + var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); + var receivedPlaintextJson = naclUtil.encodeUTF8(receivedPlaintextBytes); + var receivedPlaintext = JSON.parse(receivedPlaintextJson); + return receivedPlaintext === sentPlaintext; + } + ) + .reply(200, "{}"); + + pusher.trigger("private-encrypted-bla", "test_event", sentPlaintext, done); + }); + }); +}); \ No newline at end of file From 0fd659a822856fada22f114e4421d42b2472075d Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Thu, 22 Nov 2018 11:21:48 +0000 Subject: [PATCH 18/20] Test encrypted triggerBatch --- tests/integration/pusher/trigger.js | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index c405518..253ae4e 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -421,4 +421,49 @@ describe("Pusher with encryptionMasterKey", function() { pusher.trigger("private-encrypted-bla", "test_event", sentPlaintext, done); }); }); + + describe("#triggerBatch", function(){ + it("should encrypt the bodies of an events triggered on a private-encrypted- channels", function(done) { + var mock = nock("http://api.pusherapp.com") + .filteringPath(function(path) { + return path + .replace(/auth_timestamp=[0-9]+/, "auth_timestamp=X") + .replace(/auth_signature=[0-9a-f]{64}/, "auth_signature=Y") + .replace(/body_md5=[0-9a-f]{32}/, "body_md5=Z"); + }) + .post( + "/apps/1234/batch_events?auth_key=f00d&auth_timestamp=X&auth_version=1.0&body_md5=Z&auth_signature=Y", + function (body) { + if (body.batch.length !== 2) return false; + var event1 = body.batch[0]; + if (event1.channel !== "integration") return false; + if (event1.name !== "event") return false; + if (event1.data !== "test") return false; + var event2 = body.batch[1]; + if (event2.channel !== "private-encrypted-integration2") return false; + if (event2.name !== "event2") return false; + var encrypted = JSON.parse(event2.data); + var nonce = naclUtil.decodeBase64(encrypted.nonce); + var ciphertext = naclUtil.decodeBase64(encrypted.ciphertext); + var channelSharedSecret = pusher.channelSharedSecret(event2.channel); + var receivedPlaintextBytes = nacl.secretbox.open(ciphertext, nonce, channelSharedSecret); + var receivedPlaintextJson = naclUtil.encodeUTF8(receivedPlaintextBytes); + var receivedPlaintext = JSON.parse(receivedPlaintextJson); + return receivedPlaintext === 'test2'; + } + ) + .reply(200, "{}"); + + pusher.triggerBatch([{ + channel: "integration", + name: "event", + data: "test" + }, + { + channel: "private-encrypted-integration2", + name: "event2", + data: "test2" + }], done); + }); + }); }); \ No newline at end of file From c1b84b8751994a3ccaafd5fde8ea154345043d0c Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Fri, 23 Nov 2018 10:34:22 +0000 Subject: [PATCH 19/20] Add trailing newline to trigger.js --- tests/integration/pusher/trigger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/pusher/trigger.js b/tests/integration/pusher/trigger.js index 253ae4e..fe08c10 100644 --- a/tests/integration/pusher/trigger.js +++ b/tests/integration/pusher/trigger.js @@ -466,4 +466,4 @@ describe("Pusher with encryptionMasterKey", function() { }], done); }); }); -}); \ No newline at end of file +}); From d697baf428105c31198da8bb207448e16e3171c6 Mon Sep 17 00:00:00 2001 From: Will Sewell Date: Mon, 26 Nov 2018 10:13:20 +0000 Subject: [PATCH 20/20] Document end-to-end encryption (mostly copied from PHP sdk docs) --- README.md | 34 ++++++++++++++++++++++++++++++++++ lib/events.js | 1 + 2 files changed, 35 insertions(+) diff --git a/README.md b/README.md index cad2540..10b31a5 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ var pusher = new Pusher({ cluster: 'CLUSTER', // if `host` is present, it will override the `cluster` option. host: 'HOST', // optional, defaults to api.pusherapp.com port: PORT, // optional, defaults to 80 for non-TLS connections and 443 for TLS connections + encryptionMasterKey: ENCRYPTION_MASTER_KEY, // a 32 character long key used to derive secrets for end to end encryption (see below!) }); ``` @@ -70,6 +71,7 @@ var pusher = Pusher.forCluster("CLUSTER", { secret: 'SECRET_KEY', useTLS: USE_TLS, // optional, defaults to false port: PORT, // optional, defaults to 80 for non-TLS connections and 443 for TLS connections + encryptionMasterKey: ENCRYPTION_MASTER_KEY, // a 32 character long key used to derive secrets for end to end encryption (see below!) }); ``` @@ -169,6 +171,38 @@ var socketId = '1302.1081607'; pusher.trigger(channel, event, data, socketId); ``` +### End-to-end encryption [BETA] + +This library supports end-to-end encryption of your private channels. This means that only you and your connected clients will be able to read your messages. Pusher cannot decrypt them. You can enable this feature by following these steps: + +1. You should first set up Private channels. This involves [creating an authentication endpoint on your server](https://pusher.com/docs/authenticating_users). + +2. Next, Specify your 32 character `encryption_master_key`. This is secret and you should never share this with anyone. Not even Pusher. + + ```javascript + var pusher = new Pusher({ + appId: 'APP_ID', + key: 'APP_KEY', + secret: 'SECRET_KEY', + useTLS: true, + encryptionMasterKey: 'abcdefghijklmnopqrstuvwxyzabcdef', + }); + ``` + +3. Channels where you wish to use end-to-end encryption should be prefixed with `private-encrypted-`. + +4. Subscribe to these channels in your client, and you're done! You can verify it is working by checking out the debug console on the [https://dashboard.pusher.com/](dashboard) and seeing the scrambled ciphertext. + +**Important note: This will __not__ encrypt messages on channels that are not prefixed by `private-encrypted-`.** + +**Limitation**: you cannot trigger a single event on multiple channels in a call to `trigger`, e.g. + +```javascript +pusher.trigger([ 'channel-1', 'private-encrypted-channel-2' ], 'test_event', { message: "hello world" }); +``` + +Rationale: the methods in this library map directly to individual Channels HTTP API requests. If we allowed triggering a single event on multiple channels (some encrypted, some unencrypted), then it would require two API requests: one where the event is encrypted to the encrypted channels, and one where the event is unencrypted for unencrypted channels. + ### Push Notifications [BETA] Pusher now allows sending native notifications to iOS and Android devices. Check out the [documentation](https://pusher.com/docs/push_notifications) for information on how to set up push notifications on Android and iOS. There is no additional setup required to use it with this library. It works out of the box wit the same Pusher instance. All you need are the same pusher credentials. diff --git a/lib/events.js b/lib/events.js index 0097f4a..e8a74f3 100644 --- a/lib/events.js +++ b/lib/events.js @@ -35,6 +35,7 @@ exports.trigger = function(pusher, channels, eventName, data, socketId, callback } else { for (var i = 0; i < channels.length; i++) { if (util.isEncryptedChannel(channels[i])) { + // For rationale, see limitations of end-to-end encryption in the README throw new Error("You cannot trigger to multiple channels when using encrypted channels"); } }