signing.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. """
  2. Functions for creating and restoring url-safe signed JSON objects.
  3. The format used looks like this:
  4. >>> signing.dumps("hello")
  5. 'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk'
  6. There are two components here, separated by a ':'. The first component is a
  7. URLsafe base64 encoded JSON of the object passed to dumps(). The second
  8. component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret"
  9. signing.loads(s) checks the signature and returns the deserialized object.
  10. If the signature fails, a BadSignature exception is raised.
  11. >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
  12. u'hello'
  13. >>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified")
  14. ...
  15. BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified
  16. You can optionally compress the JSON prior to base64 encoding it to save
  17. space, using the compress=True argument. This checks if compression actually
  18. helps and only applies compression if the result is a shorter string:
  19. >>> signing.dumps(range(1, 20), compress=True)
  20. '.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ'
  21. The fact that the string is compressed is signalled by the prefixed '.' at the
  22. start of the base64 JSON.
  23. There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
  24. These functions make use of all of them.
  25. """
  26. from __future__ import unicode_literals
  27. import base64
  28. import datetime
  29. import json
  30. import re
  31. import time
  32. import zlib
  33. from django.conf import settings
  34. from django.utils import baseconv
  35. from django.utils.crypto import constant_time_compare, salted_hmac
  36. from django.utils.encoding import force_bytes, force_str, force_text
  37. from django.utils.module_loading import import_string
  38. _SEP_UNSAFE = re.compile(r'^[A-z0-9-_=]*$')
  39. class BadSignature(Exception):
  40. """
  41. Signature does not match
  42. """
  43. pass
  44. class SignatureExpired(BadSignature):
  45. """
  46. Signature timestamp is older than required max_age
  47. """
  48. pass
  49. def b64_encode(s):
  50. return base64.urlsafe_b64encode(s).strip(b'=')
  51. def b64_decode(s):
  52. pad = b'=' * (-len(s) % 4)
  53. return base64.urlsafe_b64decode(s + pad)
  54. def base64_hmac(salt, value, key):
  55. return b64_encode(salted_hmac(salt, value, key).digest())
  56. def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
  57. Signer = import_string(settings.SIGNING_BACKEND)
  58. key = force_bytes(settings.SECRET_KEY)
  59. return Signer(b'django.http.cookies' + key, salt=salt)
  60. class JSONSerializer(object):
  61. """
  62. Simple wrapper around json to be used in signing.dumps and
  63. signing.loads.
  64. """
  65. def dumps(self, obj):
  66. return json.dumps(obj, separators=(',', ':')).encode('latin-1')
  67. def loads(self, data):
  68. return json.loads(data.decode('latin-1'))
  69. def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
  70. """
  71. Returns URL-safe, sha1 signed base64 compressed JSON string. If key is
  72. None, settings.SECRET_KEY is used instead.
  73. If compress is True (not the default) checks if compressing using zlib can
  74. save some space. Prepends a '.' to signify compression. This is included
  75. in the signature, to protect against zip bombs.
  76. Salt can be used to namespace the hash, so that a signed string is
  77. only valid for a given namespace. Leaving this at the default
  78. value or re-using a salt value across different parts of your
  79. application without good cause is a security risk.
  80. The serializer is expected to return a bytestring.
  81. """
  82. data = serializer().dumps(obj)
  83. # Flag for if it's been compressed or not
  84. is_compressed = False
  85. if compress:
  86. # Avoid zlib dependency unless compress is being used
  87. compressed = zlib.compress(data)
  88. if len(compressed) < (len(data) - 1):
  89. data = compressed
  90. is_compressed = True
  91. base64d = b64_encode(data)
  92. if is_compressed:
  93. base64d = b'.' + base64d
  94. return TimestampSigner(key, salt=salt).sign(base64d)
  95. def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
  96. """
  97. Reverse of dumps(), raises BadSignature if signature fails.
  98. The serializer is expected to accept a bytestring.
  99. """
  100. # TimestampSigner.unsign always returns unicode but base64 and zlib
  101. # compression operate on bytes.
  102. base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
  103. decompress = False
  104. if base64d[:1] == b'.':
  105. # It's compressed; uncompress it first
  106. base64d = base64d[1:]
  107. decompress = True
  108. data = b64_decode(base64d)
  109. if decompress:
  110. data = zlib.decompress(data)
  111. return serializer().loads(data)
  112. class Signer(object):
  113. def __init__(self, key=None, sep=':', salt=None):
  114. # Use of native strings in all versions of Python
  115. self.key = key or settings.SECRET_KEY
  116. self.sep = force_str(sep)
  117. if _SEP_UNSAFE.match(self.sep):
  118. raise ValueError(
  119. 'Unsafe Signer separator: %r (cannot be empty or consist of '
  120. 'only A-z0-9-_=)' % sep,
  121. )
  122. self.salt = force_str(salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__))
  123. def signature(self, value):
  124. signature = base64_hmac(self.salt + 'signer', value, self.key)
  125. # Convert the signature from bytes to str only on Python 3
  126. return force_str(signature)
  127. def sign(self, value):
  128. value = force_str(value)
  129. return str('%s%s%s') % (value, self.sep, self.signature(value))
  130. def unsign(self, signed_value):
  131. signed_value = force_str(signed_value)
  132. if self.sep not in signed_value:
  133. raise BadSignature('No "%s" found in value' % self.sep)
  134. value, sig = signed_value.rsplit(self.sep, 1)
  135. if constant_time_compare(sig, self.signature(value)):
  136. return force_text(value)
  137. raise BadSignature('Signature "%s" does not match' % sig)
  138. class TimestampSigner(Signer):
  139. def timestamp(self):
  140. return baseconv.base62.encode(int(time.time()))
  141. def sign(self, value):
  142. value = force_str(value)
  143. value = str('%s%s%s') % (value, self.sep, self.timestamp())
  144. return super(TimestampSigner, self).sign(value)
  145. def unsign(self, value, max_age=None):
  146. """
  147. Retrieve original value and check it wasn't signed more
  148. than max_age seconds ago.
  149. """
  150. result = super(TimestampSigner, self).unsign(value)
  151. value, timestamp = result.rsplit(self.sep, 1)
  152. timestamp = baseconv.base62.decode(timestamp)
  153. if max_age is not None:
  154. if isinstance(max_age, datetime.timedelta):
  155. max_age = max_age.total_seconds()
  156. # Check timestamp is not older than max_age
  157. age = time.time() - timestamp
  158. if age > max_age:
  159. raise SignatureExpired(
  160. 'Signature age %s > %s seconds' % (age, max_age))
  161. return value