Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cryptobox.py 10KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int 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. from autobahn.util import public
  27. from autobahn.wamp.interfaces import IPayloadCodec
  28. from autobahn.wamp.types import EncodedPayload
  29. from autobahn.wamp.serializer import _dumps as _json_dumps
  30. from autobahn.wamp.serializer import _loads as _json_loads
  31. __all__ = [
  32. 'HAS_CRYPTOBOX',
  33. 'EncodedPayload'
  34. ]
  35. try:
  36. # try to import everything we need for WAMP-cryptobox
  37. from nacl.encoding import Base64Encoder, RawEncoder, HexEncoder
  38. from nacl.public import PrivateKey, PublicKey, Box
  39. from nacl.utils import random
  40. from pytrie import StringTrie
  41. except ImportError:
  42. HAS_CRYPTOBOX = False
  43. else:
  44. HAS_CRYPTOBOX = True
  45. __all__.extend(['Key', 'KeyRing'])
  46. if HAS_CRYPTOBOX:
  47. @public
  48. class Key(object):
  49. """
  50. Holds originator and responder keys for an URI.
  51. The originator is either a caller or a publisher. The responder is either a callee or subscriber.
  52. """
  53. def __init__(self, originator_priv=None, originator_pub=None, responder_priv=None, responder_pub=None):
  54. # the originator private and public keys, as available
  55. if originator_priv:
  56. self.originator_priv = PrivateKey(originator_priv, encoder=Base64Encoder)
  57. else:
  58. self.originator_priv = None
  59. if self.originator_priv:
  60. self.originator_pub = self.originator_priv.public_key
  61. assert(originator_pub is None or originator_pub == self.originator_pub)
  62. else:
  63. self.originator_pub = PublicKey(originator_pub, encoder=Base64Encoder)
  64. # the responder private and public keys, as available
  65. if responder_priv:
  66. self.responder_priv = PrivateKey(responder_priv, encoder=Base64Encoder)
  67. else:
  68. self.responder_priv = None
  69. if self.responder_priv:
  70. self.responder_pub = self.responder_priv.public_key
  71. assert(responder_pub is None or responder_pub == self.responder_pub)
  72. else:
  73. self.responder_pub = PublicKey(responder_pub, encoder=Base64Encoder)
  74. # this crypto box is for originators (callers, publishers):
  75. #
  76. # 1. _encrypting_ WAMP messages outgoing from originators: CALL*, PUBLISH*
  77. # 2. _decrypting_ WAMP messages incoming to originators: RESULT*, ERROR
  78. #
  79. if self.originator_priv and self.responder_pub:
  80. self.originator_box = Box(self.originator_priv, self.responder_pub)
  81. else:
  82. self.originator_box = None
  83. # this crypto box is for responders (callees, subscribers):
  84. #
  85. # 1. _decrypting_ WAMP messages incoming to responders: INVOCATION*, EVENT*
  86. # 2. _encrypting_ WAMP messages outgoing from responders: YIELD*, ERROR
  87. #
  88. if self.responder_priv and self.originator_pub:
  89. self.responder_box = Box(self.responder_priv, self.originator_pub)
  90. else:
  91. self.responder_box = None
  92. if not (self.originator_box or self.responder_box):
  93. raise Exception("insufficient keys provided for at least originator or responder role")
  94. @public
  95. class SymKey(object):
  96. """
  97. Holds a symmetric key for an URI.
  98. """
  99. def __init__(self, raw=None):
  100. pass
  101. @public
  102. class KeyRing(object):
  103. """
  104. A keyring holds (cryptobox) public-private key pairs for use with WAMP-cryptobox payload
  105. encryption. The keyring can be set on a WAMP session and then transparently will get used
  106. for encrypting and decrypting WAMP message payloads.
  107. """
  108. @public
  109. def __init__(self, default_key=None):
  110. """
  111. Create a new key ring to hold public and private keys mapped from an URI space.
  112. """
  113. assert(default_key is None or isinstance(default_key, Key) or type(default_key == str))
  114. self._uri_to_key = StringTrie()
  115. if type(default_key) == str:
  116. default_key = Key(originator_priv=default_key, responder_priv=default_key)
  117. self._default_key = default_key
  118. @public
  119. def generate_key(self):
  120. """
  121. Generate a new private key and return a pair with the base64 encodings
  122. of (priv_key, pub_key).
  123. """
  124. key = PrivateKey.generate()
  125. priv_key = key.encode(encoder=Base64Encoder)
  126. pub_key = key.public_key.encode(encoder=Base64Encoder)
  127. return priv_key.decode('ascii'), pub_key.decode('ascii')
  128. @public
  129. def generate_key_hex(self):
  130. """
  131. Generate a new private key and return a pair with the hex encodings
  132. of (priv_key, pub_key).
  133. """
  134. key = PrivateKey.generate()
  135. priv_key = key.encode(encoder=HexEncoder)
  136. pub_key = key.public_key.encode(encoder=HexEncoder)
  137. return priv_key.decode('ascii'), pub_key.decode('ascii')
  138. @public
  139. def set_key(self, uri, key):
  140. """
  141. Add a key set for a given URI.
  142. """
  143. assert(type(uri) == str)
  144. assert(key is None or isinstance(key, Key) or type(key) == str)
  145. if type(key) == str:
  146. key = Key(originator_priv=key, responder_priv=key)
  147. if uri == '':
  148. self._default_key = key
  149. else:
  150. if key is None:
  151. if uri in self._uri_to_key:
  152. del self._uri_to_key[uri]
  153. else:
  154. self._uri_to_key[uri] = key
  155. @public
  156. def rotate_key(self, uri):
  157. assert(type(uri) == str)
  158. if uri in self._uri_to_key:
  159. self._uri_to_key[uri].rotate()
  160. else:
  161. self._uri_to_key[uri].rotate()
  162. def _get_box(self, is_originating, uri, match_exact=False):
  163. try:
  164. if match_exact:
  165. key = self._uri_to_key[uri]
  166. else:
  167. key = self._uri_to_key.longest_prefix_value(uri)
  168. except KeyError:
  169. if self._default_key:
  170. key = self._default_key
  171. else:
  172. return None
  173. if is_originating:
  174. return key.originator_box
  175. else:
  176. return key.responder_box
  177. @public
  178. def encode(self, is_originating, uri, args=None, kwargs=None):
  179. """
  180. Encrypt the given WAMP URI, args and kwargs into an EncodedPayload instance, or None
  181. if the URI should not be encrypted.
  182. """
  183. assert(type(is_originating) == bool)
  184. assert(type(uri) == str)
  185. assert(args is None or type(args) in (list, tuple))
  186. assert(kwargs is None or type(kwargs) == dict)
  187. box = self._get_box(is_originating, uri)
  188. if not box:
  189. # if we didn't find a crypto box, then return None, which
  190. # signals that the payload travel unencrypted (normal)
  191. return None
  192. payload = {
  193. 'uri': uri,
  194. 'args': args,
  195. 'kwargs': kwargs
  196. }
  197. nonce = random(Box.NONCE_SIZE)
  198. payload_ser = _json_dumps(payload).encode('utf8')
  199. payload_encr = box.encrypt(payload_ser, nonce, encoder=RawEncoder)
  200. # above returns an instance of http://pynacl.readthedocs.io/en/latest/utils/#nacl.utils.EncryptedMessage
  201. # which is a bytes _subclass_! hence we apply bytes() to get at the underlying plain
  202. # bytes "scalar", which is the concatenation of `payload_encr.nonce + payload_encr.ciphertext`
  203. payload_bytes = bytes(payload_encr)
  204. payload_key = None
  205. return EncodedPayload(payload_bytes, 'cryptobox', 'json', enc_key=payload_key)
  206. @public
  207. def decode(self, is_originating, uri, encoded_payload):
  208. """
  209. Decrypt the given WAMP URI and EncodedPayload into a tuple ``(uri, args, kwargs)``.
  210. """
  211. assert(type(uri) == str)
  212. assert(isinstance(encoded_payload, EncodedPayload))
  213. assert(encoded_payload.enc_algo == 'cryptobox')
  214. box = self._get_box(is_originating, uri)
  215. if not box:
  216. raise Exception("received encrypted payload, but can't find key!")
  217. payload_ser = box.decrypt(encoded_payload.payload, encoder=RawEncoder)
  218. if encoded_payload.enc_serializer != 'json':
  219. raise Exception("received encrypted payload, but don't know how to process serializer '{}'".format(encoded_payload.enc_serializer))
  220. payload = _json_loads(payload_ser.decode('utf8'))
  221. uri = payload.get('uri', None)
  222. args = payload.get('args', None)
  223. kwargs = payload.get('kwargs', None)
  224. return uri, args, kwargs
  225. # A WAMP-cryptobox keyring can work as a codec for
  226. # payload transparency
  227. IPayloadCodec.register(KeyRing)