Skip to content

Commit

Permalink
Merge pull request #158 from pusher/base64-master-key
Browse files Browse the repository at this point in the history
Base64 master key
  • Loading branch information
mdpye authored Apr 1, 2020
2 parents da70e27 + dc88172 commit 75fd1c5
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 263 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### next (major)

* [ADDED] option `encryption_master_key_base64`
* [DEPRECATED] option `encryption_master_key`

* [REMOVED] old support for Push Notifications, see https://github.com/pusher/push-notifications-python

### 2.1.4 2019-08-09

* [FIXED] TypeError in AuthenticationClient when using encrypted channels
Expand Down
60 changes: 40 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ pusher_client = pusher.Pusher(app_id, key, secret, cluster=u'cluster')
|host `String` | **Default:`None`** <br> The host to connect to |
|port `int` | **Default:`None`** <br>Which port to connect to |
|ssl `bool` | **Default:`True`** <br> Use HTTPS |
|encryption_master_key `String` | **Default:`None`** <br> The encryption master key for End-to-end Encryption |
|~~encryption_master_key~~ `String` | **Default:`None`** <br> *Deprecated*, see `encryption_master_key_base64` |
|encryption_master_key_base64 `String` | **Default:`None`** <br> The encryption master key for End-to-end Encryption |
|backend `Object` | an object that responds to the `send_request(request)` method. If none is provided, a `pusher.requests.RequestsBackend` instance is created. |
|json_encoder `Object` | **Default: `None`**<br> Custom JSON encoder. |
|json_decoder `Object` | **Default: `None`**<br> Custom JSON decoder.
Expand Down Expand Up @@ -287,32 +288,51 @@ auth = pusher_client.authenticate(

## 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:
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).
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.
2. Next, generate a 32 byte master encryption key, base64 encode it and store
it securely.

```python
This is secret and you should never share this with anyone. Not even Pusher.

import pusher
To generate a suitable key from a secure random source, you could use:

pusher_client = pusher.Pusher(
app_id='yourappid',
key='yourkey',
secret='yoursecret',
encryption_master_key='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
cluster='yourclustername',
ssl=True
)
```bash
openssl rand -base64 32
```

pusher_client.trigger('private-encrypted-my-channel', 'my-event', {
'message': 'hello world'
})
```
3. Channels where you wish to use end to end encryption must be prefixed with `private-encrypted-`.
3. Pass your master key to the SDK constructor

```python
import pusher

pusher_client = pusher.Pusher(
app_id='yourappid',
key='yourkey',
secret='yoursecret',
encryption_master_key_base64='<output from command above>',
cluster='yourclustername',
ssl=True
)

pusher_client.trigger('private-encrypted-my-channel', 'my-event', {
'message': 'hello world'
})
```

4. Channels where you wish to use end to end encryption must 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/ and seeing the scrambled ciphertext.
5. 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.

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

Expand Down
45 changes: 30 additions & 15 deletions pusher/authentication_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,37 @@

class AuthenticationClient(Client):
def __init__(
self, app_id, key, secret, ssl=True, host=None, port=None,
timeout=5, cluster=None, encryption_master_key=None, json_encoder=None, json_decoder=None,
backend=None, **backend_options):
self,
app_id,
key,
secret,
ssl=True,
host=None,
port=None,
timeout=5,
cluster=None,
encryption_master_key=None,
encryption_master_key_base64=None,
json_encoder=None,
json_decoder=None,
backend=None,
**backend_options):

super(AuthenticationClient, self).__init__(
app_id, key, secret, ssl, host, port, timeout, cluster, encryption_master_key,
json_encoder, json_decoder, backend, **backend_options)

if host:
self._host = ensure_text(host, "host")

elif cluster:
self._host = (
six.text_type("api-%s.pusher.com") %
ensure_text(cluster, "cluster"))
else:
self._host = six.text_type("api.pusherapp.com")
app_id,
key,
secret,
ssl,
host,
port,
timeout,
cluster,
encryption_master_key,
encryption_master_key_base64,
json_encoder,
json_decoder,
backend,
**backend_options)


