Plain is headed towards 1.0! Subscribe for development updates →

  1import time
  2import zlib
  3
  4from plain.signing import (
  5    JSONSerializer,
  6    SignatureExpired,
  7    Signer,
  8    b62_decode,
  9    b62_encode,
 10    b64_decode,
 11    b64_encode,
 12)
 13
 14
 15class ExpiringSigner(Signer):
 16    """A signer with an embedded expiration (vs max age unsign)"""
 17
 18    def sign(self, value, expires_in):
 19        timestamp = b62_encode(int(time.time() + expires_in))
 20        value = f"{value}{self.sep}{timestamp}"
 21        return super().sign(value)
 22
 23    def unsign(self, value):
 24        """
 25        Retrieve original value and check the expiration hasn't passed.
 26        """
 27        result = super().unsign(value)
 28        value, timestamp = result.rsplit(self.sep, 1)
 29        timestamp = b62_decode(timestamp)
 30        if timestamp < time.time():
 31            raise SignatureExpired("Signature expired")
 32        return value
 33
 34    def sign_object(
 35        self, obj, serializer=JSONSerializer, compress=False, expires_in=None
 36    ):
 37        """
 38        Return URL-safe, hmac signed base64 compressed JSON string.
 39
 40        If compress is True (not the default), check if compressing using zlib
 41        can save some space. Prepend a '.' to signify compression. This is
 42        included in the signature, to protect against zip bombs.
 43
 44        The serializer is expected to return a bytestring.
 45        """
 46        data = serializer().dumps(obj)
 47        # Flag for if it's been compressed or not.
 48        is_compressed = False
 49
 50        if compress:
 51            # Avoid zlib dependency unless compress is being used.
 52            compressed = zlib.compress(data)
 53            if len(compressed) < (len(data) - 1):
 54                data = compressed
 55                is_compressed = True
 56        base64d = b64_encode(data).decode()
 57        if is_compressed:
 58            base64d = "." + base64d
 59        return self.sign(base64d, expires_in)
 60
 61    def unsign_object(self, signed_obj, serializer=JSONSerializer):
 62        # Signer.unsign() returns str but base64 and zlib compression operate
 63        # on bytes.
 64        base64d = self.unsign(signed_obj).encode()
 65        decompress = base64d[:1] == b"."
 66        if decompress:
 67            # It's compressed; uncompress it first.
 68            base64d = base64d[1:]
 69        data = b64_decode(base64d)
 70        if decompress:
 71            data = zlib.decompress(data)
 72        return serializer().loads(data)
 73
 74
 75def dumps(
 76    obj,
 77    key=None,
 78    salt="plain.loginlink",
 79    serializer=JSONSerializer,
 80    compress=False,
 81    expires_in=None,
 82):
 83    """
 84    Return URL-safe, hmac signed base64 compressed JSON string. If key is
 85    None, use settings.SECRET_KEY instead. The hmac algorithm is the default
 86    Signer algorithm.
 87
 88    If compress is True (not the default), check if compressing using zlib can
 89    save some space. Prepend a '.' to signify compression. This is included
 90    in the signature, to protect against zip bombs.
 91
 92    Salt can be used to namespace the hash, so that a signed string is
 93    only valid for a given namespace. Leaving this at the default
 94    value or re-using a salt value across different parts of your
 95    application without good cause is a security risk.
 96
 97    The serializer is expected to return a bytestring.
 98    """
 99    return ExpiringSigner(key=key, salt=salt).sign_object(
100        obj, serializer=serializer, compress=compress, expires_in=expires_in
101    )
102
103
104def loads(
105    s,
106    key=None,
107    salt="plain.loginlink",
108    serializer=JSONSerializer,
109    fallback_keys=None,
110):
111    """
112    Reverse of dumps(), raise BadSignature if signature fails.
113
114    The serializer is expected to accept a bytestring.
115    """
116    return ExpiringSigner(
117        key=key, salt=salt, fallback_keys=fallback_keys
118    ).unsign_object(
119        s,
120        serializer=serializer,
121    )