Skip to content

Commit 5f0cb13

Browse files
committed
Accept the master key as base64
1 parent 31eceba commit 5f0cb13

File tree

6 files changed

+99
-48
lines changed

6 files changed

+99
-48
lines changed

pusher/crypto.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import hashlib
1010
import nacl
1111
import base64
12+
import binascii
1213

1314
from pusher.util import (
1415
ensure_text,
@@ -29,27 +30,45 @@ def is_encrypted_channel(channel):
2930
return True
3031
return False
3132

32-
def is_encryption_master_key_valid(encryption_master_key):
33+
def parse_master_key(encryption_master_key, encryption_master_key_base64):
3334
"""
34-
is_encryption_master_key_valid() checks if the provided encryption_master_key is valid by checking its length
35-
the key is assumed to be a six.binary_type (python2 str or python3 bytes)
35+
parse_master_key validates, parses and returns the bytes of the encryption master key
36+
from the constructor arguments.
37+
At present there is a deprecated "raw" key and a suggested base64 encoding.
3638
"""
37-
if encryption_master_key is not None and len(encryption_master_key) == 32:
38-
return True
39+
if encryption_master_key is not None and encryption_master_key_base64 is not None:
40+
raise ValueError("Do not provide both encryption_master_key and encryption_master_key_base64. " +
41+
"encryption_master_key is deprecated, provide only encryption_master_key_base64")
3942

40-
return False
43+
if encryption_master_key is not None:
44+
if len(encryption_master_key) == 32:
45+
return ensure_binary(encryption_master_key, "encryption_master_key")
46+
else:
47+
raise ValueError("encryption_master_key must be 32 bytes long. It is also deprecated, please use encryption_master_key_base64")
48+
49+
if encryption_master_key_base64 is not None:
50+
try:
51+
decoded = base64.b64decode(encryption_master_key_base64, validate=True)
52+
53+
if len(decoded) == 32:
54+
return decoded
55+
else:
56+
raise ValueError("encryption_master_key_base64 must be a base64 string which decodes to 32 bytes")
57+
except binascii.Error:
58+
raise ValueError("encryption_master_key_base64 must be valid base64")
59+
60+
return None
4161

4262
def generate_shared_secret(channel, encryption_master_key):
4363
"""
4464
generate_shared_secret() takes a six.binary_type (python2 str or python3 bytes) channel name and encryption_master_key
4565
and returns the sha256 hash in six.binary_type format
4666
"""
47-
if is_encryption_master_key_valid(encryption_master_key):
48-
# the key has to be 32 bytes long
49-
hashable = channel + encryption_master_key
50-
return hashlib.sha256(hashable).digest()
67+
if encryption_master_key is None:
68+
raise ValueError("No master key was provided for use with encrypted channels. Please provide encryption_master_key_base64 as an argument to the Pusher SDK")
5169

52-
raise ValueError("Provided encryption_master_key is not 32 char long")
70+
hashable = channel + encryption_master_key
71+
return hashlib.sha256(hashable).digest()
5372

5473
def encrypt(channel, data, encryption_master_key, nonce=None):
5574
"""

pusher/pusher_client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ def trigger(self, channels, event_name, data, socket_id=None):
125125
return Request(self, POST, "/apps/%s/events" % self.app_id, params)
126126

127127

128-
129128
@request_method
130129
def trigger_batch(self, batch=[], already_encoded=False):
131130
"""Trigger multiple events with a single HTTP call.

pusher/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import re
1111
import six
1212
import sys
13+
import base64
1314

1415
channel_name_re = re.compile('\A[-a-zA-Z0-9_=@,.;]+\Z')
1516
app_id_re = re.compile('\A[0-9]+\Z')

pusher_tests/test_authentication_client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,16 @@ def test_authenticate_for_private_channels(self):
3434
self.assertEqual(authenticationClient.authenticate(u'private-channel', u'345.23'), expected)
3535

3636
def test_authenticate_for_private_encrypted_channels(self):
37-
encryp_master_key=u'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
37+
# The authentication client receives the decoded bytes of the key
38+
# not the base64 representation
39+
master_key=b'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
3840
authenticationClient = AuthenticationClient(
39-
key=u'foo', secret=u'bar', host=u'host', app_id=u'4', encryption_master_key=encryp_master_key, ssl=True)
41+
key=u'foo',
42+
secret=u'bar',
43+
host=u'host',
44+
app_id=u'4',
45+
encryption_master_key=master_key,
46+
ssl=True)
4047

4148
expected = {
4249
u'auth': u'foo:fff0503dfe4929f5162efe4d1dacbce524b0d8e7e1331117a8651c0e74d369e3',

pusher_tests/test_crypto.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,40 @@ def test_is_encrypted_channel(self):
2525
{ "input":"--djsah private-encrypted-ajkshdak", "expected":False }
2626
]
2727

