diff --git a/autopush/router/fcm.py b/autopush/router/fcm.py index 2f375acf..6385eb42 100644 --- a/autopush/router/fcm.py +++ b/autopush/router/fcm.py @@ -175,8 +175,9 @@ def _route(self, notification, router_data): data['body'] = notification.data data['con'] = notification.headers['encoding'] - data['enc'] = notification.headers['encryption'] + if 'encryption' in notification.headers: + data['enc'] = notification.headers['encryption'] if 'crypto_key' in notification.headers: data['cryptokey'] = notification.headers['crypto_key'] elif 'encryption_key' in notification.headers: diff --git a/autopush/router/gcm.py b/autopush/router/gcm.py index 6aaf594a..1fe6f8be 100644 --- a/autopush/router/gcm.py +++ b/autopush/router/gcm.py @@ -93,8 +93,9 @@ def _route(self, notification, uaid_data): data['body'] = notification.data data['con'] = notification.headers['encoding'] - data['enc'] = notification.headers['encryption'] + if 'encryption' in notification.headers: + data['enc'] = notification.headers.get('encryption') if 'crypto_key' in notification.headers: data['cryptokey'] = notification.headers['crypto_key'] elif 'encryption_key' in notification.headers: diff --git a/autopush/tests/test_integration.py b/autopush/tests/test_integration.py index d82221cb..f9c3c5a6 100644 --- a/autopush/tests/test_integration.py +++ b/autopush/tests/test_integration.py @@ -1865,6 +1865,100 @@ def test_registration(self): eq_(ca_data['enc'], salt) eq_(ca_data['body'], base64url_encode(data)) + @inlineCallbacks + def test_registration_aes128gcm(self): + self._add_router() + # get the senderid + url = "{}/v1/{}/{}/registration".format( + self.ep.settings.endpoint_url, + "gcm", + self.senderID, + ) + response, body = yield _agent('POST', url, body=json.dumps( + {"chid": str(uuid.uuid4()), + "token": uuid.uuid4().hex, + } + )) + eq_(response.code, 200) + jbody = json.loads(body) + + # Send a fake message + data = ("\xa2\xa5\xbd\xda\x40\xdc\xd1\xa5\xf9\x6a\x60\xa8\x57\x7b\x48" + "\xe4\x43\x02\x5a\x72\xe0\x64\x69\xcd\x29\x6f\x65\x44\x53\x78" + "\xe1\xd9\xf6\x46\x26\xce\x69") + content_encoding = "aes128gcm" + + response, body = yield _agent( + 'POST', + str(jbody['endpoint']), + headers=Headers({ + "ttl": ["0"], + "content-encoding": [content_encoding], + }), + body=data + ) + + ca_data = self._mock_send.call_args[0][0].data + eq_(response.code, 201) + # ChannelID here MUST match what we got from the registration call. + # Currently, this is a lowercase, hex UUID without dashes. + eq_(ca_data['chid'], jbody['channelID']) + eq_(ca_data['con'], content_encoding) + eq_(ca_data['body'], base64url_encode(data)) + ok_('enc' not in ca_data) + + @inlineCallbacks + def test_registration_aes128gcm_bad_(self): + self._add_router() + # get the senderid + url = "{}/v1/{}/{}/registration".format( + self.ep.settings.endpoint_url, + "gcm", + self.senderID, + ) + response, body = yield _agent('POST', url, body=json.dumps( + {"chid": str(uuid.uuid4()), + "token": uuid.uuid4().hex, + } + )) + eq_(response.code, 200) + jbody = json.loads(body) + + # Send a fake message + data = ("\xa2\xa5\xbd\xda\x40\xdc\xd1\xa5\xf9\x6a\x60\xa8\x57\x7b\x48" + "\xe4\x43\x02\x5a\x72\xe0\x64\x69\xcd\x29\x6f\x65\x44\x53\x78" + "\xe1\xd9\xf6\x46\x26\xce\x69") + crypto_key = ("keyid=p256dh;dh=BAFJxCIaaWyb4JSkZopERL9MjXBeh3WdBxew" + "SYP0cZWNMJaT7YNaJUiSqBuGUxfRj-9vpTPz5ANmUYq3-u-HWOI") + salt = "keyid=p256dh;salt=S82AseB7pAVBJ2143qtM3A" + content_encoding = "aes128gcm" + + response, body = yield _agent( + 'POST', + str(jbody['endpoint']), + headers=Headers({ + "ttl": ["0"], + "content-encoding": [content_encoding], + "crypto-key": [crypto_key] + }), + body=data + ) + + eq_(response.code, 400) + ok_("do not include 'dh' in " in body.lower()) + response, body = yield _agent( + 'POST', + str(jbody['endpoint']), + headers=Headers({ + "ttl": ["0"], + "content-encoding": [content_encoding], + "encryption": [salt] + }), + body=data + ) + eq_(response.code, 400) + ok_("do not include 'salt' in " in body.lower()) + @inlineCallbacks def test_registration_no_token(self): self._add_router() diff --git a/autopush/web/webpush.py b/autopush/web/webpush.py index d31f36e5..4b88d8aa 100644 --- a/autopush/web/webpush.py +++ b/autopush/web/webpush.py @@ -13,7 +13,7 @@ validates_schema, ) from marshmallow_polyfield import PolyField -from marshmallow.validate import OneOf +from marshmallow.validate import Equal from twisted.logger import Logger # noqa from twisted.internet.defer import Deferred # noqa from twisted.internet.defer import maybeDeferred @@ -170,7 +170,7 @@ class WebPushCrypto01HeaderSchema(Schema): content_encoding = fields.String( required=True, load_from="content-encoding", - validate=OneOf(["aesgcm128"]) + validate=Equal("aesgcm128") ) encryption = fields.String(required=True) encryption_key = fields.String( @@ -213,13 +213,13 @@ def validate_encryption_key(self, value): class WebPushCrypto04HeaderSchema(Schema): """Validates WebPush Message Encryption - Uses draft-ietf-webpush-encryption-04 rules for validation. + Uses draft-ietf-httpbis-encryption-encoding-04 rules for validation. """ content_encoding = fields.String( required=True, load_from="content-encoding", - validate=OneOf(["aesgcm"]) + validate=Equal("aesgcm") ) encryption = fields.String(required=True) crypto_key = fields.String( @@ -255,6 +255,40 @@ def reject_encryption_key(self, data, original_data): ) +class WebPushCrypto06HeaderSchema(Schema): + """Validates WebPush Message Encryption + + Uses draft-ietf-httpbis-encryption-encoding-06 rules for validation + + """ + + content_encoding = fields.String( + required=True, + load_from="content-encoding", + validate=Equal("aes128gcm") + ) + + encryption = fields.String(required=False) + crypto_key = fields.String(required=False, + load_from="crypto-key") + + @validates("encryption") + def validate_encryption(self, value): + if CryptoKey.parse_and_get_label(value, "salt"): + raise InvalidRequest("Do not include 'salt' in aes128gcm " + "Encryption header", + status_code=400, + errno=110) + + @validates("crypto_key") + def validate_crypto_key(self, value): + if CryptoKey.parse_and_get_label(value, "dh"): + raise InvalidRequest("Do not include 'dh' in aes128gcm " + "Crypto-Key header", + status_code=400, + errno=110) + + class WebPushInvalidContentEncodingSchema(Schema): """Returned to raise an Invalid Content-encoding error""" @validates_schema @@ -275,6 +309,8 @@ def conditional_crypto_deserialize(object_dict, parent_object_dict): return WebPushCrypto01HeaderSchema() elif encoding == "aesgcm": return WebPushCrypto04HeaderSchema() + elif encoding == "aes128gcm": + return WebPushCrypto06HeaderSchema() else: return WebPushInvalidContentEncodingSchema() else: