Skip to content

Commit 75fd1c5

Browse files
authored
Merge pull request #158 from pusher/base64-master-key
Base64 master key
2 parents da70e27 + dc88172 commit 75fd1c5

13 files changed

+324
-263
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
### next (major)
2+
3+
* [ADDED] option `encryption_master_key_base64`
4+
* [DEPRECATED] option `encryption_master_key`
5+
6+
* [REMOVED] old support for Push Notifications, see https://github.com/pusher/push-notifications-python
7+
18
### 2.1.4 2019-08-09
29

310
* [FIXED] TypeError in AuthenticationClient when using encrypted channels

README.md

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ pusher_client = pusher.Pusher(app_id, key, secret, cluster=u'cluster')
8686
|host `String` | **Default:`None`** <br> The host to connect to |
8787
|port `int` | **Default:`None`** <br>Which port to connect to |
8888
|ssl `bool` | **Default:`True`** <br> Use HTTPS |
89-
|encryption_master_key `String` | **Default:`None`** <br> The encryption master key for End-to-end Encryption |
89+
|~~encryption_master_key~~ `String` | **Default:`None`** <br> *Deprecated*, see `encryption_master_key_base64` |
90+
|encryption_master_key_base64 `String` | **Default:`None`** <br> The encryption master key for End-to-end Encryption |
9091
|backend `Object` | an object that responds to the `send_request(request)` method. If none is provided, a `pusher.requests.RequestsBackend` instance is created. |
9192
|json_encoder `Object` | **Default: `None`**<br> Custom JSON encoder. |
9293
|json_decoder `Object` | **Default: `None`**<br> Custom JSON decoder.
@@ -287,32 +288,51 @@ auth = pusher_client.authenticate(
287288

288289
## End to End Encryption (Beta)
289290

290-
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:
291+
This library supports end to end encryption of your private channels. This
292+
means that only you and your connected clients will be able to read your
293+
messages. Pusher cannot decrypt them. You can enable this feature by following
294+
these steps:
291295

292-
1. You should first set up Private channels. This involves [creating an authentication endpoint on your server](https://pusher.com/docs/authenticating_users).
296+
1. You should first set up Private channels. This involves [creating an
297+
authentication endpoint on your
298+
server](https://pusher.com/docs/authenticating_users).
293299

294-
2. Next, Specify your 32 character `encryption_master_key`. This is secret and you should never share this with anyone. Not even Pusher.
300+
2. Next, generate a 32 byte master encryption key, base64 encode it and store
301+
it securely.
295302

296-
```python
303+
This is secret and you should never share this with anyone. Not even Pusher.
297304

298-
import pusher
305+
To generate a suitable key from a secure random source, you could use:
299306

300-
pusher_client = pusher.Pusher(
301-
app_id='yourappid',
302-
key='yourkey',
303-
secret='yoursecret',
304-
encryption_master_key='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
305-
cluster='yourclustername',
306-
ssl=True
307-
)
307+
```bash
308+
openssl rand -base64 32
309+
```
308310

309-
pusher_client.trigger('private-encrypted-my-channel', 'my-event', {
310-
'message': 'hello world'
311-
})
312-
```
313-
3. Channels where you wish to use end to end encryption must be prefixed with `private-encrypted-`.
311+
3. Pass your master key to the SDK constructor
312+
313+
```python
314+
import pusher
315+
316+
pusher_client = pusher.Pusher(
317+
app_id='yourappid',
318+
key='yourkey',
319+
secret='yoursecret',
320+
encryption_master_key_base64='<output from command above>',
321+
cluster='yourclustername',
322+
ssl=True
323+
)
324+
325+
pusher_client.trigger('private-encrypted-my-channel', 'my-event', {
326+
'message': 'hello world'
327+
})
328+
```
329+
330+
4. Channels where you wish to use end to end encryption must be prefixed with
331+
`private-encrypted-`.
314332

315-
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/ and seeing the scrambled ciphertext.
333+
5. Subscribe to these channels in your client, and you're done! You can verify
334+
it is working by checking out the debug console on the
335+
https://dashboard.pusher.com/ and seeing the scrambled ciphertext.
316336

317337
**Important note: This will not encrypt messages on channels that are not prefixed by private-encrypted-.**
318338

pusher/authentication_client.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,37 @@
3131

3232
class AuthenticationClient(Client):
3333
def __init__(
34-
self, app_id, key, secret, ssl=True, host=None, port=None,
35-
timeout=5, cluster=None, encryption_master_key=None, json_encoder=None, json_decoder=None,
36-
backend=None, **backend_options):
34+
self,
35+
app_id,
36+
key,
37+
secret,
38+
ssl=True,
39+
host=None,
40+
port=None,
41+
timeout=5,
42+
cluster=None,
43+
encryption_master_key=None,
44+
encryption_master_key_base64=None,
45+
json_encoder=None,
46+
json_decoder=None,
47+
backend=None,
48+
**backend_options):
49+
3750
super(AuthenticationClient, self).__init__(
38-
app_id, key, secret, ssl, host, port, timeout, cluster, encryption_master_key,
39-
json_encoder, json_decoder, backend, **backend_options)
40-
41-
if host:
42-
self._host = ensure_text(host, "host")
43-
44-
elif cluster:
45-
self._host = (
46-
six.text_type("api-%s.pusher.com") %
47-
ensure_text(cluster, "cluster"))
48-
else:
49-
self._host = six.text_type("api.pusherapp.com")
51+
app_id,
52+
key,
53+
secret,
54+
ssl,
55+
host,
56+
port,
57+
timeout,
58+
cluster,
59+
encryption_master_key,
60+
encryption_master_key_base64,
61+
json_encoder,
62+
json_decoder,
63+
backend,
64+
**backend_options)
5065

5166

5267
def authenticate(self, channel, socket_id, custom_data=None):

pusher/client.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,27 @@
99
import six
1010

1111
from pusher.util import ensure_text, ensure_binary, app_id_re
12+
from pusher.crypto import parse_master_key
1213

1314

1415
class Client(object):
1516
def __init__(
16-
self, app_id, key, secret, ssl=True, host=None, port=None,
17-
timeout=5, cluster=None, encryption_master_key=None, json_encoder=None, json_decoder=None,
18-
backend=None, **backend_options):
17+
self,
18+
app_id,
19+
key,
20+
secret,
21+
ssl=True,
22+
host=None,
23+
port=None,
24+
timeout=5,
25+
cluster=None,
26+
encryption_master_key=None,
27+
encryption_master_key_base64=None,
28+
json_encoder=None,
29+
json_decoder=None,
30+
backend=None,
31+
**backend_options):
32+
1933
if backend is None:
2034
from .requests import RequestsBackend
2135
backend = RequestsBackend
@@ -32,6 +46,15 @@ def __init__(
3246

3347
self._ssl = ssl
3448

49+
if host:
50+
self._host = ensure_text(host, "host")
51+
elif cluster:
52+
self._host = (
53+
six.text_type("api-%s.pusher.com") %
54+
ensure_text(cluster, "cluster"))
55+
else:
56+
self._host = six.text_type("api.pusherapp.com")
57+
3558
if port and not isinstance(port, six.integer_types):
3659
raise TypeError("port should be an integer")
3760

@@ -44,14 +67,11 @@ def __init__(
4467
self._json_encoder = json_encoder
4568
self._json_decoder = json_decoder
4669

47-
48-
if encryption_master_key is not None:
49-
encryption_master_key = ensure_binary(encryption_master_key, "encryption_master_key")
50-
51-
self._encryption_master_key = encryption_master_key
70+
self._encryption_master_key = parse_master_key(encryption_master_key, encryption_master_key_base64)
5271

5372
self.http = backend(self, **backend_options)
5473

74+
5575
@property
5676
def app_id(self):
5777
return self._app_id

pusher/crypto.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
import hashlib
1010
import nacl
1111
import base64
12+
import binascii
13+
import warnings
1214

1315
from pusher.util import (
1416
ensure_text,
1517
ensure_binary,
16-
data_to_string)
18+
data_to_string,
19+
is_base64)
1720

1821
import nacl.secret
1922
import nacl.utils
@@ -29,27 +32,46 @@ def is_encrypted_channel(channel):
2932
return True
3033
return False
3134

32-
def is_encryption_master_key_valid(encryption_master_key):
35+
def parse_master_key(encryption_master_key, encryption_master_key_base64):
3336
"""
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)
37+
parse_master_key validates, parses and returns the bytes of the encryption master key
38+
from the constructor arguments.
39+
At present there is a deprecated "raw" key and a suggested base64 encoding.
3640
"""
37-
if encryption_master_key is not None and len(encryption_master_key) == 32:
38-
return True
39-
40-
return False
41+
if encryption_master_key is not None and encryption_master_key_base64 is not None:
42+
raise ValueError("Do not provide both encryption_master_key and encryption_master_key_base64. " +
43+
"encryption_master_key is deprecated, provide only encryption_master_key_base64")
44+
45+
if encryption_master_key is not None:
46+
warnings.warn("`encryption_master_key` is deprecated, please use `encryption_master_key_base64`")
47+
if len(encryption_master_key) == 32:
48+
return ensure_binary(encryption_master_key, "encryption_master_key")
49+
else:
50+
raise ValueError("encryption_master_key must be 32 bytes long")
51+
52+
if encryption_master_key_base64 is not None:
53+
if is_base64(encryption_master_key_base64):
54+
decoded = base64.b64decode(encryption_master_key_base64)
55+
56+
if len(decoded) == 32:
57+
return decoded
58+
else:
59+
raise ValueError("encryption_master_key_base64 must be a base64 string which decodes to 32 bytes")
60+
else:
61+
raise ValueError("encryption_master_key_base64 must be valid base64")
62+
63+
return None
4164

4265
def generate_shared_secret(channel, encryption_master_key):
4366
"""
4467
generate_shared_secret() takes a six.binary_type (python2 str or python3 bytes) channel name and encryption_master_key
4568
and returns the sha256 hash in six.binary_type format
4669
"""
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()
70+
if encryption_master_key is None:
71+
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")
5172

52-
raise ValueError("Provided encryption_master_key is not 32 char long")
73+
hashable = channel + encryption_master_key
74+
return hashlib.sha256(hashable).digest()
5375

5476
def encrypt(channel, data, encryption_master_key, nonce=None):
5577
"""

pusher/notification_client.py

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)