28-
# do the actual testing
2928
for t in testcases:
3029
self.assertEqual(
3130
crypto.is_encrypted_channel( t["input"] ),
32-
t["expected"]
31+
t["expected"],
3332
)
3433

35-
def test_is_encryption_master_key_valid(self):
34+
def test_parse_master_key_successes(self):
3635
testcases = [
37-
{ "input":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "expected":True },
38-
{ "input":" ", "expected":True },
39-
{ "input":"private--encrypted--djsahajksha-", "expected":True },
40-
{ "input":"dadasda;enc ted--djsahajkshdak", "expected":True },
41-
{ "input":"--====9.djsahajkshdak", "expected":False }
42-
]
36+
{ "deprecated": "this is 32 bytes 123456789012345", "base64": None, "expected": b"this is 32 bytes 123456789012345" },
37+
{ "deprecated": "this key has nonprintable char \x00", "base64": None, "expected": b"this key has nonprintable char \x00"},
38+
{ "deprecated": None, "base64": "dGhpcyBpcyAzMiBieXRlcyAxMjM0NTY3ODkwMTIzNDU=", "expected": b"this is 32 bytes 123456789012345" },
39+
{ "deprecated": None, "base64": "dGhpcyBrZXkgaGFzIG5vbnByaW50YWJsZSBjaGFyIAA=", "expected": b"this key has nonprintable char \x00" },
40+
]
4341

44-
# do the actual testing
4542
for t in testcases:
46-
self.assertEqual(
47-
crypto.is_encryption_master_key_valid( t["input"] ),
48-
t["expected"]
49-
)
43+
self.assertEqual(
44+
crypto.parse_master_key(t["deprecated"], t["base64"]),
45+
t["expected"]
46+
)
47+
48+
def test_parse_master_key_rejections(self):
49+
testcases = [
50+
{ "deprecated": "some bytes", "base64": "also some bytes", "expected": "both" },
51+
{ "deprecated": "this is 31 bytes 12345678901234", "base64": None, "expected": "32 bytes"},
52+
{ "deprecated": "this is 33 bytes 1234567890123456", "base64": None, "expected": "32 bytes"},
53+
{ "deprecated": None, "base64": "dGhpcyBpcyAzMSBieXRlcyAxMjM0NTY3ODkwMTIzNA==", "expected": "decodes to 32 bytes" },
54+
{ "deprecated": None, "base64": "dGhpcyBpcyAzMyBieXRlcyAxMjM0NTY3ODkwMTIzNDU2", "expected": "decodes to 32 bytes" },
55+
{ "deprecated": None, "base64": "dGhpcyBpcyAzMSBieXRlcyAxMjM0NTY3ODkwMTIzNA=", "expected": "valid base64" },
56+
{ "deprecated": None, "base64": "dGhpcyBpcyA!MiBieXRlcyAxMjM0NTY3OD#wMTIzNDU=", "expected": "valid base64" },
57+
]
58+
59+
for t in testcases:
60+
with self.assertRaisesRegex(ValueError, t["expected"]):
61+
crypto.parse_master_key(t["deprecated"], t["base64"])
5062

