Skip to content

Commit 812b8c7

Browse files
committed
feat: update to use Cryptography library
* uses lastest ece(1.7.2) and vapid libraries (1.2.1) * Will attempt to autofill vapid `aud` from the endpoint if VAPID requested * Allows for the older `'aesgcm'` and newer, albeit not as widely supported `'aes128gcm'` encryption content types. * Includes fixes provided by https://github.com/Flimm NOTE: Currently BLOCKED due to web-push-libs/encrypted-content-encoding#36 closes: #49, #48, #42
1 parent 5f173cb commit 812b8c7

File tree

7 files changed

+80
-75
lines changed

7 files changed

+80
-75
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
[![Build_Status](https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master)](https://travis-ci.org/web-push-libs/pywebpush)
1+
[![Build
2+
Status](https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master)](https://travis-ci.org/web-push-libs/pywebpush)
23
[![Requirements
3-
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)
4+
Status](https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=master)](https://requires.io/github/web-push-libs/pywebpush/requirements/?branch=master)
45

56
# Webpush Data encryption library for Python
67

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

65+
*content_type* - specifies the form of Encryption to use, either `'aesgcm'` or the newer `'aes128gcm'`. NOTE that
66+
not all User Agents can decrypt `'aes128gcm'`, so the library defaults to the older form.
67+
6468
*vapid_claims* - a `dict` containing the VAPID claims required for authorization (See
65-
[py_vapid](https://github.com/web-push-libs/vapid/tree/master/python) for more details)
69+
[py_vapid](https://github.com/web-push-libs/vapid/tree/master/python) for more details). If `aud` is not specified,
70+
pywebpush will attempt to auto-fill from the `endpoint`.
6671

6772
*vapid_private_key* - Either a path to a VAPID EC2 private key PEM file, or a string containing the DER representation.
6873
(See [py_vapid](https://github.com/web-push-libs/vapid/tree/master/python) for more details.) The `private_key` may be

README.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
|Build\_Status| |Requirements Status|
1+
|Build Status| |Requirements Status|
22

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

68+
*content\_type* - specifies the form of Encryption to use, either
69+
``'aesgcm'`` or the newer ``'aes128gcm'``. NOTE that not all User Agents
70+
can decrypt ``'aes128gcm'``, so the library defaults to the older form.
71+
6872
*vapid\_claims* - a ``dict`` containing the VAPID claims required for
6973
authorization (See
7074
`py\_vapid <https://github.com/web-push-libs/vapid/tree/master/python>`__
71-
for more details)
75+
for more details). If ``aud`` is not specified, pywebpush will attempt
76+
to auto-fill from the ``endpoint``.
7277

7378
*vapid\_private\_key* - Either a path to a VAPID EC2 private key PEM
7479
file, or a string containing the DER representation. (See
@@ -170,7 +175,7 @@ Encode the ``data`` for future use. On error, returns a
170175
171176
encoded_data = WebPush(subscription_info).encode(data)
172177
173-
.. |Build\_Status| image:: https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master
178+
.. |Build Status| image:: https://travis-ci.org/web-push-libs/pywebpush.svg?branch=master
174179
:target: https://travis-ci.org/web-push-libs/pywebpush
175-
.. |Requirements Status| image:: https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=feat%2F44
180+
.. |Requirements Status| image:: https://requires.io/github/web-push-libs/pywebpush/requirements.svg?branch=master
176181
:target: https://requires.io/github/web-push-libs/pywebpush/requirements/?branch=master

pywebpush/__init__.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414
import six
1515
import http_ece
16-
import pyelliptic
1716
import requests
17+
from cryptography.hazmat.backends import default_backend
18+
from cryptography.hazmat.primitives.asymmetric import ec
1819
from py_vapid import Vapid
1920

2021

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

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

166-
key_id = server_key_id.decode('utf8')
167-
# http_ece requires that these both be set BEFORE encrypt or
168-
# decrypt is called if you specify the key as "dh".
169-
http_ece.keys[key_id] = server_key
170-
http_ece.labels[key_id] = "P-256"
171-
172167
encrypted = http_ece.encrypt(
173168
data,
174169
salt=salt,
175-
keyid=key_id,
170+
keyid=crypto_key.decode(),
171+
private_key=server_key,
176172
dh=self.receiver_key,
177-
authSecret=self.auth_key,
173+
auth_secret=self.auth_key,
178174
version=content_encoding)
179175

180176
reply = CaseInsensitiveDict({
181-
'crypto_key': base64.urlsafe_b64encode(
182-
server_key.get_pubkey()).strip(b'='),
177+
'crypto_key': crypto_key,
183178
'body': encrypted,
184179
})
185180
if salt:
@@ -329,7 +324,7 @@ def webpush(subscription_info,
329324
:type subscription_info: dict
330325
:param data: Serialized data to send
331326
:type data: str
332-
:param vapid_private_key: Dath to vapid private key PEM or encoded str
327+
:param vapid_private_key: Path to vapid private key PEM or encoded str
333328
:type vapid_private_key: str
334329
:param vapid_claims: Dictionary of claims ('sub' required)
335330
:type vapid_claims: dict
@@ -344,16 +339,17 @@ def webpush(subscription_info,
344339
if vapid_claims:
345340
if not vapid_claims.get('aud'):
346341
url = urlparse(subscription_info.get('endpoint'))
347-
aud = "{}://{}/".format(url.scheme, url.netloc)
342+
aud = "{}://{}".format(url.scheme, url.netloc)
348343
vapid_claims['aud'] = aud
349344
if not vapid_private_key:
350345
raise WebPushException("VAPID dict missing 'private_key'")
351346
if os.path.isfile(vapid_private_key):
352347
# Presume that key from file is handled correctly by
353348
# py_vapid.
354-
vv = Vapid(private_key_file=vapid_private_key) # pragma no cover
349+
vv = Vapid.from_file(
350+
private_key_file=vapid_private_key) # pragma no cover
355351
else:
356-
vv = Vapid(private_key=vapid_private_key)
352+
vv = Vapid.from_raw(private_raw=vapid_private_key.encode())
357353
vapid_headers = vv.sign(vapid_claims)
358354
result = WebPusher(subscription_info).send(
359355
data,
@@ -362,6 +358,6 @@ def webpush(subscription_info,
362358
curl=curl,
363359
)
364360
if not curl and result.status_code > 202:
365-
raise WebPushException("Push failed: {}:".format(
361+
raise WebPushException("Push failed: {}: {}".format(
366362
result, result.text))
367363
return result

pywebpush/tests/test_webpush.py

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from mock import patch, Mock
77
from nose.tools import eq_, ok_, assert_raises
88
import http_ece
9-
import pyelliptic
9+
from cryptography.hazmat.primitives.asymmetric import ec
10+
from cryptography.hazmat.backends import default_backend
1011

1112
from pywebpush import WebPusher, WebPushException, CaseInsensitiveDict, webpush
1213

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

24-
def _gen_subscription_info(self, recv_key,
25+
def _gen_subscription_info(self,
26+
recv_key=None,
2527
endpoint="https://example.com/"):
28+
if not recv_key:
29+
recv_key = ec.generate_private_key(ec.SECP256R1, default_backend())
2630
return {
2731
"endpoint": endpoint,
2832
"keys": {
2933
'auth': base64.urlsafe_b64encode(os.urandom(16)).strip(b'='),
30-
'p256dh': base64.urlsafe_b64encode(
31-
recv_key.get_pubkey()).strip(b'='),
34+
'p256dh': self._get_pubkey_str(recv_key),
3235
}
3336
}
3437

38+
def _get_pubkey_str(self, priv_key):
39+
return base64.urlsafe_b64encode(
40+
priv_key.public_key().public_numbers().encode_point()
41+
).strip(b'=')
42+
3543
def test_init(self):
3644
# use static values so we know what to look for in the reply
3745
subscription_info = {
@@ -72,14 +80,17 @@ def test_init(self):
7280

7381
def test_encode(self):
7482
for content_encoding in ["aesgcm", "aes128gcm"]:
75-
recv_key = pyelliptic.ECC(curve="prime256v1")
83+
recv_key = ec.generate_private_key(
84+
ec.SECP256R1, default_backend())
7685
subscription_info = self._gen_subscription_info(recv_key)
7786
data = "Mary had a little lamb, with some nice mint jelly"
7887
push = WebPusher(subscription_info)
7988
encoded = push.encode(data, content_encoding=content_encoding)
80-
keyid = base64.urlsafe_b64encode(recv_key.get_pubkey()[1:])
81-
http_ece.keys[keyid] = recv_key
82-
http_ece.labels[keyid] = 'P-256'
89+
"""
90+
crypto_key = base64.urlsafe_b64encode(
91+
self._get_pubkey_str(recv_key)
92+
).strip(b'=')
93+
"""
8394
# Convert these b64 strings into their raw, binary form.
8495
raw_salt = None
8596
if 'salt' in encoded:
@@ -94,15 +105,14 @@ def test_encode(self):
94105
encoded['body'],
95106
salt=raw_salt,
96107
dh=raw_dh,
97-
keyid=keyid,
98-
authSecret=raw_auth,
108+
private_key=recv_key,
109+
auth_secret=raw_auth,
99110
version=content_encoding
100111
)
101112
eq_(decoded.decode('utf8'), data)
102113

103114
def test_bad_content_encoding(self):
104-
recv_key = pyelliptic.ECC(curve="prime256v1")
105-
subscription_info = self._gen_subscription_info(recv_key)
115+
subscription_info = self._gen_subscription_info()
106116
data = "Mary had a little lamb, with some nice mint jelly"
107117
push = WebPusher(subscription_info)
108118
self.assertRaises(WebPushException,
@@ -112,8 +122,7 @@ def test_bad_content_encoding(self):
112122

113123
@patch("requests.post")
114124
def test_send(self, mock_post):
115-
recv_key = pyelliptic.ECC(curve="prime256v1")
116-
subscription_info = self._gen_subscription_info(recv_key)
125+
subscription_info = self._gen_subscription_info()
117126
headers = {"Crypto-Key": "pre-existing",
118127
"Authentication": "bearer vapid"}
119128
data = "Mary had a little lamb"
@@ -131,9 +140,7 @@ def test_send(self, mock_post):
131140
def test_send_vapid(self, mock_post):
132141
mock_post.return_value = Mock()
133142
mock_post.return_value.status_code = 200
134-
recv_key = pyelliptic.ECC(curve="prime256v1")
135-
136-
subscription_info = self._gen_subscription_info(recv_key)
143+
subscription_info = self._gen_subscription_info()
137144
data = "Mary had a little lamb"
138145
webpush(
139146
subscription_info=subscription_info,
@@ -165,43 +172,40 @@ def repad(str):
165172
def test_send_bad_vapid_no_key(self, mock_post):
166173
mock_post.return_value = Mock()
167174
mock_post.return_value.status_code = 200
168-
recv_key = pyelliptic.ECC(curve="prime256v1")
169175

170-
subscription_info = self._gen_subscription_info(recv_key)
176+
subscription_info = self._gen_subscription_info()
171177
data = "Mary had a little lamb"
172178
assert_raises(WebPushException,
173179
webpush,
174180
subscription_info=subscription_info,
175181
data=data,
176182
vapid_claims={
177-
"aud": "https://example.com",
178-
"sub": "mailto:[email protected]"
179-
}
183+
"aud": "https://example.com",
184+
"sub": "mailto:[email protected]"
185+
}
180186
)
181187

182188
@patch("requests.post")
183189
def test_send_bad_vapid_bad_return(self, mock_post):
184190
mock_post.return_value = Mock()
185191
mock_post.return_value.status_code = 410
186-
recv_key = pyelliptic.ECC(curve="prime256v1")
187192

188-
subscription_info = self._gen_subscription_info(recv_key)
193+
subscription_info = self._gen_subscription_info()
189194
data = "Mary had a little lamb"
190195
assert_raises(WebPushException,
191196
webpush,
192197
subscription_info=subscription_info,
193198
data=data,
194199
vapid_claims={
195-
"aud": "https://example.com",
196-
"sub": "mailto:[email protected]"
197-
},
200+
"aud": "https://example.com",
201+
"sub": "mailto:[email protected]"
202+
},
198203
vapid_private_key=self.vapid_key
199204
)
200205

201206
@patch("requests.post")
202207
def test_send_empty(self, mock_post):
203-
recv_key = pyelliptic.ECC(curve="prime256v1")
204-
subscription_info = self._gen_subscription_info(recv_key)
208+
subscription_info = self._gen_subscription_info()
205209
headers = {"Crypto-Key": "pre-existing",
206210
"Authentication": "bearer vapid"}
207211
WebPusher(subscription_info).send('', headers)
@@ -214,16 +218,14 @@ def test_send_empty(self, mock_post):
214218
ok_('pre-existing' in ckey)
215219

216220
def test_encode_empty(self):
217-
recv_key = pyelliptic.ECC(curve="prime256v1")
218-
subscription_info = self._gen_subscription_info(recv_key)
221+
subscription_info = self._gen_subscription_info()
219222
headers = {"Crypto-Key": "pre-existing",
220223
"Authentication": "bearer vapid"}
221224
encoded = WebPusher(subscription_info).encode('', headers)
222225
eq_(encoded, None)
223226

224227
def test_encode_no_crypto(self):
225-
recv_key = pyelliptic.ECC(curve="prime256v1")
226-
subscription_info = self._gen_subscription_info(recv_key)
228+
subscription_info = self._gen_subscription_info()
227229
del(subscription_info['keys'])
228230
headers = {"Crypto-Key": "pre-existing",
229231
"Authentication": "bearer vapid"}
@@ -236,8 +238,7 @@ def test_encode_no_crypto(self):
236238

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

249250
@patch("pywebpush.open")
250251
def test_as_curl(self, opener):
251-
recv_key = pyelliptic.ECC(curve="prime256v1")
252-
subscription_info = self._gen_subscription_info(recv_key)
252+
subscription_info = self._gen_subscription_info()
253253
result = webpush(
254254
subscription_info,
255255
data="Mary had a little lamb",
256256
vapid_claims={
257-
"aud": "https://example.com",
258-
"sub": "mailto:[email protected]"
259-
},
257+
"aud": "https://example.com",
258+
"sub": "mailto:[email protected]"
259+
},
260260
vapid_private_key=self.vapid_key,
261261
curl=True
262262
)
@@ -281,9 +281,8 @@ def test_ci_dict(self):
281281

282282
@patch("requests.post")
283283
def test_gcm(self, mock_post):
284-
recv_key = pyelliptic.ECC(curve="prime256v1")
285284
subscription_info = self._gen_subscription_info(
286-
recv_key,
285+
None,
287286
endpoint="https://android.googleapis.com/gcm/send/regid123")
288287
headers = {"Crypto-Key": "pre-existing",
289288
"Authentication": "bearer vapid"}

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
http-ece==0.7.1
2-
python-jose==1.3.2
1+
cryptography==1.8.1
2+
http-ece==1.0.1
33
requests==2.13.0
4-
py-vapid==0.8.1
4+
py-vapid==1.2.1

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from setuptools import find_packages, setup
55

6-
__version__ = "0.8.0"
6+
__version__ = "1.0.0"
77

88

99
def read_from(file):

test-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-r requirements.txt
22
nose>=1.3.7
3-
coverage>=4.3.4
3+
coverage>=4.4
44
mock==2.0.0
55
flake8

0 commit comments

Comments
 (0)