Source code for django.core.signing

Functions for creating and restoring url-safe signed JSON objects.

The format used looks like this:

>>> signing.dumps("hello")

There are two components here, separated by a ':'. The first component is a
URLsafe base64 encoded JSON of the object passed to dumps(). The second
component is a base64 encoded hmac/SHA1 hash of "$first_component:$secret"

signing.loads(s) checks the signature and returns the deserialized object.
If the signature fails, a BadSignature exception is raised.

>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk")
>>> signing.loads("ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified")
BadSignature: Signature failed: ImhlbGxvIg:1QaUZC:YIye-ze3TTx7gtSv422nZA4sgmk-modified

You can optionally compress the JSON prior to base64 encoding it to save
space, using the compress=True argument. This checks if compression actually
helps and only applies compression if the result is a shorter string:

>>> signing.dumps(list(range(1, 20)), compress=True)

The fact that the string is compressed is signalled by the prefixed '.' at the
start of the base64 JSON.

There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'.
These functions make use of all of them.

import base64
import datetime
import json
import time
import zlib

from django.conf import settings
from django.utils import baseconv
from django.utils.crypto import constant_time_compare, salted_hmac
from django.utils.encoding import force_bytes
from django.utils.module_loading import import_string
from django.utils.regex_helper import _lazy_re_compile

_SEP_UNSAFE = _lazy_re_compile(r'^[A-z0-9-_=]*$')

class BadSignature(Exception):
    """Signature does not match."""

class SignatureExpired(BadSignature):
    """Signature timestamp is older than required max_age."""

def b64_encode(s):
    return base64.urlsafe_b64encode(s).strip(b'=')

def b64_decode(s):
    pad = b'=' * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)

def base64_hmac(salt, value, key, algorithm='sha1'):
    return b64_encode(salted_hmac(salt, value, key, algorithm=algorithm).digest()).decode()

def get_cookie_signer(salt='django.core.signing.get_cookie_signer'):
    Signer = import_string(settings.SIGNING_BACKEND)
    key = force_bytes(settings.SECRET_KEY)  # SECRET_KEY may be str or bytes.
    return Signer(b'django.http.cookies' + key, salt=salt)

class JSONSerializer:
    Simple wrapper around json to be used in signing.dumps and
    def dumps(self, obj):
        return json.dumps(obj, separators=(',', ':')).encode('latin-1')

    def loads(self, data):
        return json.loads(data.decode('latin-1'))

[docs]def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False): """ Return URL-safe, hmac signed base64 compressed JSON string. If key is None, use settings.SECRET_KEY instead. The hmac algorithm is the default Signer algorithm. If compress is True (not the default), check if compressing using zlib can save some space. Prepend a '.' to signify compression. This is included in the signature, to protect against zip bombs. Salt can be used to namespace the hash, so that a signed string is only valid for a given namespace. Leaving this at the default value or re-using a salt value across different parts of your application without good cause is a security risk. The serializer is expected to return a bytestring. """ return TimestampSigner(key, salt=salt).sign_object(obj, serializer=serializer, compress=compress)
[docs]def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None): """ Reverse of dumps(), raise BadSignature if signature fails. The serializer is expected to accept a bytestring. """ return TimestampSigner(key, salt=salt).unsign_object(s, serializer=serializer, max_age=max_age)
[docs]class Signer: # RemovedInDjango40Warning. legacy_algorithm = 'sha1' def __init__(self, key=None, sep=':', salt=None, algorithm=None): self.key = key or settings.SECRET_KEY self.sep = sep if _SEP_UNSAFE.match(self.sep): raise ValueError( 'Unsafe Signer separator: %r (cannot be empty or consist of ' 'only A-z0-9-_=)' % sep, ) self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__) # RemovedInDjango40Warning: when the deprecation ends, replace with: # self.algorithm = algorithm or 'sha256' self.algorithm = algorithm or settings.DEFAULT_HASHING_ALGORITHM def signature(self, value): return base64_hmac(self.salt + 'signer', value, self.key, algorithm=self.algorithm) def _legacy_signature(self, value): # RemovedInDjango40Warning. return base64_hmac(self.salt + 'signer', value, self.key, algorithm=self.legacy_algorithm) def sign(self, value): return '%s%s%s' % (value, self.sep, self.signature(value)) def unsign(self, signed_value): if self.sep not in signed_value: raise BadSignature('No "%s" found in value' % self.sep) value, sig = signed_value.rsplit(self.sep, 1) if ( constant_time_compare(sig, self.signature(value)) or ( self.legacy_algorithm and constant_time_compare(sig, self._legacy_signature(value)) ) ): return value raise BadSignature('Signature "%s" does not match' % sig) def sign_object(self, obj, serializer=JSONSerializer, compress=False): """ Return URL-safe, hmac signed base64 compressed JSON string. If compress is True (not the default), check if compressing using zlib can save some space. Prepend a '.' to signify compression. This is included in the signature, to protect against zip bombs. The serializer is expected to return a bytestring. """ data = serializer().dumps(obj) # Flag for if it's been compressed or not. is_compressed = False if compress: # Avoid zlib dependency unless compress is being used. compressed = zlib.compress(data) if len(compressed) < (len(data) - 1): data = compressed is_compressed = True base64d = b64_encode(data).decode() if is_compressed: base64d = '.' + base64d return self.sign(base64d) def unsign_object(self, signed_obj, serializer=JSONSerializer, **kwargs): # Signer.unsign() returns str but base64 and zlib compression operate # on bytes. base64d = self.unsign(signed_obj, **kwargs).encode() decompress = base64d[:1] == b'.' if decompress: # It's compressed; uncompress it first. base64d = base64d[1:] data = b64_decode(base64d) if decompress: data = zlib.decompress(data) return serializer().loads(data)
[docs]class TimestampSigner(Signer): def timestamp(self): return baseconv.base62.encode(int(time.time()))
[docs] def sign(self, value): value = '%s%s%s' % (value, self.sep, self.timestamp()) return super().sign(value)
[docs] def unsign(self, value, max_age=None): """ Retrieve original value and check it wasn't signed more than max_age seconds ago. """ result = super().unsign(value) value, timestamp = result.rsplit(self.sep, 1) timestamp = baseconv.base62.decode(timestamp) if max_age is not None: if isinstance(max_age, datetime.timedelta): max_age = max_age.total_seconds() # Check timestamp is not older than max_age age = time.time() - timestamp if age > max_age: raise SignatureExpired( 'Signature age %s > %s seconds' % (age, max_age)) return value
Back to Top