5163
def test_generate_shared_secret(self):
5264
testcases = [

pusher_tests/test_pusher_client.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from decimal import Decimal
1313
import random
1414
import nacl
15+
import base64
1516

1617
from pusher.pusher_client import PusherClient
1718
from pusher.http import GET
@@ -36,11 +37,6 @@ def test_cluster_should_be_text(self):
3637

3738
self.assertRaises(TypeError, lambda: PusherClient(app_id=u'4', key=u'key', secret=u'secret', ssl=True, cluster=4))
3839

39-
def test_encryption_master_key_should_be_text(self):
40-
PusherClient(app_id=u'4', key=u'key', secret=u'secret', ssl=True, cluster=u'eu', encryption_master_key="8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY")
41-
42-
self.assertRaises(TypeError, lambda: PusherClient(app_id=u'4', key=u'key', secret=u'secret', ssl=True, cluster=4, encryption_master_key=48762478647865374856347856888764 ))
43-
4440
def test_host_behaviour(self):
4541
conf = PusherClient(app_id=u'4', key=u'key', secret=u'secret', ssl=True)
4642
self.assertEqual(conf.host, u'api.pusherapp.com', u'default host should be correct')
@@ -143,12 +139,18 @@ def test_trigger_batch_success_case_2(self):
143139
def test_trigger_batch_with_mixed_channels_success_case(self):
144140
json_dumped = u'{"message": "something"}'
145141

146-
encryp_master_key=u'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
142+
master_key = b'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
143+
master_key_base64 = base64.b64encode(master_key)
147144
event_name_2 = "my-event-2"
148145
chan_2 = "private-encrypted-2"
149146
payload = {"message": "hello worlds"}
150147

151-
pc = PusherClient(app_id=u'4', key=u'key', secret=u'secret', encryption_master_key=encryp_master_key, ssl=True)
148+
pc = PusherClient(
149+
app_id=u'4',
150+
key=u'key',
151+
secret=u'secret',
152+
encryption_master_key_base64=master_key_base64,
153+
ssl=True)
152154
request = pc.trigger_batch.make_request(
153155
[{
154156
u'channel': u'my-chan',
@@ -162,9 +164,8 @@ def test_trigger_batch_with_mixed_channels_success_case(self):
162164
)
163165

164166
# simulate the same encryption process and check equality
165-
encryp_master_key = ensure_binary(encryp_master_key, "encryp_master_key")
166-
chan_2 = ensure_binary(chan_2,"chan_2")
167-
shared_secret = generate_shared_secret(chan_2, encryp_master_key)
167+
chan_2 = ensure_binary(chan_2, "chan_2")
168+
shared_secret = generate_shared_secret(chan_2, master_key)
168169

169170
box = nacl.secret.SecretBox(shared_secret)
170171

@@ -207,7 +208,12 @@ def test_trigger_with_private_encrypted_channel_string_fail_case_no_encryption_m
207208
def test_trigger_with_public_channel_with_encryption_master_key_specified_success(self):
208209
json_dumped = u'{"message": "something"}'
209210

210-
pc = PusherClient(app_id=u'4', key=u'key', secret=u'secret', encryption_master_key=u'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY', ssl=True)
211+
pc = PusherClient(
212+
app_id=u'4',
213+
key=u'key',
214+
secret=u'secret',
215+
encryption_master_key_base64=u'OHRXNUZRTG5pUTFzQlFGd3J3N3Q2VFZFc0paZDEweVk=',
216+
ssl=True)
211217

212218
with mock.patch('json.dumps', return_value=json_dumped) as json_dumps_mock:
213219

@@ -222,8 +228,14 @@ def test_trigger_with_public_channel_with_encryption_master_key_specified_succes
222228

223229
def test_trigger_with_private_encrypted_channel_success(self):
224230
# instantiate a new client configured with the master encryption key
225-
encryp_master_key=u'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
226-
pc = PusherClient(app_id=u'4', key=u'key', secret=u'secret', encryption_master_key=encryp_master_key, ssl=True)
231+
master_key = b'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
232+
master_key_base64 = base64.b64encode(master_key)
233+
pc = PusherClient(
234+
app_id=u'4',
235+
key=u'key',
236+
secret=u'secret',
237+
encryption_master_key_base64=master_key_base64,
238+
ssl=True)
227239

228240
# trigger a request to a private-encrypted channel and capture the request to assert equality
229241
chan = "private-encrypted-tst"
@@ -232,10 +244,8 @@ def test_trigger_with_private_encrypted_channel_success(self):
232244
request = pc.trigger.make_request(chan, event_name, payload)
233245

234246
# simulate the same encryption process and check equality
235-
236-
encryp_master_key = ensure_binary(encryp_master_key,"encryp_master_key")
237-
chan = ensure_binary(chan,"chan")
238-
shared_secret = generate_shared_secret(chan, encryp_master_key)
247+
chan = ensure_binary(chan, "chan")
248+
shared_secret = generate_shared_secret(chan, master_key)
239249

240250
box = nacl.secret.SecretBox(shared_secret)
241251

@@ -284,9 +294,12 @@ def test_trigger_disallow_invalid_channels(self):
284294
self.pusher_client.trigger.make_request([u'so/me_channel!'], u'some_event', {u'message': u'hello world'}))
285295

286296
def test_trigger_disallow_private_encrypted_channel_with_multiple_channels(self):
287-
# instantiate a new client configured with the master encryption key
288-
encryp_master_key=u'8tW5FQLniQ1sBQFwrw7t6TVEsJZd10yY'
289-
pc = PusherClient(app_id=u'4', key=u'key', secret=u'secret', encryption_master_key=encryp_master_key, ssl=True)
297+
pc = PusherClient(
298+
app_id=u'4',
299+
key=u'key',
300+
secret=u'secret',
301+
encryption_master_key_base64=u'OHRXNUZRTG5pUTFzQlFGd3J3N3Q2VFZFc0paZDEweVk=',
302+
ssl=True)
290303

291304
self.assertRaises(ValueError, lambda:
292305
self.pusher_client.trigger.make_request([u'my-chan', u'private-encrypted-pippo'], u'some_event', {u'message': u'hello world'}))

0 commit comments

Comments
 (0)