/lib/galaxy/security/idencoding.py

https://github.com/galaxyproject/galaxy · Python · 128 lines · 114 code · 10 blank · 4 comment · 1 complexity · 5679b75596e66be192c1a89ca2282b2d MD5 · raw file

  1. import codecs
  2. import collections
  3. import logging
  4. from Crypto.Cipher import Blowfish
  5. from Crypto.Random import get_random_bytes
  6. import galaxy.exceptions
  7. from galaxy.util import (
  8. smart_str,
  9. unicodify
  10. )
  11. log = logging.getLogger(__name__)
  12. MAXIMUM_ID_SECRET_BITS = 448
  13. MAXIMUM_ID_SECRET_LENGTH = int(MAXIMUM_ID_SECRET_BITS / 8)
  14. KIND_TOO_LONG_MESSAGE = "Galaxy coding error, keep encryption 'kinds' smaller to utilize more bites of randomness from id_secret values."
  15. class IdEncodingHelper:
  16. def __init__(self, **config):
  17. id_secret = config['id_secret']
  18. self.id_secret = id_secret
  19. self.id_cipher = Blowfish.new(smart_str(self.id_secret), mode=Blowfish.MODE_ECB)
  20. per_kind_id_secret_base = config.get('per_kind_id_secret_base', self.id_secret)
  21. self.id_ciphers_for_kind = _cipher_cache(per_kind_id_secret_base)
  22. def encode_id(self, obj_id, kind=None):
  23. if obj_id is None:
  24. raise galaxy.exceptions.MalformedId("Attempted to encode None id")
  25. id_cipher = self.__id_cipher(kind)
  26. # Convert to bytes
  27. s = smart_str(obj_id)
  28. # Pad to a multiple of 8 with leading "!"
  29. s = (b"!" * (8 - len(s) % 8)) + s
  30. # Encrypt
  31. return unicodify(codecs.encode(id_cipher.encrypt(s), 'hex'))
  32. def encode_dict_ids(self, a_dict, kind=None, skip_startswith=None):
  33. """
  34. Encode all ids in dictionary. Ids are identified by (a) an 'id' key or
  35. (b) a key that ends with '_id'
  36. """
  37. for key, val in a_dict.items():
  38. if key == 'id' or key.endswith('_id') and (skip_startswith is None or not key.startswith(skip_startswith)):
  39. a_dict[key] = self.encode_id(val, kind=kind)
  40. return a_dict
  41. def encode_all_ids(self, rval, recursive=False):
  42. """
  43. Encodes all integer values in the dict rval whose keys are 'id' or end
  44. with '_id' excluding `tool_id` which are consumed and produced as is
  45. via the API.
  46. """
  47. if not isinstance(rval, dict):
  48. return rval
  49. for k, v in rval.items():
  50. if (k == 'id' or k.endswith('_id')) and v is not None and k not in ['tool_id', 'external_id']:
  51. try:
  52. rval[k] = self.encode_id(v)
  53. except Exception:
  54. pass # probably already encoded
  55. if (k.endswith("_ids") and isinstance(v, list)):
  56. try:
  57. o = []
  58. for i in v:
  59. o.append(self.encode_id(i))
  60. rval[k] = o
  61. except Exception:
  62. pass
  63. else:
  64. if recursive and isinstance(v, dict):
  65. rval[k] = self.encode_all_ids(v, recursive)
  66. elif recursive and isinstance(v, list):
  67. rval[k] = [self.encode_all_ids(el, True) for el in v]
  68. return rval
  69. def decode_id(self, obj_id, kind=None):
  70. id_cipher = self.__id_cipher(kind)
  71. return int(unicodify(id_cipher.decrypt(codecs.decode(obj_id, 'hex'))).lstrip("!"))
  72. def encode_guid(self, session_key):
  73. # Session keys are strings
  74. # Pad to a multiple of 8 with leading "!"
  75. session_key = smart_str(session_key)
  76. s = (b"!" * (8 - len(session_key) % 8)) + session_key
  77. # Encrypt
  78. return codecs.encode(self.id_cipher.encrypt(s), 'hex')
  79. def decode_guid(self, session_key):
  80. # Session keys are strings
  81. decoded_session_key = codecs.decode(session_key, 'hex')
  82. return unicodify(self.id_cipher.decrypt(decoded_session_key)).lstrip('!')
  83. def get_new_guid(self):
  84. # Generate a unique, high entropy 128 bit random number
  85. return unicodify(codecs.encode(get_random_bytes(16), 'hex'))
  86. def __id_cipher(self, kind):
  87. if not kind:
  88. id_cipher = self.id_cipher
  89. else:
  90. id_cipher = self.id_ciphers_for_kind[kind]
  91. return id_cipher
  92. class _cipher_cache(collections.defaultdict):
  93. def __init__(self, secret_base):
  94. self.secret_base = secret_base
  95. def __missing__(self, key):
  96. assert len(key) < 15, KIND_TOO_LONG_MESSAGE
  97. secret = self.secret_base + "__" + key
  98. return Blowfish.new(_last_bits(secret), mode=Blowfish.MODE_ECB)
  99. def _last_bits(secret):
  100. """We append the kind at the end, so just use the bits at the end.
  101. """
  102. last_bits = smart_str(secret)
  103. if len(last_bits) > MAXIMUM_ID_SECRET_LENGTH:
  104. last_bits = last_bits[-MAXIMUM_ID_SECRET_LENGTH:]
  105. return last_bits