Skip to content

Commit

Permalink
Merge pull request #809 from oberstet/payload_transparency
Browse files Browse the repository at this point in the history
add initial cut of interface for payload codec
  • Loading branch information
oberstet committed Apr 14, 2017
2 parents 159885f + 9d94fe3 commit b1e3fc7
Show file tree
Hide file tree
Showing 9 changed files with 852 additions and 480 deletions.
77 changes: 41 additions & 36 deletions autobahn/wamp/cryptobox.py
Expand Up @@ -26,20 +26,22 @@

from __future__ import absolute_import

import json

import six

from autobahn.util import public
from autobahn.wamp.interfaces import IPayloadCodec
from autobahn.wamp.types import EncodedPayload
from autobahn.wamp.serializer import _dumps as _json_dumps
from autobahn.wamp.serializer import _loads as _json_loads

__all__ = [
'HAS_CRYPTOBOX',
'EncryptedPayload'
'EncodedPayload'
]

try:
# try to import everything we need for WAMP-cryptobox
from nacl.encoding import Base64Encoder
from nacl.encoding import Base64Encoder, RawEncoder
from nacl.public import PrivateKey, PublicKey, Box
from nacl.utils import random
from pytrie import StringTrie
Expand All @@ -50,18 +52,6 @@
__all__.extend(['Key', 'KeyRing'])


class EncryptedPayload(object):
"""
Thin-wrapper holding encrypted application payloads.
"""

def __init__(self, algo, pkey, serializer, payload):
self.algo = algo
self.pkey = pkey
self.serializer = serializer
self.payload = payload


if HAS_CRYPTOBOX:

@public
Expand Down Expand Up @@ -128,6 +118,7 @@ class KeyRing(object):
for encrypting and decrypting WAMP message payloads.
"""

@public
def __init__(self, default_key=None):
"""
Expand Down Expand Up @@ -168,7 +159,7 @@ def set_key(self, uri, key):
else:
self._uri_to_key[uri] = key

def _get_box(self, is_originator, uri, match_exact=False):
def _get_box(self, is_originating, uri, match_exact=False):
try:
if match_exact:
key = self._uri_to_key[uri]
Expand All @@ -180,60 +171,74 @@ def _get_box(self, is_originator, uri, match_exact=False):
else:
return None

if is_originator:
if is_originating:
return key.originator_box
else:
return key.responder_box

def encrypt(self, is_originator, uri, args=None, kwargs=None):
@public
def encode(self, is_originating, uri, args=None, kwargs=None):
"""
Encrypt the given WAMP URI, args and kwargs into an EncryptedPayload instance, or None
Encrypt the given WAMP URI, args and kwargs into an EncodedPayload instance, or None
if the URI should not be encrypted.
"""
assert(type(is_originating) == bool)
assert(type(uri) == six.text_type)
assert(type(is_originator) == bool)
assert(args is None or type(args) in (list, tuple))
assert(kwargs is None or type(kwargs) == dict)

box = self._get_box(is_originator, uri)
box = self._get_box(is_originating, uri)

if not box:
return
# if we didn't find a crypto box, then return None, which
# signals that the payload travel unencrypted (normal)
return None

payload = {
u'uri': uri,
u'args': args,
u'kwargs': kwargs
}
nonce = random(Box.NONCE_SIZE)
payload_ser = json.dumps(payload)
payload_encr = box.encrypt(payload_ser, nonce, encoder=Base64Encoder)
payload_bytes = payload_encr.encode().decode('ascii')
payload_ser = _json_dumps(payload).encode('utf8')

payload_encr = box.encrypt(payload_ser, nonce, encoder=RawEncoder)

# above returns an instance of http://pynacl.readthedocs.io/en/latest/utils/#nacl.utils.EncryptedMessage
# which is a bytes _subclass_! hence we apply bytes() to get at the underlying plain
# bytes "scalar", which is the concatenation of `payload_encr.nonce + payload_encr.ciphertext`
payload_bytes = bytes(payload_encr)
payload_key = None

return EncryptedPayload(u'cryptobox', payload_key, u'json', payload_bytes)
return EncodedPayload(payload_bytes, u'cryptobox', u'json', enc_key=payload_key)

def decrypt(self, is_originator, uri, encrypted_payload):
@public
def decode(self, is_originating, uri, encoded_payload):
"""
Decrypt the given WAMP URI and EncryptedPayload into a tuple (uri, args, kwargs).
Decrypt the given WAMP URI and EncodedPayload into a tuple ``(uri, args, kwargs)``.
"""
assert(type(uri) == six.text_type)
assert(isinstance(encrypted_payload, EncryptedPayload))
assert(isinstance(encoded_payload, EncodedPayload))
assert(encoded_payload.enc_algo == u'cryptobox')

box = self._get_box(is_originator, uri)
box = self._get_box(is_originating, uri)

if not box:
raise Exception("received encrypted payload, but can't find key!")

