Skip to content
This repository has been archived by the owner on Jul 13, 2023. It is now read-only.

Commit

Permalink
feature: Add channel association with JWT public key
Browse files Browse the repository at this point in the history
Fixes #326
  • Loading branch information
jrconlin committed Feb 19, 2016
1 parent 5b50265 commit 6637b6b
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 28 deletions.
54 changes: 30 additions & 24 deletions autopush/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def _invalid_auth(self, fail):
log.msg("Invalid bearer token: " + message, **self._client_info)
raise VapidAuthException("Invalid bearer token: " + message)

def _process_auth(self, result):
def _process_auth(self, result, keys):
"""Process the optional VAPID auth token.
VAPID requires two headers to be present;
Expand All @@ -317,28 +317,25 @@ def _process_auth(self, result):
if not authorization:
return result

header_info = parse_header(self.request.headers.get('crypto-key'))
if not header_info:
if keys is None:
raise VapidAuthException("Missing Crypto-Key")
values = header_info[-1]
if isinstance(values, dict):
crypto_key = values.get('p256ecdsa')
try:
(auth_type, token) = authorization.split(' ', 1)
except ValueError:
raise VapidAuthException("Invalid Authorization header")
# if it's a bearer token containing what may be a JWT
if auth_type.lower() == AUTH_SCHEME and '.' in token:
d = deferToThread(extract_jwt, token, crypto_key)
d.addCallback(self._store_auth, crypto_key, token, result)
d.addErrback(self._invalid_auth)
return d
# otherwise, it's not, so ignore the VAPID data.
return result
else:

crypto_key = keys.get('p256ecdsa')
if not crypto_key:
raise VapidAuthException("Invalid bearer token: "
"improperly specified crypto-key")

try:
(auth_type, token) = authorization.split(' ', 1)
except ValueError:
raise VapidAuthException("Invalid Authorization header")
# if it's a bearer token containing what may be a JWT
if auth_type.lower() == AUTH_SCHEME and '.' in token:
d = deferToThread(extract_jwt, token, crypto_key)
d.addCallback(self._store_auth, crypto_key, token, result)
d.addErrback(self._invalid_auth)
return d
# otherwise, it's not, so ignore the VAPID data.
return result

class MessageHandler(AutoendpointHandler):
cors_methods = "DELETE"
Expand Down Expand Up @@ -405,10 +402,19 @@ def put(self, token):
"""
self.start_time = time.time()
fernet = self.ap_settings.fernet

d = deferToThread(fernet.decrypt, token.encode('utf8'))
d.addCallback(self._process_auth)
public_key = None
keys = {}
crypto_key = self.request.headers.get('crypto-key')
if crypto_key:
header_info = parse_header(crypto_key)
keys = header_info[-1]
if isinstance(keys, dict):
public_key = keys.get('p256ecdsa')

d = deferToThread(self.ap_settings.parse_endpoint,
token,
public_key)
d.addCallback(self._process_auth, )
d.addCallback(self._token_valid)
d.addErrback(self._auth_err)
d.addErrback(self._token_err)
Expand Down
69 changes: 65 additions & 4 deletions autopush/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Autopush Settings Object and Setup"""
import datetime
import socket
import urlparse

from hashlib import sha256

from cryptography.fernet import Fernet, MultiFernet
from twisted.internet import reactor
Expand Down Expand Up @@ -248,7 +251,65 @@ def update(self, **kwargs):
else:
setattr(self, key, val)

def make_endpoint(self, uaid, chid):
""" Create an endpoint from the identifiers"""
return self.endpoint_url + '/push/' + \
self.fernet.encrypt((uaid + ':' + chid).encode('utf8'))
def make_endpoint(self, uaid, chid, key=None):
"""Create an v1 or v2 endpoint from the indentifiers.
Both endpoints use bytes instead of hex to reduce ID length.
v0 is uaid.hex + ':' + chid.hex and is deprecated.
v1 is the uaid + chid
v2 is the uaid + chid + sha256(key).bytes
:param uaid: User Agent Identifier
:param chid: Channel or Subscription ID
:param key: Optional provided Public Key
:returns: Push endpoint
"""
root = self.endpoint_url + '/push/'
base = uaid.decode("hex") + chid.decode("hex")

if key is None:
return root + 'v1/' + self.fernet.encrypt(base).strip('=')

return root + 'v2/' + self.fernet.encrypt(base + sha256(key).digest())

def parse_endpoint(self, token, public_key=None):
"""Parse an endpoint into component elements of UAID, CHID and optional
key hash if v2
:param endpoint: this is either a path string or a full URL that will
be parsed into a path string.
:param public_key: the public key (from Encryption-Key: p256ecdsa=)
:raises ValueError: In the case of a malformed endpoint.
:returns: a tuple containing the (UAID, CHID, Public Key hash)
"""
path = token.split('/')
if path[0] is '' and len(path) > 1:
path = path[:1]
if len(path) == 1:
# version 0
d = deferToThread(self.fernet.decrypt, path[0].encode('utf8'))
d.addCallback(self._handle_tokens, 'v0', None)
return d

d = deferToThread(self.fernet.decrypt, path[1])
d.addCallback(self._handle_tokens, path[0], public_key)
return d

def _handle_tokens(self, token, version='v0', public_key=None):
if version is 'v0':
if ':' not in token:
raise ValueError("Corrupted push token")
return token.split(':')
import pdb;pdb.set_trace()
if version is 'v1' and len(token) != 32:
raise ValueError("Corrupted push token")
if version is 'v2':
if len(token) != 64:
raise ValueError("Corrupted push token")
if sha256(public_key).digest is not token[32:]:
raise ValueError("Key Mismatch")
return (token[:16].encode('hex'), token[16:32].encode('hex'))

0 comments on commit 6637b6b

Please sign in to comment.