1############################################################################### 2# 3# The MIT License (MIT) 4# 5# Copyright (c) Crossbar.io Technologies GmbH 6# 7# Permission is hereby granted, free of charge, to any person obtaining a copy 8# of this software and associated documentation files (the "Software"), to deal 9# in the Software without restriction, including without limitation the rights 10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11# copies of the Software, and to permit persons to whom the Software is 12# furnished to do so, subject to the following conditions: 13# 14# The above copyright notice and this permission notice shall be included in 15# all copies or substantial portions of the Software. 16# 17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23# THE SOFTWARE. 24# 25############################################################################### 26 27from __future__ import absolute_import 28 29import six 30 31from autobahn.util import public 32from autobahn.wamp.interfaces import IPayloadCodec 33from autobahn.wamp.types import EncodedPayload 34from autobahn.wamp.serializer import _dumps as _json_dumps 35from autobahn.wamp.serializer import _loads as _json_loads 36 37__all__ = [ 38 'HAS_CRYPTOBOX', 39 'EncodedPayload' 40] 41 42try: 43 # try to import everything we need for WAMP-cryptobox 44 from nacl.encoding import Base64Encoder, RawEncoder, HexEncoder 45 from nacl.public import PrivateKey, PublicKey, Box 46 from nacl.utils import random 47 from pytrie import StringTrie 48except ImportError: 49 HAS_CRYPTOBOX = False 50else: 51 HAS_CRYPTOBOX = True 52 __all__.extend(['Key', 'KeyRing']) 53 54 55if HAS_CRYPTOBOX: 56 57 @public 58 class Key(object): 59 """ 60 Holds originator and responder keys for an URI. 61 62 The originator is either a caller or a publisher. The responder is either a callee or subscriber. 63 """ 64 65 def __init__(self, originator_priv=None, originator_pub=None, responder_priv=None, responder_pub=None): 66 # the originator private and public keys, as available 67 if originator_priv: 68 self.originator_priv = PrivateKey(originator_priv, encoder=Base64Encoder) 69 else: 70 self.originator_priv = None 71 72 if self.originator_priv: 73 self.originator_pub = self.originator_priv.public_key 74 assert(originator_pub is None or originator_pub == self.originator_pub) 75 else: 76 self.originator_pub = PublicKey(originator_pub, encoder=Base64Encoder) 77 78 # the responder private and public keys, as available 79 if responder_priv: 80 self.responder_priv = PrivateKey(responder_priv, encoder=Base64Encoder) 81 else: 82 self.responder_priv = None 83 84 if self.responder_priv: 85 self.responder_pub = self.responder_priv.public_key 86 assert(responder_pub is None or responder_pub == self.responder_pub) 87 else: 88 self.responder_pub = PublicKey(responder_pub, encoder=Base64Encoder) 89 90 # this crypto box is for originators (callers, publishers): 91 # 92 # 1. _encrypting_ WAMP messages outgoing from originators: CALL*, PUBLISH* 93 # 2. _decrypting_ WAMP messages incoming to originators: RESULT*, ERROR 94 # 95 if self.originator_priv and self.responder_pub: 96 self.originator_box = Box(self.originator_priv, self.responder_pub) 97 else: 98 self.originator_box = None 99 100 # this crypto box is for responders (callees, subscribers): 101 # 102 # 1. _decrypting_ WAMP messages incoming to responders: INVOCATION*, EVENT* 103 # 2. _encrypting_ WAMP messages outgoing from responders: YIELD*, ERROR 104 # 105 if self.responder_priv and self.originator_pub: 106 self.responder_box = Box(self.responder_priv, self.originator_pub) 107 else: 108 self.responder_box = None 109 110 if not (self.originator_box or self.responder_box): 111 raise Exception("insufficient keys provided for at least originator or responder role") 112 113 @public 114 class SymKey(object): 115 """ 116 Holds a symmetric key for an URI. 117 """ 118 def __init__(self, raw=None): 119 pass 120 121 @public 122 class KeyRing(object): 123 """ 124 A keyring holds (cryptobox) public-private key pairs for use with WAMP-cryptobox payload 125 encryption. The keyring can be set on a WAMP session and then transparently will get used 126 for encrypting and decrypting WAMP message payloads. 127 """ 128 129 @public 130 def __init__(self, default_key=None): 131 """ 132 133 Create a new key ring to hold public and private keys mapped from an URI space. 134 """ 135 assert(default_key is None or isinstance(default_key, Key) or type(default_key == six.text_type)) 136 self._uri_to_key = StringTrie() 137 if type(default_key) == six.text_type: 138 default_key = Key(originator_priv=default_key, responder_priv=default_key) 139 self._default_key = default_key 140 141 @public 142 def generate_key(self): 143 """ 144 Generate a new private key and return a pair with the base64 encodings 145 of (priv_key, pub_key). 146 """ 147 key = PrivateKey.generate() 148 priv_key = key.encode(encoder=Base64Encoder) 149 pub_key = key.public_key.encode(encoder=Base64Encoder) 150 return priv_key.decode('ascii'), pub_key.decode('ascii') 151 152 @public 153 def generate_key_hex(self): 154 """ 155 Generate a new private key and return a pair with the hex encodings 156 of (priv_key, pub_key). 157 """ 158 key = PrivateKey.generate() 159 priv_key = key.encode(encoder=HexEncoder) 160 pub_key = key.public_key.encode(encoder=HexEncoder) 161 return priv_key.decode('ascii'), pub_key.decode('ascii') 162 163 @public 164 def set_key(self, uri, key): 165 """ 166 Add a key set for a given URI. 167 """ 168 assert(type(uri) == six.text_type) 169 assert(key is None or isinstance(key, Key) or type(key) == six.text_type) 170 if type(key) == six.text_type: 171 key = Key(originator_priv=key, responder_priv=key) 172 if uri == u'': 173 self._default_key = key 174 else: 175 if key is None: 176 if uri in self._uri_to_key: 177 del self._uri_to_key[uri] 178 else: 179 self._uri_to_key[uri] = key 180 181 @public 182 def rotate_key(self, uri): 183 assert(type(uri) == six.text_type) 184 if uri in self._uri_to_key: 185 self._uri_to_key[uri].rotate() 186 else: 187 self._uri_to_key[uri].rotate() 188 189 def _get_box(self, is_originating, uri, match_exact=False): 190 try: 191 if match_exact: 192 key = self._uri_to_key[uri] 193 else: 194 key = self._uri_to_key.longest_prefix_value(uri) 195 except KeyError: 196 if self._default_key: 197 key = self._default_key 198 else: 199 return None 200 201 if is_originating: 202 return key.originator_box 203 else: 204 return key.responder_box 205 206 @public 207 def encode(self, is_originating, uri, args=None, kwargs=None): 208 """ 209 Encrypt the given WAMP URI, args and kwargs into an EncodedPayload instance, or None 210 if the URI should not be encrypted. 211 """ 212 assert(type(is_originating) == bool) 213 assert(type(uri) == six.text_type) 214 assert(args is None or type(args) in (list, tuple)) 215 assert(kwargs is None or type(kwargs) == dict) 216 217 box = self._get_box(is_originating, uri) 218 219 if not box: 220 # if we didn't find a crypto box, then return None, which 221 # signals that the payload travel unencrypted (normal) 222 return None 223 224 payload = { 225 u'uri': uri, 226 u'args': args, 227 u'kwargs': kwargs 228 } 229 nonce = random(Box.NONCE_SIZE) 230 payload_ser = _json_dumps(payload).encode('utf8') 231 232 payload_encr = box.encrypt(payload_ser, nonce, encoder=RawEncoder) 233 234 # above returns an instance of http://pynacl.readthedocs.io/en/latest/utils/#nacl.utils.EncryptedMessage 235 # which is a bytes _subclass_! hence we apply bytes() to get at the underlying plain 236 # bytes "scalar", which is the concatenation of `payload_encr.nonce + payload_encr.ciphertext` 237 payload_bytes = bytes(payload_encr) 238 payload_key = None 239 240 return EncodedPayload(payload_bytes, u'cryptobox', u'json', enc_key=payload_key) 241 242 @public 243 def decode(self, is_originating, uri, encoded_payload): 244 """ 245 Decrypt the given WAMP URI and EncodedPayload into a tuple ``(uri, args, kwargs)``. 246 """ 247 assert(type(uri) == six.text_type) 248 assert(isinstance(encoded_payload, EncodedPayload)) 249 assert(encoded_payload.enc_algo == u'cryptobox') 250 251 box = self._get_box(is_originating, uri) 252 253 if not box: 254 raise Exception("received encrypted payload, but can't find key!") 255 256 payload_ser = box.decrypt(encoded_payload.payload, encoder=RawEncoder) 257 258 if encoded_payload.enc_serializer != u'json': 259 raise Exception("received encrypted payload, but don't know how to process serializer '{}'".format(encoded_payload.enc_serializer)) 260 261 payload = _json_loads(payload_ser.decode('utf8')) 262 263 uri = payload.get(u'uri', None) 264 args = payload.get(u'args', None) 265 kwargs = payload.get(u'kwargs', None) 266 267 return uri, args, kwargs 268 269 # A WAMP-cryptobox keyring can work as a codec for 270 # payload transparency 271 IPayloadCodec.register(KeyRing) 272