Skip to content

Commit 4d24158

Browse files
committed
encryption support for trigger
1 parent 799a7e4 commit 4d24158

File tree

3 files changed

+102
-3
lines changed

3 files changed

+102
-3
lines changed

lib/pusher/client.rb

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
require 'base64'
2+
13
require 'pusher-signature'
24

35
module Pusher
46
class Client
5-
attr_accessor :scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
7+
attr_accessor :scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme, :encryption_master_key
68
attr_reader :http_proxy, :proxy
79
attr_writer :connect_timeout, :send_timeout, :receive_timeout,
810
:keep_alive_timeout
@@ -55,6 +57,11 @@ def initialize(options = {})
5557
:scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
5658
)
5759

60+
if options.has_key?(:encryption_master_key_base64)
61+
@encryption_master_key =
62+
Base64.decode64(options[:encryption_master_key_base64])
63+
end
64+
5865
@http_proxy = nil
5966
self.http_proxy = options[:http_proxy] if options[:http_proxy]
6067

@@ -138,6 +145,12 @@ def timeout=(value)
138145
@connect_timeout, @send_timeout, @receive_timeout = value, value, value
139146
end
140147

148+
# Set an encryption_master_key to use with private-encrypted channels from
149+
# a base64 encoded string.
150+
def encryption_master_key_base64=(s)
151+
@encryption_master_key = s ? Base64.decode64(s) : nil
152+
end
153+
141154
## INTERACT WITH THE API ##
142155

143156
def resource(path)
@@ -413,10 +426,17 @@ def trigger_params(channels, event_name, data, params)
413426
channels = Array(channels).map(&:to_s)
414427
raise Pusher::Error, "Too many channels (#{channels.length}), max 10" if channels.length > 10
415428

429+
encoded_data = if channels.any?(/^private-encrypted-/) then
430+
raise Pusher::Error, "Cannot trigger to multiple channels if any are encrypted" if channels.length > 1
431+
encrypt(channels[0], encode_data(data))
432+
else
433+
encode_data(data)
434+
end
435+
416436
params.merge({
417437
name: event_name,
418438
channels: channels,
419-
data: encode_data(data),
439+
data: encoded_data,
420440
})
421441
end
422442

@@ -436,6 +456,28 @@ def encode_data(data)
436456
MultiJson.encode(data)
437457
end
438458

459+
# Encrypts a message with a key derived from the master key and channel
460+
# name
461+
def encrypt(channel, encoded_data)
462+
raise ConfigurationError, :encryption_master_key unless @encryption_master_key
463+
464+
# Only now load rbnacl, so that people that aren't using it don't need to
465+
# install libsodium
466+
require 'rbnacl'
467+
468+
secret_box = RbNaCl::SecretBox.new(
469+
RbNaCl::Hash.sha256(channel + @encryption_master_key)
470+
)
471+
472+
nonce = RbNaCl::Random.random_bytes(secret_box.nonce_bytes)
473+
ciphertext = secret_box.encrypt(nonce, encoded_data)
474+
475+
MultiJson.encode({
476+
"nonce" => Base64::encode64(nonce),
477+
"ciphertext" => Base64::encode64(ciphertext),
478+
})
479+
end
480+
439481
def configured?
440482
host && scheme && key && secret && app_id
441483
end

pusher.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Gem::Specification.new do |s|
1717
s.add_dependency 'pusher-signature', "~> 0.1.8"
1818
s.add_dependency "httpclient", "~> 2.7"
1919
s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
20+
s.add_dependency "rbnacl", "~> 7.1"
2021

2122
s.add_development_dependency "rspec", "~> 3.0"
2223
s.add_development_dependency "webmock"

spec/client_spec.rb

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
require 'spec_helper'
1+
require 'base64'
22

3+
require 'rbnacl'
34
require 'em-http'
45

6+
require 'spec_helper'
7+
8+
encryption_master_key = RbNaCl::Random.random_bytes(32)
9+
510
describe Pusher do
611
# The behaviour should be the same when using the Client object, or the
712
# 'global' client delegated through the Pusher class
@@ -171,11 +176,22 @@
171176
end
172177
end
173178

179+
describe 'can set encryption_master_key_base64' do
180+
it "sets encryption_master_key" do
181+
@client.encryption_master_key_base64 =
182+
Base64.encode64(encryption_master_key)
183+
184+
expect(@client.encryption_master_key).to eq(encryption_master_key)
185+
end
186+
end
187+
174188
describe 'when configured' do
175189
before :each do
176190
@client.app_id = '20'
177191
@client.key = '12345678900000001'
178192
@client.secret = '12345678900000001'
193+
@client.encryption_master_key_base64 =
194+
Base64.encode64(encryption_master_key)
179195
end
180196

181197
describe '#[]' do
@@ -321,6 +337,46 @@
321337
}
322338
end
323339
end
340+
341+
it "should fail to publish to encrypted channels when missing key" do
342+
@client.encryption_master_key_base64 = nil
343+
expect {
344+
@client.trigger('private-encrypted-channel', 'event', {'some' => 'data'})
345+
}.to raise_error(Pusher::ConfigurationError)
346+
expect(WebMock).not_to have_requested(:post, @api_path)
347+
end
348+
349+
it "should fail to publish to multiple channels if one is encrypted" do
350+
expect {
351+
@client.trigger(
352+
['private-encrypted-channel', 'some-other-channel'],
353+
'event',
354+
{'some' => 'data'},
355+
)
356+
}.to raise_error(Pusher::Error)
357+
expect(WebMock).not_to have_requested(:post, @api_path)
358+
end
359+
360+
it "should encrypt publishes to encrypted channels" do
361+
@client.trigger(
362+
'private-encrypted-channel',
363+
'event',
364+
{'some' => 'data'},
365+
)
366+
367+
expect(WebMock).to have_requested(:post, @api_path).with { |req|
368+
data = MultiJson.decode(MultiJson.decode(req.body)["data"])
369+
370+
key = RbNaCl::Hash.sha256(
371+
'private-encrypted-channel' + encryption_master_key
372+
)
373+
374+
expect(MultiJson.decode(RbNaCl::SecretBox.new(key).decrypt(
375+
Base64.decode64(data["nonce"]),
376+
Base64.decode64(data["ciphertext"]),
377+
))).to eq({ 'some' => 'data' })
378+
}
379+
end
324380
end
325381

326382
describe '#trigger_batch' do

0 commit comments

Comments
 (0)