Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/core/signing.py: 33%
125 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1"""
2Functions for creating and restoring url-safe signed JSON objects.
4The format used looks like this:
6>>> signing.dumps("hello")
7'ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk'
9There are two components here, separated by a ':'. The first component is a
10URLsafe base64 encoded JSON of the object passed to dumps(). The second
11component is a base64 encoded hmac/SHA-256 hash of "$first_component:$secret"
13signing.loads(s) checks the signature and returns the deserialized object.
14If the signature fails, a BadSignature exception is raised.
16>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
17'hello'
18>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified")
19...
20BadSignature: Signature "ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv42-modified" does not match
22You can optionally compress the JSON prior to base64 encoding it to save
23space, using the compress=True argument. This checks if compression actually
24helps and only applies compression if the result is a shorter string:
26>>> signing.dumps(list(range(1, 20)), compress=True)
27'.eJwFwcERACAIwLCF-rCiILN47r-GyZVJsNgkxaFxoDgxcOHGxMKD_T7vhAml:1QaUaL:BA0thEZrp4FQVXIXuOvYJtLJSrQ'
29The fact that the string is compressed is signalled by the prefixed '.' at the
30start of the base64 JSON.
32There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
33These functions make use of all of them.
34"""
36import base64
37import datetime
38import json
39import time
40import warnings
41import zlib
43from django.conf import settings
44from django.utils.crypto import constant_time_compare, salted_hmac
45from django.utils.deprecation import RemovedInDjango51Warning
46from django.utils.encoding import force_bytes
47from django.utils.module_loading import import_string
48from django.utils.regex_helper import _lazy_re_compile
50_SEP_UNSAFE = _lazy_re_compile(r"^[A-z0-9-_=]*$")
51BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
54class BadSignature(Exception):
55 """Signature does not match."""
57 pass
60class SignatureExpired(BadSignature):
61 """Signature timestamp is older than required max_age."""
63 pass
66def b62_encode(s):
67 if s == 0:
68 return "0"
69 sign = "-" if s < 0 else ""
70 s = abs(s)
71 encoded = ""
72 while s > 0:
73 s, remainder = divmod(s, 62)
74 encoded = BASE62_ALPHABET[remainder] + encoded
75 return sign + encoded
78def b62_decode(s):
79 if s == "0":
80 return 0
81 sign = 1
82 if s[0] == "-":
83 s = s[1:]
84 sign = -1
85 decoded = 0
86 for digit in s:
87 decoded = decoded * 62 + BASE62_ALPHABET.index(digit)
88 return sign * decoded
91def b64_encode(s):
92 return base64.urlsafe_b64encode(s).strip(b"=")
95def b64_decode(s):
96 pad = b"=" * (-len(s) % 4)
97 return base64.urlsafe_b64decode(s + pad)
100def base64_hmac(salt, value, key, algorithm="sha1"):
101 return b64_encode(
102 salted_hmac(salt, value, key, algorithm=algorithm).digest()
103 ).decode()
106def _cookie_signer_key(key):
107 # SECRET_KEYS items may be str or bytes.
108 return b"django.http.cookies" + force_bytes(key)
111def get_cookie_signer(salt="django.core.signing.get_cookie_signer"):
112 Signer = import_string(settings.SIGNING_BACKEND)
113 return Signer(
114 key=_cookie_signer_key(settings.SECRET_KEY),
115 fallback_keys=map(_cookie_signer_key, settings.SECRET_KEY_FALLBACKS),
116 salt=salt,
117 )
120class JSONSerializer:
121 """
122 Simple wrapper around json to be used in signing.dumps and
123 signing.loads.
124 """
126 def dumps(self, obj):
127 return json.dumps(obj, separators=(",", ":")).encode("latin-1")
129 def loads(self, data):
130 return json.loads(data.decode("latin-1"))
133def dumps(
134 obj, key=None, salt="django.core.signing", serializer=JSONSerializer, compress=False
135):
136 """
137 Return URL-safe, hmac signed base64 compressed JSON string. If key is
138 None, use settings.SECRET_KEY instead. The hmac algorithm is the default
139 Signer algorithm.
141 If compress is True (not the default), check if compressing using zlib can
142 save some space. Prepend a '.' to signify compression. This is included
143 in the signature, to protect against zip bombs.
145 Salt can be used to namespace the hash, so that a signed string is
146 only valid for a given namespace. Leaving this at the default
147 value or re-using a salt value across different parts of your
148 application without good cause is a security risk.
150 The serializer is expected to return a bytestring.
151 """
152 return TimestampSigner(key=key, salt=salt).sign_object(
153 obj, serializer=serializer, compress=compress
154 )
157def loads(
158 s,
159 key=None,
160 salt="django.core.signing",
161 serializer=JSONSerializer,
162 max_age=None,
163 fallback_keys=None,
164):
165 """
166 Reverse of dumps(), raise BadSignature if signature fails.
168 The serializer is expected to accept a bytestring.
169 """
170 return TimestampSigner(
171 key=key, salt=salt, fallback_keys=fallback_keys
172 ).unsign_object(
173 s,
174 serializer=serializer,
175 max_age=max_age,
176 )
179class Signer:
180 # RemovedInDjango51Warning: When the deprecation ends, replace with:
181 # def __init__(
182 # self, *, key=None, sep=":", salt=None, algorithm=None, fallback_keys=None
183 # ):
184 def __init__(
185 self,
186 *args,
187 key=None,
188 sep=":",
189 salt=None,
190 algorithm=None,
191 fallback_keys=None,
192 ):
193 self.key = key or settings.SECRET_KEY
194 self.fallback_keys = (
195 fallback_keys
196 if fallback_keys is not None
197 else settings.SECRET_KEY_FALLBACKS
198 )
199 self.sep = sep
200 self.salt = salt or "%s.%s" % (
201 self.__class__.__module__,
202 self.__class__.__name__,
203 )
204 self.algorithm = algorithm or "sha256"
205 # RemovedInDjango51Warning.
206 if args:
207 warnings.warn(
208 f"Passing positional arguments to {self.__class__.__name__} is "
209 f"deprecated.",
210 RemovedInDjango51Warning,
211 stacklevel=2,
212 )
213 for arg, attr in zip(
214 args, ["key", "sep", "salt", "algorithm", "fallback_keys"]
215 ):
216 if arg or attr == "sep":
217 setattr(self, attr, arg)
218 if _SEP_UNSAFE.match(self.sep):
219 raise ValueError(
220 "Unsafe Signer separator: %r (cannot be empty or consist of "
221 "only A-z0-9-_=)" % sep,
222 )
224 def signature(self, value, key=None):
225 key = key or self.key
226 return base64_hmac(self.salt + "signer", value, key, algorithm=self.algorithm)
228 def sign(self, value):
229 return "%s%s%s" % (value, self.sep, self.signature(value))
231 def unsign(self, signed_value):
232 if self.sep not in signed_value:
233 raise BadSignature('No "%s" found in value' % self.sep)
234 value, sig = signed_value.rsplit(self.sep, 1)
235 for key in [self.key, *self.fallback_keys]:
236 if constant_time_compare(sig, self.signature(value, key)):
237 return value
238 raise BadSignature('Signature "%s" does not match' % sig)
240 def sign_object(self, obj, serializer=JSONSerializer, compress=False):
241 """
242 Return URL-safe, hmac signed base64 compressed JSON string.
244 If compress is True (not the default), check if compressing using zlib
245 can save some space. Prepend a '.' to signify compression. This is
246 included in the signature, to protect against zip bombs.
248 The serializer is expected to return a bytestring.
249 """
250 data = serializer().dumps(obj)
251 # Flag for if it's been compressed or not.
252 is_compressed = False
254 if compress:
255 # Avoid zlib dependency unless compress is being used.
256 compressed = zlib.compress(data)
257 if len(compressed) < (len(data) - 1):
258 data = compressed
259 is_compressed = True
260 base64d = b64_encode(data).decode()
261 if is_compressed:
262 base64d = "." + base64d
263 return self.sign(base64d)
265 def unsign_object(self, signed_obj, serializer=JSONSerializer, **kwargs):
266 # Signer.unsign() returns str but base64 and zlib compression operate
267 # on bytes.
268 base64d = self.unsign(signed_obj, **kwargs).encode()
269 decompress = base64d[:1] == b"."
270 if decompress:
271 # It's compressed; uncompress it first.
272 base64d = base64d[1:]
273 data = b64_decode(base64d)
274 if decompress:
275 data = zlib.decompress(data)
276 return serializer().loads(data)
279class TimestampSigner(Signer):
280 def timestamp(self):
281 return b62_encode(int(time.time()))
283 def sign(self, value):
284 value = "%s%s%s" % (value, self.sep, self.timestamp())
285 return super().sign(value)
287 def unsign(self, value, max_age=None):
288 """
289 Retrieve original value and check it wasn't signed more
290 than max_age seconds ago.
291 """
292 result = super().unsign(value)
293 value, timestamp = result.rsplit(self.sep, 1)
294 timestamp = b62_decode(timestamp)
295 if max_age is not None:
296 if isinstance(max_age, datetime.timedelta):
297 max_age = max_age.total_seconds()
298 # Check timestamp is not older than max_age
299 age = time.time() - timestamp
300 if age > max_age:
301 raise SignatureExpired("Signature age %s > %s seconds" % (age, max_age))
302 return value