From 5c134746ac299d8dc25e4a97df9a4f1bc6228b23 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Mon, 25 Apr 2016 14:54:57 -0700 Subject: [PATCH] WIP: Initial version Needs tests, docs, holy water --- .gitignore | 4 ++ CHANGELOG.md | 3 ++ README.md | 17 +++++++ requirements.txt | 2 + setup.py | 37 ++++++++++++++ webpush/__init__.py | 0 webpush/publish.py | 117 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 180 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 webpush/__init__.py create mode 100644 webpush/publish.py diff --git a/.gitignore b/.gitignore index 1dbc687..5e68aad 100644 --- a/.gitignore +++ b/.gitignore @@ -9,12 +9,15 @@ __pycache__/ # Distribution / packaging .Python env/ +bin/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ +include/ +local/ lib/ lib64/ parts/ @@ -60,3 +63,4 @@ target/ #Ipython Notebook .ipynb_checkpoints +*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6d2ce45 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1 (2016-04-25) + +Initial release diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9f46f6 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Webpush Data encryption library for Python + +This is a work in progress. + +## App Installation + +You'll need to run `python virtualenv`. +Then +``` +bin/pip install -r requirements.txt +bin/python setup.py develop +``` + +### App Usage + +`webpush/publish.py` contains an example in the stand-alone function. + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f16eb97 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +http-ece==0.5.0 +python-jose==0.5.6 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0949acf --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +import io +import os + +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) +with io.open(os.path.join(here, 'README.md'), encoding='utf8') as f: + README = f.read() +with io.open(os.path.join(here, 'CHANGELOG.md'), encoding='utf8') as f: + CHANGES = f.read() + +extra_options = { + "packages": ["http-ece==0.5.0"] +} + + +setup(name="Webpusher", + version="0.1", + description='Webpush publication library', + long_description=README + '\n\n' + CHANGES, + classifiers=["Topic :: Internet :: WWW/HTTP", + "Programming Language :: Python :: Implementation :: PyPy", + 'Programming Language :: Python', + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7" + ], + keywords='push webpush publication', + author="jr conlin", + author_email="src+webpusher@jrconlin.com", + url='http:///', + license="MPL2", + test_suite="nose.collector", + include_package_data=True, + zip_safe=False, + tests_require=['nose', 'coverage', 'mock>=1.0.1', 'moto>=0.4.1'], + **extra_options + ) diff --git a/webpush/__init__.py b/webpush/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webpush/publish.py b/webpush/publish.py new file mode 100644 index 0000000..0785f10 --- /dev/null +++ b/webpush/publish.py @@ -0,0 +1,117 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import base64 +import os + +import http_ece +import pyelliptic + +VAPID = True + +try: + # from https://github.com/mozilla-services/vapid/tree/master/python + from py_vapid import Vapid +except ImportError: + VAPID = False + + +class WebPushException(Exception): + pass + + +class WebPusher: + + def __init__(self, keys): + for k in ['p256dh', 'auth']: + if keys.get(k) is None: + raise WebPushException("Missing keys value: %s", k) + receiverRaw = base64.urlsafe_b64decode(self._repad(keys['p256dh'])) + if len(receiverRaw) != 65 and receiverRaw[0] != "\x04": + raise WebPushException("Invalid p256dh key specified") + self.receiverKey = receiverRaw + self.authKey = base64.urlsafe_b64decode(self._repad(keys['auth'])) + + def _repad(self, str): + return str + "===="[:len(str) % 4] + + def encode(self, data): + # Salt is a random 16 byte array. + salt = os.urandom(16) + # The server key is an ephemeral ECDH key used only for this + # transaction + serverKey = pyelliptic.ECC(curve="prime256v1") + # the ID is the base64 of the raw key, minus the leading "\x04" + # ID tag. + serverKeyID = base64.urlsafe_b64encode(serverKey.get_pubkey()[1:]) + http_ece.keys[serverKeyID] = serverKey + http_ece.labels[serverKeyID] = "P-256" + + encrypted = http_ece.encrypt( + data, + salt=salt, + keyid=serverKeyID, + dh=self.receiverKey, + authSecret=self.authKey) + + return { + 'cryptokey': base64.urlsafe_b64encode( + serverKey.get_pubkey()).strip('='), + 'salt': base64.urlsafe_b64encode(salt).strip("="), + 'body': encrypted, + } + + def to_curl(self, endpoint, encode, headers={}, dataFile="encrypted.data"): + cryptokey = "keyid=p256dh;dh=%s" % encoded.get("cryptokey") + if headers.get('crypto-key'): + cryptokey = headers.get('crypto-key') + ',' + headers["crypto-key"] = headers.get("crypto-key", "") + cryptokey + headers["TTL"] = 60 + headers["content-encoding"] = "aesgcm" + headers["encryption"] = "keyid=p256dh;salt=%s" % encoded.get("salt") + reply = "curl -v -X POST %s " % endpoint + for key in headers: + reply += """-H "%s: %s" """ % (key, headers.get(key)) + if dataFile: + reply += "--data-binary @%s" % dataFile + return reply + + +if __name__ == "__main__": + + # The client provides the following values: + endpoint = ("https://updates.push.services.mozilla.com/push/v1/gAAAAABXAuZ" + "mKfEEyPbYfLXqtPW-yblFhEj-wjW5XHPJ3SMqjv9LlDWOAY9ljyZ80R4xHfD8" + "x2D_20j5mH4nbRQFyyCS33uyLgTp56zizeaitkMsw5EoAM8sRN_fz0Aaezrk9" + "W5uKpaf") + keys = { + "p256dh": ("BOrnIslXrUow2VAzKCUAE4sIbK00daEZCswOcf8m3T" + "F8V82B-OpOg5JbmYLg44kRcvQC1E2gMJshsUYA-_zMPR8"), + "auth": "k8JV6sjdbhAi1n3_LDBLvA", + } + + # This is the optional VAPID data + + vapid_claims = { + "aud": "http://example.com", + "sub": "mailto:admin@example.com", + } + + data = "Mary had a little lamb, with a nice mint jelly." + + vapid_headers = {} + if VAPID: + # You should only generate keys once, and write them out to + # safe storage. See https://github.com/mozilla-services/vapid for + # details. + vapid = Vapid() + vapid.generate_keys() + vapid_headers = vapid.sign(vapid_claims) + + push = WebPusher(keys=keys) + encoded = push.encode(data) + with open("encrypted.data", "w") as out: + out.write(encoded.get('body')) + + print push.to_curl(endpoint, encoded, vapid_headers)