Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[![Build_Status](https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master)](https://travis-ci.org/web-push-libs/pywebpush)
[![Build
Status](https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master)](https://travis-ci.org/web-push-libs/pywebpush)
[![Requirements
Status](https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=feat%2F44)](https://requires.io/github/web-push-libs/pywebpush/requirements/?branch=master)
Status](https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=master)](https://requires.io/github/web-push-libs/pywebpush/requirements/?branch=master)

# Webpush Data encryption library for Python

Expand Down Expand Up @@ -61,8 +62,12 @@ in the `subscription_info` block.
*data* - can be any serial content (string, bit array, serialized JSON, etc), but be sure that your receiving
application is able to parse and understand it. (e.g. `data = "Mary had a little lamb."`)

*content_type* - specifies the form of Encryption to use, either `'aesgcm'` or the newer `'aes128gcm'`. NOTE that
not all User Agents can decrypt `'aes128gcm'`, so the library defaults to the older form.

*vapid_claims* - a `dict` containing the VAPID claims required for authorization (See
[py_vapid](https://github.com/web-push-libs/vapid/tree/master/python) for more details)
[py_vapid](https://github.com/web-push-libs/vapid/tree/master/python) for more details). If `aud` is not specified,
pywebpush will attempt to auto-fill from the `endpoint`.

*vapid_private_key* - Either a path to a VAPID EC2 private key PEM file, or a string containing the DER representation.
(See [py_vapid](https://github.com/web-push-libs/vapid/tree/master/python) for more details.) The `private_key` may be
Expand Down
13 changes: 9 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
|Build\_Status| |Requirements Status|
|Build Status| |Requirements Status|

Webpush Data encryption library for Python
==========================================
Expand Down Expand Up @@ -65,10 +65,15 @@ above).
etc), but be sure that your receiving application is able to parse and
understand it. (e.g. ``data = "Mary had a little lamb."``)

*content\_type* - specifies the form of Encryption to use, either
``'aesgcm'`` or the newer ``'aes128gcm'``. NOTE that not all User Agents
can decrypt ``'aes128gcm'``, so the library defaults to the older form.

*vapid\_claims* - a ``dict`` containing the VAPID claims required for
authorization (See
`py\_vapid <https://github.com/web-push-libs/vapid/tree/master/python>`__
for more details)
for more details). If ``aud`` is not specified, pywebpush will attempt
to auto-fill from the ``endpoint``.

*vapid\_private\_key* - Either a path to a VAPID EC2 private key PEM
file, or a string containing the DER representation. (See
Expand Down Expand Up @@ -170,7 +175,7 @@ Encode the ``data`` for future use. On error, returns a

encoded_data = WebPush(subscription_info).encode(data)

.. |Build\_Status| image:: https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master
.. |Build Status| image:: https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master
:target: https://travis-ci.org/web-push-libs/pywebpush
.. |Requirements Status| image:: https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=feat%2F44
.. |Requirements Status| image:: https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=master
:target: https://requires.io/github/web-push-libs/pywebpush/requirements/?branch=master
38 changes: 17 additions & 21 deletions pywebpush/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@

import six
import http_ece
import pyelliptic
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from py_vapid import Vapid


Expand Down Expand Up @@ -112,7 +113,7 @@ def __init__(self, subscription_info):
keys = self.subscription_info['keys']
for k in ['p256dh', 'auth']:
if keys.get(k) is None:
raise WebPushException("Missing keys value: %s", k)
raise WebPushException("Missing keys value: {}".format(k))
if isinstance(keys[k], six.string_types):
keys[k] = bytes(keys[k].encode('utf8'))
receiver_raw = base64.urlsafe_b64decode(
Expand Down Expand Up @@ -155,31 +156,25 @@ def encode(self, data, content_encoding="aesgcm"):
salt = os.urandom(16)
# The server key is an ephemeral ECDH key used only for this
# transaction
server_key = pyelliptic.ECC(curve="prime256v1")
# the ID is the base64 of the raw key, minus the leading "\x04"
# ID tag.
server_key_id = base64.urlsafe_b64encode(server_key.get_pubkey()[1:])
server_key = ec.generate_private_key(ec.SECP256R1, default_backend())
crypto_key = base64.urlsafe_b64encode(
server_key.public_key().public_numbers().encode_point()
).strip(b'=')

if isinstance(data, six.string_types):
data = bytes(data.encode('utf8'))

key_id = server_key_id.decode('utf8')
# http_ece requires that these both be set BEFORE encrypt or
# decrypt is called if you specify the key as "dh".
http_ece.keys[key_id] = server_key
http_ece.labels[key_id] = "P-256"

encrypted = http_ece.encrypt(
data,
salt=salt,
keyid=key_id,
keyid=crypto_key.decode(),
private_key=server_key,
dh=self.receiver_key,
authSecret=self.auth_key,
auth_secret=self.auth_key,
version=content_encoding)

reply = CaseInsensitiveDict({
'crypto_key': base64.urlsafe_b64encode(
server_key.get_pubkey()).strip(b'='),
'crypto_key': crypto_key,
'body': encrypted,
})
if salt:
Expand Down Expand Up @@ -329,7 +324,7 @@ def webpush(subscription_info,
:type subscription_info: dict
:param data: Serialized data to send
:type data: str
:param vapid_private_key: Dath to vapid private key PEM or encoded str
:param vapid_private_key: Path to vapid private key PEM or encoded str
:type vapid_private_key: str
:param vapid_claims: Dictionary of claims ('sub' required)
:type vapid_claims: dict
Expand All @@ -344,16 +339,17 @@ def webpush(subscription_info,
if vapid_claims:
if not vapid_claims.get('aud'):
url = urlparse(subscription_info.get('endpoint'))
aud = "{}://{}/".format(url.scheme, url.netloc)
aud = "{}://{}".format(url.scheme, url.netloc)
vapid_claims['aud'] = aud
if not vapid_private_key:
raise WebPushException("VAPID dict missing 'private_key'")
if os.path.isfile(vapid_private_key):
# Presume that key from file is handled correctly by
# py_vapid.
vv = Vapid(private_key_file=vapid_private_key) # pragma no cover
vv = Vapid.from_file(
private_key_file=vapid_private_key) # pragma no cover
else:
vv = Vapid(private_key=vapid_private_key)
vv = Vapid.from_raw(private_raw=vapid_private_key.encode())
vapid_headers = vv.sign(vapid_claims)
result = WebPusher(subscription_info).send(
data,
Expand All @@ -362,6 +358,6 @@ def webpush(subscription_info,
curl=curl,
)
if not curl and result.status_code > 202:
raise WebPushException("Push failed: {}:".format(
raise WebPushException("Push failed: {}: {}".format(
result, result.text))
return result
83 changes: 41 additions & 42 deletions pywebpush/tests/test_webpush.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from mock import patch, Mock
from nose.tools import eq_, ok_, assert_raises
import http_ece
import pyelliptic
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend

from pywebpush import WebPusher, WebPushException, CaseInsensitiveDict, webpush

Expand All @@ -21,17 +22,24 @@ class WebpushTestCase(unittest.TestCase):
"M5xqEwuPM7VuQcyiLDhvovthPIXx+gsQRQ=="
)

def _gen_subscription_info(self, recv_key,
def _gen_subscription_info(self,
recv_key=None,
endpoint="https://example.com/"):
if not recv_key:
recv_key = ec.generate_private_key(ec.SECP256R1, default_backend())
return {
"endpoint": endpoint,
"keys": {
'auth': base64.urlsafe_b64encode(os.urandom(16)).strip(b'='),
'p256dh': base64.urlsafe_b64encode(
recv_key.get_pubkey()).strip(b'='),
'p256dh': self._get_pubkey_str(recv_key),
}
}

def _get_pubkey_str(self, priv_key):
return base64.urlsafe_b64encode(
priv_key.public_key().public_numbers().encode_point()
).strip(b'=')

def test_init(self):
# use static values so we know what to look for in the reply
subscription_info = {
Expand Down Expand Up @@ -72,14 +80,17 @@ def test_init(self):

def test_encode(self):
for content_encoding in ["aesgcm", "aes128gcm"]:
recv_key = pyelliptic.ECC(curve="prime256v1")
recv_key = ec.generate_private_key(
ec.SECP256R1, default_backend())
subscription_info = self._gen_subscription_info(recv_key)
data = "Mary had a little lamb, with some nice mint jelly"
push = WebPusher(subscription_info)
encoded = push.encode(data, content_encoding=content_encoding)
keyid = base64.urlsafe_b64encode(recv_key.get_pubkey()[1:])
http_ece.keys[keyid] = recv_key
http_ece.labels[keyid] = 'P-256'
"""
crypto_key = base64.urlsafe_b64encode(
self._get_pubkey_str(recv_key)
).strip(b'=')
"""
# Convert these b64 strings into their raw, binary form.
raw_salt = None
if 'salt' in encoded:
Expand All @@ -94,15 +105,14 @@ def test_encode(self):
encoded['body'],
salt=raw_salt,
dh=raw_dh,
keyid=keyid,
authSecret=raw_auth,
private_key=recv_key,
auth_secret=raw_auth,
version=content_encoding
)
eq_(decoded.decode('utf8'), data)

def test_bad_content_encoding(self):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb, with some nice mint jelly"
push = WebPusher(subscription_info)
self.assertRaises(WebPushException,
Expand All @@ -112,8 +122,7 @@ def test_bad_content_encoding(self):

@patch("requests.post")
def test_send(self, mock_post):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
data = "Mary had a little lamb"
Expand All @@ -131,9 +140,7 @@ def test_send(self, mock_post):
def test_send_vapid(self, mock_post):
mock_post.return_value = Mock()
mock_post.return_value.status_code = 200
recv_key = pyelliptic.ECC(curve="prime256v1")

subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
webpush(
subscription_info=subscription_info,
Expand Down Expand Up @@ -165,43 +172,40 @@ def repad(str):
def test_send_bad_vapid_no_key(self, mock_post):
mock_post.return_value = Mock()
mock_post.return_value.status_code = 200
recv_key = pyelliptic.ECC(curve="prime256v1")

subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
assert_raises(WebPushException,
webpush,
subscription_info=subscription_info,
data=data,
vapid_claims={
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
}
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
}
)

@patch("requests.post")
def test_send_bad_vapid_bad_return(self, mock_post):
mock_post.return_value = Mock()
mock_post.return_value.status_code = 410
recv_key = pyelliptic.ECC(curve="prime256v1")

subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
assert_raises(WebPushException,
webpush,
subscription_info=subscription_info,
data=data,
vapid_claims={
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
},
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
},
vapid_private_key=self.vapid_key
)

@patch("requests.post")
def test_send_empty(self, mock_post):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
WebPusher(subscription_info).send('', headers)
Expand All @@ -214,16 +218,14 @@ def test_send_empty(self, mock_post):
ok_('pre-existing' in ckey)

def test_encode_empty(self):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
encoded = WebPusher(subscription_info).encode('', headers)
eq_(encoded, None)

def test_encode_no_crypto(self):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
del(subscription_info['keys'])
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
Expand All @@ -236,8 +238,7 @@ def test_encode_no_crypto(self):

@patch("requests.post")
def test_send_no_headers(self, mock_post):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
data = "Mary had a little lamb"
WebPusher(subscription_info).send(data)
eq_(subscription_info.get('endpoint'), mock_post.call_args[0][0])
Expand All @@ -248,15 +249,14 @@ def test_send_no_headers(self, mock_post):

@patch("pywebpush.open")
def test_as_curl(self, opener):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(recv_key)
subscription_info = self._gen_subscription_info()
result = webpush(
subscription_info,
data="Mary had a little lamb",
vapid_claims={
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
},
"aud": "https://example.com",
"sub": "mailto:ops@example.com"
},
vapid_private_key=self.vapid_key,
curl=True
)
Expand All @@ -281,9 +281,8 @@ def test_ci_dict(self):

@patch("requests.post")
def test_gcm(self, mock_post):
recv_key = pyelliptic.ECC(curve="prime256v1")
subscription_info = self._gen_subscription_info(
recv_key,
None,
endpoint="https://android.googleapis.com/gcm/send/regid123")
headers = {"Crypto-Key": "pre-existing",
"Authentication": "bearer vapid"}
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
http-ece==0.7.1
python-jose==1.3.2
cryptography==1.8.1
http-ece==1.0.1
requests==2.13.0
py-vapid==0.8.1
py-vapid==1.2.1
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from setuptools import find_packages, setup

__version__ = "0.8.0"
__version__ = "1.0.0"


def read_from(file):
Expand Down
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-r requirements.txt
nose>=1.3.7
coverage>=4.3.4
coverage>=4.4
mock==2.0.0
flake8