diff --git a/autopush/endpoint.py b/autopush/endpoint.py index 0f81d568..be589ecd 100644 --- a/autopush/endpoint.py +++ b/autopush/endpoint.py @@ -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; @@ -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" @@ -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) diff --git a/autopush/settings.py b/autopush/settings.py index 8a8daec4..7b2df8e9 100644 --- a/autopush/settings.py +++ b/autopush/settings.py @@ -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 @@ -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'))