payload_ser = box.decrypt(encrypted_payload.payload, encoder=Base64Encoder)
payload_ser = box.decrypt(encoded_payload.payload, encoder=RawEncoder)

if encrypted_payload.serializer != u'json':
raise Exception("received encrypted payload, but don't know how to process serializer '{}'".format(encrypted_payload.serializer))
if encoded_payload.enc_serializer != u'json':
raise Exception("received encrypted payload, but don't know how to process serializer '{}'".format(encoded_payload.enc_serializer))

payload = json.loads(payload_ser)
payload = _json_loads(payload_ser)

uri = payload[u'uri']
uri = payload.get(u'uri', None)
args = payload.get(u'args', None)
kwargs = payload.get(u'kwargs', None)

return uri, args, kwargs

# A WAMP-cryptobox keyring can work as a codec for
# payload transparency
IPayloadCodec.register(KeyRing)
5 changes: 3 additions & 2 deletions autobahn/wamp/exception.py
Expand Up @@ -216,9 +216,10 @@ class ApplicationError(Error):
exclusion of (any) *Callee* providing the procedure (WAMP AP).
"""

ENC_NO_KEYRING_ACTIVE = u"wamp.error.encryption.no_keyring_active"
ENC_NO_PAYLOAD_CODEC = u"wamp.error.no_payload_codec"
"""
WAMP-cryptobox application payload end-to-end encryption error.
WAMP message in payload transparency mode received, but no codec set
or codec did not decode the payload.
"""

ENC_TRUSTED_URI_MISMATCH = u"wamp.error.encryption.trusted_uri_mismatch"
Expand Down
95 changes: 91 additions & 4 deletions autobahn/wamp/interfaces.py
Expand Up @@ -36,6 +36,7 @@
'ITransport',
'ITransportHandler',
'ISession',
'IPayloadCodec'
)


Expand Down Expand Up @@ -470,12 +471,31 @@ def is_attached():
"""

@public
def set_keyring(keyring):
@abc.abstractmethod
def set_payload_codec(payload_codec):
"""
Set a payload codec on the session. To remove a previously set payload codec,
set the codec to ``None``.
Payload codecs are used with WAMP payload transparency mode.
:param payload_codec: The payload codec that should process application
payload of the given encoding.
:type payload_codec: object
implementing :class:`autobahn.wamp.interfaces.IPayloadCodec` or ``None``
"""

@public
@abc.abstractmethod
def get_payload_codec():
"""
Set the WAMP-cryptobox keyring to be used.
Get the current payload codec (if any) for the session.
:param keyring: The WAMP-cryptosign keyring.
:type keyring: instance of :class:`autobahn.wamp.cryptobox.KeyRing`
Payload codecs are used with WAMP payload transparency mode.
:returns: The current payload codec or ``None`` if no codec is active.
:rtype: object implementing
:class:`autobahn.wamp.interfaces.IPayloadCodec` or ``None``
"""

@public
Expand Down Expand Up @@ -647,3 +667,70 @@ class IAuthenticator(object):
def on_challenge(session, challenge):
"""
"""


@public
@six.add_metaclass(abc.ABCMeta)
class IPayloadCodec(object):
"""
WAMP payload codecs are used with WAMP payload transparency mode.
In payload transparency mode, application payloads are transmitted "raw",
as binary strings, without any processing at the WAMP router.
Payload transparency can be used eg for these use cases:
* end-to-end encryption of application payloads (WAMP-cryptobox)
* using serializers with custom user types, where the serializer and
the serializer implementation has native support for serializing
custom types (such as CBOR)
* transmitting MQTT payloads within WAMP, when the WAMP router is
providing a MQTT-WAMP bridge
"""

@public
@abc.abstractmethod
def encode(is_originating, uri, args=None, kwargs=None):
"""
Encodes application payload.
:param is_originating: Flag indicating whether the encoding
is to be done from an originator (a caller or publisher).
:type is_originating: bool
:param uri: The WAMP URI associated with the WAMP message for which
the payload is to be encoded (eg topic or procedure).
:type uri: str
:param args: Positional application payload.
:type args: list or None
:param kwargs: Keyword-based application payload.
:type kwargs: dict or None
:returns: The encoded application payload or None to
signal no encoding should be used.
:rtype: instance of :class:`autobahn.wamp.types.EncodedPayload`
"""

@public
@abc.abstractmethod
def decode(is_originating, uri, encoded_payload):
"""
Decode application payload.
:param is_originating: Flag indicating whether the encoding
is to be done from an originator (a caller or publisher).
:type is_originating: bool
:param uri: The WAMP URI associated with the WAMP message for which
the payload is to be encoded (eg topic or procedure).
:type uri: str
:param payload: The encoded application payload to be decoded.
:type payload: instance of :class:`autobahn.wamp.types.EncodedPayload`
:returns: A tuple with the decoded positional and keyword-based
application payload: ``(uri, args, kwargs)``
:rtype: tuple
"""

0 comments on commit b1e3fc7

Please sign in to comment.