def authenticate(self, channel, socket_id, custom_data=None):
Expand Down
36 changes: 28 additions & 8 deletions pusher/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,27 @@
import six

from pusher.util import ensure_text, ensure_binary, app_id_re
from pusher.crypto import parse_master_key


class Client(object):
def __init__(
self, app_id, key, secret, ssl=True, host=None, port=None,
timeout=5, cluster=None, encryption_master_key=None, json_encoder=None, json_decoder=None,
backend=None, **backend_options):
self,
app_id,
key,
secret,
ssl=True,
host=None,
port=None,
timeout=5,
cluster=None,
encryption_master_key=None,
encryption_master_key_base64=None,
json_encoder=None,
json_decoder=None,
backend=None,
**backend_options):

if backend is None:
from .requests import RequestsBackend
backend = RequestsBackend
Expand All @@ -32,6 +46,15 @@ def __init__(

self._ssl = ssl

if host:
self._host = ensure_text(host, "host")
elif cluster:
self._host = (
six.text_type("api-%s.pusher.com") %
ensure_text(cluster, "cluster"))
else:
self._host = six.text_type("api.pusherapp.com")

if port and not isinstance(port, six.integer_types):
raise TypeError("port should be an integer")

Expand All @@ -44,14 +67,11 @@ def __init__(
self._json_encoder = json_encoder
self._json_decoder = json_decoder


if encryption_master_key is not None:
encryption_master_key = ensure_binary(encryption_master_key, "encryption_master_key")

self._encryption_master_key = encryption_master_key
self._encryption_master_key = parse_master_key(encryption_master_key, encryption_master_key_base64)

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


@property
def app_id(self):
return self._app_id
Expand Down
48 changes: 35 additions & 13 deletions pusher/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
import hashlib
import nacl
import base64
import binascii
import warnings

from pusher.util import (
ensure_text,
ensure_binary,
data_to_string)
data_to_string,
is_base64)

import nacl.secret
import nacl.utils
Expand All @@ -29,27 +32,46 @@ def is_encrypted_channel(channel):
return True
return False

def is_encryption_master_key_valid(encryption_master_key):
def parse_master_key(encryption_master_key, encryption_master_key_base64):
"""
is_encryption_master_key_valid() checks if the provided encryption_master_key is valid by checking its length
the key is assumed to be a six.binary_type (python2 str or python3 bytes)
parse_master_key validates, parses and returns the bytes of the encryption master key
from the constructor arguments.
At present there is a deprecated "raw" key and a suggested base64 encoding.
"""
if encryption_master_key is not None and len(encryption_master_key) == 32:
return True

return False
if encryption_master_key is not None and encryption_master_key_base64 is not None:
raise ValueError("Do not provide both encryption_master_key and encryption_master_key_base64. " +
"encryption_master_key is deprecated, provide only encryption_master_key_base64")

if encryption_master_key is not None:
warnings.warn("`encryption_master_key` is deprecated, please use `encryption_master_key_base64`")
if len(encryption_master_key) == 32:
return ensure_binary(encryption_master_key, "encryption_master_key")
else:
raise ValueError("encryption_master_key must be 32 bytes long")

if encryption_master_key_base64 is not None:
if is_base64(encryption_master_key_base64):
decoded = base64.b64decode(encryption_master_key_base64)

if len(decoded) == 32:
return decoded
else:
raise ValueError("encryption_master_key_base64 must be a base64 string which decodes to 32 bytes")
else:
raise ValueError("encryption_master_key_base64 must be valid base64")

return None

def generate_shared_secret(channel, encryption_master_key):
"""
generate_shared_secret() takes a six.binary_type (python2 str or python3 bytes) channel name and encryption_master_key
and returns the sha256 hash in six.binary_type format
"""
if is_encryption_master_key_valid(encryption_master_key):
# the key has to be 32 bytes long
hashable = channel + encryption_master_key
return hashlib.sha256(hashable).digest()
if encryption_master_key is None:
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")

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

def encrypt(channel, data, encryption_master_key, nonce=None):
"""
Expand Down
59 changes: 0 additions & 59 deletions pusher/notification_client.py

This file was deleted.

Loading

0 comments on commit 75fd1c5

Please sign in to comment.