first commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
from ._json import json
|
||||
from .encoding import base64_decode
|
||||
from .encoding import base64_encode
|
||||
from .encoding import want_bytes
|
||||
from .exc import BadData
|
||||
from .exc import BadHeader
|
||||
from .exc import BadPayload
|
||||
from .exc import BadSignature
|
||||
from .exc import BadTimeSignature
|
||||
from .exc import SignatureExpired
|
||||
from .jws import JSONWebSignatureSerializer
|
||||
from .jws import TimedJSONWebSignatureSerializer
|
||||
from .serializer import Serializer
|
||||
from .signer import HMACAlgorithm
|
||||
from .signer import NoneAlgorithm
|
||||
from .signer import Signer
|
||||
from .timed import TimedSerializer
|
||||
from .timed import TimestampSigner
|
||||
from .url_safe import URLSafeSerializer
|
||||
from .url_safe import URLSafeTimedSerializer
|
||||
|
||||
__version__ = "1.1.0"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
import decimal
|
||||
import hmac
|
||||
import numbers
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
||||
if PY2:
|
||||
from itertools import izip
|
||||
|
||||
text_type = unicode # noqa: 821
|
||||
else:
|
||||
izip = zip
|
||||
text_type = str
|
||||
|
||||
number_types = (numbers.Real, decimal.Decimal)
|
||||
|
||||
|
||||
def _constant_time_compare(val1, val2):
|
||||
"""Return ``True`` if the two strings are equal, ``False``
|
||||
otherwise.
|
||||
|
||||
The time taken is independent of the number of characters that
|
||||
match. Do not use this function for anything else than comparision
|
||||
with known length targets.
|
||||
|
||||
This is should be implemented in C in order to get it completely
|
||||
right.
|
||||
|
||||
This is an alias of :func:`hmac.compare_digest` on Python>=2.7,3.3.
|
||||
"""
|
||||
len_eq = len(val1) == len(val2)
|
||||
if len_eq:
|
||||
result = 0
|
||||
left = val1
|
||||
else:
|
||||
result = 1
|
||||
left = val2
|
||||
for x, y in izip(bytearray(left), bytearray(val2)):
|
||||
result |= x ^ y
|
||||
return result == 0
|
||||
|
||||
|
||||
# Starting with 2.7/3.3 the standard library has a c-implementation for
|
||||
# constant time string compares.
|
||||
constant_time_compare = getattr(hmac, "compare_digest", _constant_time_compare)
|
||||
@@ -0,0 +1,18 @@
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
|
||||
class _CompactJSON(object):
|
||||
"""Wrapper around json module that strips whitespace."""
|
||||
|
||||
@staticmethod
|
||||
def loads(payload):
|
||||
return json.loads(payload)
|
||||
|
||||
@staticmethod
|
||||
def dumps(obj, **kwargs):
|
||||
kwargs.setdefault("ensure_ascii", False)
|
||||
kwargs.setdefault("separators", (",", ":"))
|
||||
return json.dumps(obj, **kwargs)
|
||||
@@ -0,0 +1,49 @@
|
||||
import base64
|
||||
import string
|
||||
import struct
|
||||
|
||||
from ._compat import text_type
|
||||
from .exc import BadData
|
||||
|
||||
|
||||
def want_bytes(s, encoding="utf-8", errors="strict"):
|
||||
if isinstance(s, text_type):
|
||||
s = s.encode(encoding, errors)
|
||||
return s
|
||||
|
||||
|
||||
def base64_encode(string):
|
||||
"""Base64 encode a string of bytes or text. The resulting bytes are
|
||||
safe to use in URLs.
|
||||
"""
|
||||
string = want_bytes(string)
|
||||
return base64.urlsafe_b64encode(string).rstrip(b"=")
|
||||
|
||||
|
||||
def base64_decode(string):
|
||||
"""Base64 decode a URL-safe string of bytes or text. The result is
|
||||
bytes.
|
||||
"""
|
||||
string = want_bytes(string, encoding="ascii", errors="ignore")
|
||||
string += b"=" * (-len(string) % 4)
|
||||
|
||||
try:
|
||||
return base64.urlsafe_b64decode(string)
|
||||
except (TypeError, ValueError):
|
||||
raise BadData("Invalid base64-encoded data")
|
||||
|
||||
|
||||
# The alphabet used by base64.urlsafe_*
|
||||
_base64_alphabet = (string.ascii_letters + string.digits + "-_=").encode("ascii")
|
||||
|
||||
_int64_struct = struct.Struct(">Q")
|
||||
_int_to_bytes = _int64_struct.pack
|
||||
_bytes_to_int = _int64_struct.unpack
|
||||
|
||||
|
||||
def int_to_bytes(num):
|
||||
return _int_to_bytes(num).lstrip(b"\x00")
|
||||
|
||||
|
||||
def bytes_to_int(bytestr):
|
||||
return _bytes_to_int(bytestr.rjust(8, b"\x00"))[0]
|
||||
@@ -0,0 +1,98 @@
|
||||
from ._compat import PY2
|
||||
from ._compat import text_type
|
||||
|
||||
|
||||
class BadData(Exception):
|
||||
"""Raised if bad data of any sort was encountered. This is the base
|
||||
for all exceptions that itsdangerous defines.
|
||||
|
||||
.. versionadded:: 0.15
|
||||
"""
|
||||
|
||||
message = None
|
||||
|
||||
def __init__(self, message):
|
||||
super(BadData, self).__init__(self, message)
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return text_type(self.message)
|
||||
|
||||
if PY2:
|
||||
__unicode__ = __str__
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__().encode("utf-8")
|
||||
|
||||
|
||||
class BadSignature(BadData):
|
||||
"""Raised if a signature does not match."""
|
||||
|
||||
def __init__(self, message, payload=None):
|
||||
BadData.__init__(self, message)
|
||||
|
||||
#: The payload that failed the signature test. In some
|
||||
#: situations you might still want to inspect this, even if
|
||||
#: you know it was tampered with.
|
||||
#:
|
||||
#: .. versionadded:: 0.14
|
||||
self.payload = payload
|
||||
|
||||
|
||||
class BadTimeSignature(BadSignature):
|
||||
"""Raised if a time-based signature is invalid. This is a subclass
|
||||
of :class:`BadSignature`.
|
||||
"""
|
||||
|
||||
def __init__(self, message, payload=None, date_signed=None):
|
||||
BadSignature.__init__(self, message, payload)
|
||||
|
||||
#: If the signature expired this exposes the date of when the
|
||||
#: signature was created. This can be helpful in order to
|
||||
#: tell the user how long a link has been gone stale.
|
||||
#:
|
||||
#: .. versionadded:: 0.14
|
||||
self.date_signed = date_signed
|
||||
|
||||
|
||||
class SignatureExpired(BadTimeSignature):
|
||||
"""Raised if a signature timestamp is older than ``max_age``. This
|
||||
is a subclass of :exc:`BadTimeSignature`.
|
||||
"""
|
||||
|
||||
|
||||
class BadHeader(BadSignature):
|
||||
"""Raised if a signed header is invalid in some form. This only
|
||||
happens for serializers that have a header that goes with the
|
||||
signature.
|
||||
|
||||
.. versionadded:: 0.24
|
||||
"""
|
||||
|
||||
def __init__(self, message, payload=None, header=None, original_error=None):
|
||||
BadSignature.__init__(self, message, payload)
|
||||
|
||||
#: If the header is actually available but just malformed it
|
||||
#: might be stored here.
|
||||
self.header = header
|
||||
|
||||
#: If available, the error that indicates why the payload was
|
||||
#: not valid. This might be ``None``.
|
||||
self.original_error = original_error
|
||||
|
||||
|
||||
class BadPayload(BadData):
|
||||
"""Raised if a payload is invalid. This could happen if the payload
|
||||
is loaded despite an invalid signature, or if there is a mismatch
|
||||
between the serializer and deserializer. The original exception
|
||||
that occurred during loading is stored on as :attr:`original_error`.
|
||||
|
||||
.. versionadded:: 0.15
|
||||
"""
|
||||
|
||||
def __init__(self, message, original_error=None):
|
||||
BadData.__init__(self, message)
|
||||
|
||||
#: If available, the error that indicates why the payload was
|
||||
#: not valid. This might be ``None``.
|
||||
self.original_error = original_error
|
||||
@@ -0,0 +1,218 @@
|
||||
import hashlib
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from ._compat import number_types
|
||||
from ._json import _CompactJSON
|
||||
from ._json import json
|
||||
from .encoding import base64_decode
|
||||
from .encoding import base64_encode
|
||||
from .encoding import want_bytes
|
||||
from .exc import BadData
|
||||
from .exc import BadHeader
|
||||
from .exc import BadPayload
|
||||
from .exc import BadSignature
|
||||
from .exc import SignatureExpired
|
||||
from .serializer import Serializer
|
||||
from .signer import HMACAlgorithm
|
||||
from .signer import NoneAlgorithm
|
||||
|
||||
|
||||
class JSONWebSignatureSerializer(Serializer):
|
||||
"""This serializer implements JSON Web Signature (JWS) support. Only
|
||||
supports the JWS Compact Serialization.
|
||||
"""
|
||||
|
||||
jws_algorithms = {
|
||||
"HS256": HMACAlgorithm(hashlib.sha256),
|
||||
"HS384": HMACAlgorithm(hashlib.sha384),
|
||||
"HS512": HMACAlgorithm(hashlib.sha512),
|
||||
"none": NoneAlgorithm(),
|
||||
}
|
||||
|
||||
#: The default algorithm to use for signature generation
|
||||
default_algorithm = "HS512"
|
||||
|
||||
default_serializer = _CompactJSON
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
secret_key,
|
||||
salt=None,
|
||||
serializer=None,
|
||||
serializer_kwargs=None,
|
||||
signer=None,
|
||||
signer_kwargs=None,
|
||||
algorithm_name=None,
|
||||
):
|
||||
Serializer.__init__(
|
||||
self,
|
||||
secret_key=secret_key,
|
||||
salt=salt,
|
||||
serializer=serializer,
|
||||
serializer_kwargs=serializer_kwargs,
|
||||
signer=signer,
|
||||
signer_kwargs=signer_kwargs,
|
||||
)
|
||||
if algorithm_name is None:
|
||||
algorithm_name = self.default_algorithm
|
||||
self.algorithm_name = algorithm_name
|
||||
self.algorithm = self.make_algorithm(algorithm_name)
|
||||
|
||||
def load_payload(self, payload, serializer=None, return_header=False):
|
||||
payload = want_bytes(payload)
|
||||
if b"." not in payload:
|
||||
raise BadPayload('No "." found in value')
|
||||
base64d_header, base64d_payload = payload.split(b".", 1)
|
||||
try:
|
||||
json_header = base64_decode(base64d_header)
|
||||
except Exception as e:
|
||||
raise BadHeader(
|
||||
"Could not base64 decode the header because of an exception",
|
||||
original_error=e,
|
||||
)
|
||||
try:
|
||||
json_payload = base64_decode(base64d_payload)
|
||||
except Exception as e:
|
||||
raise BadPayload(
|
||||
"Could not base64 decode the payload because of an exception",
|
||||
original_error=e,
|
||||
)
|
||||
try:
|
||||
header = Serializer.load_payload(self, json_header, serializer=json)
|
||||
except BadData as e:
|
||||
raise BadHeader(
|
||||
"Could not unserialize header because it was malformed",
|
||||
original_error=e,
|
||||
)
|
||||
if not isinstance(header, dict):
|
||||
raise BadHeader("Header payload is not a JSON object", header=header)
|
||||
payload = Serializer.load_payload(self, json_payload, serializer=serializer)
|
||||
if return_header:
|
||||
return payload, header
|
||||
return payload
|
||||
|
||||
def dump_payload(self, header, obj):
|
||||
base64d_header = base64_encode(
|
||||
self.serializer.dumps(header, **self.serializer_kwargs)
|
||||
)
|
||||
base64d_payload = base64_encode(
|
||||
self.serializer.dumps(obj, **self.serializer_kwargs)
|
||||
)
|
||||
return base64d_header + b"." + base64d_payload
|
||||
|
||||
def make_algorithm(self, algorithm_name):
|
||||
try:
|
||||
return self.jws_algorithms[algorithm_name]
|
||||
except KeyError:
|
||||
raise NotImplementedError("Algorithm not supported")
|
||||
|
||||
def make_signer(self, salt=None, algorithm=None):
|
||||
if salt is None:
|
||||
salt = self.salt
|
||||
key_derivation = "none" if salt is None else None
|
||||
if algorithm is None:
|
||||
algorithm = self.algorithm
|
||||
return self.signer(
|
||||
self.secret_key,
|
||||
salt=salt,
|
||||
sep=".",
|
||||
key_derivation=key_derivation,
|
||||
algorithm=algorithm,
|
||||
)
|
||||
|
||||
def make_header(self, header_fields):
|
||||
header = header_fields.copy() if header_fields else {}
|
||||
header["alg"] = self.algorithm_name
|
||||
return header
|
||||
|
||||
def dumps(self, obj, salt=None, header_fields=None):
|
||||
"""Like :meth:`.Serializer.dumps` but creates a JSON Web
|
||||
Signature. It also allows for specifying additional fields to be
|
||||
included in the JWS header.
|
||||
"""
|
||||
header = self.make_header(header_fields)
|
||||
signer = self.make_signer(salt, self.algorithm)
|
||||
return signer.sign(self.dump_payload(header, obj))
|
||||
|
||||
def loads(self, s, salt=None, return_header=False):
|
||||
"""Reverse of :meth:`dumps`. If requested via ``return_header``
|
||||
it will return a tuple of payload and header.
|
||||
"""
|
||||
payload, header = self.load_payload(
|
||||
self.make_signer(salt, self.algorithm).unsign(want_bytes(s)),
|
||||
return_header=True,
|
||||
)
|
||||
if header.get("alg") != self.algorithm_name:
|
||||
raise BadHeader("Algorithm mismatch", header=header, payload=payload)
|
||||
if return_header:
|
||||
return payload, header
|
||||
return payload
|
||||
|
||||
def loads_unsafe(self, s, salt=None, return_header=False):
|
||||
kwargs = {"return_header": return_header}
|
||||
return self._loads_unsafe_impl(s, salt, kwargs, kwargs)
|
||||
|
||||
|
||||
class TimedJSONWebSignatureSerializer(JSONWebSignatureSerializer):
|
||||
"""Works like the regular :class:`JSONWebSignatureSerializer` but
|
||||
also records the time of the signing and can be used to expire
|
||||
signatures.
|
||||
|
||||
JWS currently does not specify this behavior but it mentions a
|
||||
possible extension like this in the spec. Expiry date is encoded
|
||||
into the header similar to what's specified in `draft-ietf-oauth
|
||||
-json-web-token <http://self-issued.info/docs/draft-ietf-oauth-json
|
||||
-web-token.html#expDef>`_.
|
||||
"""
|
||||
|
||||
DEFAULT_EXPIRES_IN = 3600
|
||||
|
||||
def __init__(self, secret_key, expires_in=None, **kwargs):
|
||||
JSONWebSignatureSerializer.__init__(self, secret_key, **kwargs)
|
||||
if expires_in is None:
|
||||
expires_in = self.DEFAULT_EXPIRES_IN
|
||||
self.expires_in = expires_in
|
||||
|
||||
def make_header(self, header_fields):
|
||||
header = JSONWebSignatureSerializer.make_header(self, header_fields)
|
||||
iat = self.now()
|
||||
exp = iat + self.expires_in
|
||||
header["iat"] = iat
|
||||
header["exp"] = exp
|
||||
return header
|
||||
|
||||
def loads(self, s, salt=None, return_header=False):
|
||||
payload, header = JSONWebSignatureSerializer.loads(
|
||||
self, s, salt, return_header=True
|
||||
)
|
||||
|
||||
if "exp" not in header:
|
||||
raise BadSignature("Missing expiry date", payload=payload)
|
||||
|
||||
int_date_error = BadHeader("Expiry date is not an IntDate", payload=payload)
|
||||
try:
|
||||
header["exp"] = int(header["exp"])
|
||||
except ValueError:
|
||||
raise int_date_error
|
||||
if header["exp"] < 0:
|
||||
raise int_date_error
|
||||
|
||||
if header["exp"] < self.now():
|
||||
raise SignatureExpired(
|
||||
"Signature expired",
|
||||
payload=payload,
|
||||
date_signed=self.get_issue_date(header),
|
||||
)
|
||||
|
||||
if return_header:
|
||||
return payload, header
|
||||
return payload
|
||||
|
||||
def get_issue_date(self, header):
|
||||
rv = header.get("iat")
|
||||
if isinstance(rv, number_types):
|
||||
return datetime.utcfromtimestamp(int(rv))
|
||||
|
||||
def now(self):
|
||||
return int(time.time())
|
||||
@@ -0,0 +1,233 @@
|
||||
import hashlib
|
||||
|
||||
from ._compat import text_type
|
||||
from ._json import json
|
||||
from .encoding import want_bytes
|
||||
from .exc import BadPayload
|
||||
from .exc import BadSignature
|
||||
from .signer import Signer
|
||||
|
||||
|
||||
def is_text_serializer(serializer):
|
||||
"""Checks whether a serializer generates text or binary."""
|
||||
return isinstance(serializer.dumps({}), text_type)
|
||||
|
||||
|
||||
class Serializer(object):
|
||||
"""This class provides a serialization interface on top of the
|
||||
signer. It provides a similar API to json/pickle and other modules
|
||||
but is structured differently internally. If you want to change the
|
||||
underlying implementation for parsing and loading you have to
|
||||
override the :meth:`load_payload` and :meth:`dump_payload`
|
||||
functions.
|
||||
|
||||
This implementation uses simplejson if available for dumping and
|
||||
loading and will fall back to the standard library's json module if
|
||||
it's not available.
|
||||
|
||||
You do not need to subclass this class in order to switch out or
|
||||
customize the :class:`.Signer`. You can instead pass a different
|
||||
class to the constructor as well as keyword arguments as a dict that
|
||||
should be forwarded.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
s = Serializer(signer_kwargs={'key_derivation': 'hmac'})
|
||||
|
||||
You may want to upgrade the signing parameters without invalidating
|
||||
existing signatures that are in use. Fallback signatures can be
|
||||
given that will be tried if unsigning with the current signer fails.
|
||||
|
||||
Fallback signers can be defined by providing a list of
|
||||
``fallback_signers``. Each item can be one of the following: a
|
||||
signer class (which is instantiated with ``signer_kwargs``,
|
||||
``salt``, and ``secret_key``), a tuple
|
||||
``(signer_class, signer_kwargs)``, or a dict of ``signer_kwargs``.
|
||||
|
||||
For example, this is a serializer that signs using SHA-512, but will
|
||||
unsign using either SHA-512 or SHA1:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
s = Serializer(
|
||||
signer_kwargs={"digest_method": hashlib.sha512},
|
||||
fallback_signers=[{"digest_method": hashlib.sha1}]
|
||||
)
|
||||
|
||||
.. versionchanged:: 0.14:
|
||||
The ``signer`` and ``signer_kwargs`` parameters were added to
|
||||
the constructor.
|
||||
|
||||
.. versionchanged:: 1.1.0:
|
||||
Added support for ``fallback_signers`` and configured a default
|
||||
SHA-512 fallback. This fallback is for users who used the yanked
|
||||
1.0.0 release which defaulted to SHA-512.
|
||||
"""
|
||||
|
||||
#: If a serializer module or class is not passed to the constructor
|
||||
#: this one is picked up. This currently defaults to :mod:`json`.
|
||||
default_serializer = json
|
||||
|
||||
#: The default :class:`Signer` class that is being used by this
|
||||
#: serializer.
|
||||
#:
|
||||
#: .. versionadded:: 0.14
|
||||
default_signer = Signer
|
||||
|
||||
#: The default fallback signers.
|
||||
default_fallback_signers = [{"digest_method": hashlib.sha512}]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
secret_key,
|
||||
salt=b"itsdangerous",
|
||||
serializer=None,
|
||||
serializer_kwargs=None,
|
||||
signer=None,
|
||||
signer_kwargs=None,
|
||||
fallback_signers=None,
|
||||
):
|
||||
self.secret_key = want_bytes(secret_key)
|
||||
self.salt = want_bytes(salt)
|
||||
if serializer is None:
|
||||
serializer = self.default_serializer
|
||||
self.serializer = serializer
|
||||
self.is_text_serializer = is_text_serializer(serializer)
|
||||
if signer is None:
|
||||
signer = self.default_signer
|
||||
self.signer = signer
|
||||
self.signer_kwargs = signer_kwargs or {}
|
||||
if fallback_signers is None:
|
||||
fallback_signers = list(self.default_fallback_signers or ())
|
||||
self.fallback_signers = fallback_signers
|
||||
self.serializer_kwargs = serializer_kwargs or {}
|
||||
|
||||
def load_payload(self, payload, serializer=None):
|
||||
"""Loads the encoded object. This function raises
|
||||
:class:`.BadPayload` if the payload is not valid. The
|
||||
``serializer`` parameter can be used to override the serializer
|
||||
stored on the class. The encoded ``payload`` should always be
|
||||
bytes.
|
||||
"""
|
||||
if serializer is None:
|
||||
serializer = self.serializer
|
||||
is_text = self.is_text_serializer
|
||||
else:
|
||||
is_text = is_text_serializer(serializer)
|
||||
try:
|
||||
if is_text:
|
||||
payload = payload.decode("utf-8")
|
||||
return serializer.loads(payload)
|
||||
except Exception as e:
|
||||
raise BadPayload(
|
||||
"Could not load the payload because an exception"
|
||||
" occurred on unserializing the data.",
|
||||
original_error=e,
|
||||
)
|
||||
|
||||
def dump_payload(self, obj):
|
||||
"""Dumps the encoded object. The return value is always bytes.
|
||||
If the internal serializer returns text, the value will be
|
||||
encoded as UTF-8.
|
||||
"""
|
||||
return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs))
|
||||
|
||||
def make_signer(self, salt=None):
|
||||
"""Creates a new instance of the signer to be used. The default
|
||||
implementation uses the :class:`.Signer` base class.
|
||||
"""
|
||||
if salt is None:
|
||||
salt = self.salt
|
||||
return self.signer(self.secret_key, salt=salt, **self.signer_kwargs)
|
||||
|
||||
def iter_unsigners(self, salt=None):
|
||||
"""Iterates over all signers to be tried for unsigning. Starts
|
||||
with the configured signer, then constructs each signer
|
||||
specified in ``fallback_signers``.
|
||||
"""
|
||||
if salt is None:
|
||||
salt = self.salt
|
||||
yield self.make_signer(salt)
|
||||
for fallback in self.fallback_signers:
|
||||
if type(fallback) is dict:
|
||||
kwargs = fallback
|
||||
fallback = self.signer
|
||||
elif type(fallback) is tuple:
|
||||
fallback, kwargs = fallback
|
||||
else:
|
||||
kwargs = self.signer_kwargs
|
||||
yield fallback(self.secret_key, salt=salt, **kwargs)
|
||||
|
||||
def dumps(self, obj, salt=None):
|
||||
"""Returns a signed string serialized with the internal
|
||||
serializer. The return value can be either a byte or unicode
|
||||
string depending on the format of the internal serializer.
|
||||
"""
|
||||
payload = want_bytes(self.dump_payload(obj))
|
||||
rv = self.make_signer(salt).sign(payload)
|
||||
if self.is_text_serializer:
|
||||
rv = rv.decode("utf-8")
|
||||
return rv
|
||||
|
||||
def dump(self, obj, f, salt=None):
|
||||
"""Like :meth:`dumps` but dumps into a file. The file handle has
|
||||
to be compatible with what the internal serializer expects.
|
||||
"""
|
||||
f.write(self.dumps(obj, salt))
|
||||
|
||||
def loads(self, s, salt=None):
|
||||
"""Reverse of :meth:`dumps`. Raises :exc:`.BadSignature` if the
|
||||
signature validation fails.
|
||||
"""
|
||||
s = want_bytes(s)
|
||||
last_exception = None
|
||||
for signer in self.iter_unsigners(salt):
|
||||
try:
|
||||
return self.load_payload(signer.unsign(s))
|
||||
except BadSignature as err:
|
||||
last_exception = err
|
||||
raise last_exception
|
||||
|
||||
def load(self, f, salt=None):
|
||||
"""Like :meth:`loads` but loads from a file."""
|
||||
return self.loads(f.read(), salt)
|
||||
|
||||
def loads_unsafe(self, s, salt=None):
|
||||
"""Like :meth:`loads` but without verifying the signature. This
|
||||
is potentially very dangerous to use depending on how your
|
||||
serializer works. The return value is ``(signature_valid,
|
||||
payload)`` instead of just the payload. The first item will be a
|
||||
boolean that indicates if the signature is valid. This function
|
||||
never fails.
|
||||
|
||||
Use it for debugging only and if you know that your serializer
|
||||
module is not exploitable (for example, do not use it with a
|
||||
pickle serializer).
|
||||
|
||||
.. versionadded:: 0.15
|
||||
"""
|
||||
return self._loads_unsafe_impl(s, salt)
|
||||
|
||||
def _loads_unsafe_impl(self, s, salt, load_kwargs=None, load_payload_kwargs=None):
|
||||
"""Low level helper function to implement :meth:`loads_unsafe`
|
||||
in serializer subclasses.
|
||||
"""
|
||||
try:
|
||||
return True, self.loads(s, salt=salt, **(load_kwargs or {}))
|
||||
except BadSignature as e:
|
||||
if e.payload is None:
|
||||
return False, None
|
||||
try:
|
||||
return (
|
||||
False,
|
||||
self.load_payload(e.payload, **(load_payload_kwargs or {})),
|
||||
)
|
||||
except BadPayload:
|
||||
return False, None
|
||||
|
||||
def load_unsafe(self, f, *args, **kwargs):
|
||||
"""Like :meth:`loads_unsafe` but loads from a file.
|
||||
|
||||
.. versionadded:: 0.15
|
||||
"""
|
||||
return self.loads_unsafe(f.read(), *args, **kwargs)
|
||||
@@ -0,0 +1,179 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from ._compat import constant_time_compare
|
||||
from .encoding import _base64_alphabet
|
||||
from .encoding import base64_decode
|
||||
from .encoding import base64_encode
|
||||
from .encoding import want_bytes
|
||||
from .exc import BadSignature
|
||||
|
||||
|
||||
class SigningAlgorithm(object):
|
||||
"""Subclasses must implement :meth:`get_signature` to provide
|
||||
signature generation functionality.
|
||||
"""
|
||||
|
||||
def get_signature(self, key, value):
|
||||
"""Returns the signature for the given key and value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def verify_signature(self, key, value, sig):
|
||||
"""Verifies the given signature matches the expected
|
||||
signature.
|
||||
"""
|
||||
return constant_time_compare(sig, self.get_signature(key, value))
|
||||
|
||||
|
||||
class NoneAlgorithm(SigningAlgorithm):
|
||||
"""Provides an algorithm that does not perform any signing and
|
||||
returns an empty signature.
|
||||
"""
|
||||
|
||||
def get_signature(self, key, value):
|
||||
return b""
|
||||
|
||||
|
||||
class HMACAlgorithm(SigningAlgorithm):
|
||||
"""Provides signature generation using HMACs."""
|
||||
|
||||
#: The digest method to use with the MAC algorithm. This defaults to
|
||||
#: SHA1, but can be changed to any other function in the hashlib
|
||||
#: module.
|
||||
default_digest_method = staticmethod(hashlib.sha1)
|
||||
|
||||
def __init__(self, digest_method=None):
|
||||
if digest_method is None:
|
||||
digest_method = self.default_digest_method
|
||||
self.digest_method = digest_method
|
||||
|
||||
def get_signature(self, key, value):
|
||||
mac = hmac.new(key, msg=value, digestmod=self.digest_method)
|
||||
return mac.digest()
|
||||
|
||||
|
||||
class Signer(object):
|
||||
"""This class can sign and unsign bytes, validating the signature
|
||||
provided.
|
||||
|
||||
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
|
||||
where the same signed value in one part can mean something different
|
||||
in another part is a security risk.
|
||||
|
||||
See :ref:`the-salt` for an example of what the salt is doing and how
|
||||
you can utilize it.
|
||||
|
||||
.. versionadded:: 0.14
|
||||
``key_derivation`` and ``digest_method`` were added as arguments
|
||||
to the class constructor.
|
||||
|
||||
.. versionadded:: 0.18
|
||||
``algorithm`` was added as an argument to the class constructor.
|
||||
"""
|
||||
|
||||
#: The digest method to use for the signer. This defaults to
|
||||
#: SHA1 but can be changed to any other function in the hashlib
|
||||
#: module.
|
||||
#:
|
||||
#: .. versionadded:: 0.14
|
||||
default_digest_method = staticmethod(hashlib.sha1)
|
||||
|
||||
#: Controls how the key is derived. The default is Django-style
|
||||
#: concatenation. Possible values are ``concat``, ``django-concat``
|
||||
#: and ``hmac``. This is used for deriving a key from the secret key
|
||||
#: with an added salt.
|
||||
#:
|
||||
#: .. versionadded:: 0.14
|
||||
default_key_derivation = "django-concat"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
secret_key,
|
||||
salt=None,
|
||||
sep=".",
|
||||
key_derivation=None,
|
||||
digest_method=None,
|
||||
algorithm=None,
|
||||
):
|
||||
self.secret_key = want_bytes(secret_key)
|
||||
self.sep = want_bytes(sep)
|
||||
if self.sep in _base64_alphabet:
|
||||
raise ValueError(
|
||||
"The given separator cannot be used because it may be"
|
||||
" contained in the signature itself. Alphanumeric"
|
||||
" characters and `-_=` must not be used."
|
||||
)
|
||||
self.salt = "itsdangerous.Signer" if salt is None else salt
|
||||
if key_derivation is None:
|
||||
key_derivation = self.default_key_derivation
|
||||
self.key_derivation = key_derivation
|
||||
if digest_method is None:
|
||||
digest_method = self.default_digest_method
|
||||
self.digest_method = digest_method
|
||||
if algorithm is None:
|
||||
algorithm = HMACAlgorithm(self.digest_method)
|
||||
self.algorithm = algorithm
|
||||
|
||||
def derive_key(self):
|
||||
"""This method is called to derive the key. The default key
|
||||
derivation choices can be overridden here. Key derivation is not
|
||||
intended to be used as a security method to make a complex key
|
||||
out of a short password. Instead you should use large random
|
||||
secret keys.
|
||||
"""
|
||||
salt = want_bytes(self.salt)
|
||||
if self.key_derivation == "concat":
|
||||
return self.digest_method(salt + self.secret_key).digest()
|
||||
elif self.key_derivation == "django-concat":
|
||||
return self.digest_method(salt + b"signer" + self.secret_key).digest()
|
||||
elif self.key_derivation == "hmac":
|
||||
mac = hmac.new(self.secret_key, digestmod=self.digest_method)
|
||||
mac.update(salt)
|
||||
return mac.digest()
|
||||
elif self.key_derivation == "none":
|
||||
return self.secret_key
|
||||
else:
|
||||
raise TypeError("Unknown key derivation method")
|
||||
|
||||
def get_signature(self, value):
|
||||
"""Returns the signature for the given value."""
|
||||
value = want_bytes(value)
|
||||
key = self.derive_key()
|
||||
sig = self.algorithm.get_signature(key, value)
|
||||
return base64_encode(sig)
|
||||
|
||||
def sign(self, value):
|
||||
"""Signs the given string."""
|
||||
return want_bytes(value) + want_bytes(self.sep) + self.get_signature(value)
|
||||
|
||||
def verify_signature(self, value, sig):
|
||||
"""Verifies the signature for the given value."""
|
||||
key = self.derive_key()
|
||||
try:
|
||||
sig = base64_decode(sig)
|
||||
except Exception:
|
||||
return False
|
||||
return self.algorithm.verify_signature(key, value, sig)
|
||||
|
||||
def unsign(self, signed_value):
|
||||
"""Unsigns the given string."""
|
||||
signed_value = want_bytes(signed_value)
|
||||
sep = want_bytes(self.sep)
|
||||
if sep not in signed_value:
|
||||
raise BadSignature("No %r found in value" % self.sep)
|
||||
value, sig = signed_value.rsplit(sep, 1)
|
||||
if self.verify_signature(value, sig):
|
||||
return value
|
||||
raise BadSignature("Signature %r does not match" % sig, payload=value)
|
||||
|
||||
def validate(self, signed_value):
|
||||
"""Only validates the given signed value. Returns ``True`` if
|
||||
the signature exists and is valid.
|
||||
"""
|
||||
try:
|
||||
self.unsign(signed_value)
|
||||
return True
|
||||
except BadSignature:
|
||||
return False
|
||||
@@ -0,0 +1,147 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from ._compat import text_type
|
||||
from .encoding import base64_decode
|
||||
from .encoding import base64_encode
|
||||
from .encoding import bytes_to_int
|
||||
from .encoding import int_to_bytes
|
||||
from .encoding import want_bytes
|
||||
from .exc import BadSignature
|
||||
from .exc import BadTimeSignature
|
||||
from .exc import SignatureExpired
|
||||
from .serializer import Serializer
|
||||
from .signer import Signer
|
||||
|
||||
|
||||
class TimestampSigner(Signer):
|
||||
"""Works like the regular :class:`.Signer` but also records the time
|
||||
of the signing and can be used to expire signatures. The
|
||||
:meth:`unsign` method can raise :exc:`.SignatureExpired` if the
|
||||
unsigning failed because the signature is expired.
|
||||
"""
|
||||
|
||||
def get_timestamp(self):
|
||||
"""Returns the current timestamp. The function must return an
|
||||
integer.
|
||||
"""
|
||||
return int(time.time())
|
||||
|
||||
def timestamp_to_datetime(self, ts):
|
||||
"""Used to convert the timestamp from :meth:`get_timestamp` into
|
||||
a datetime object.
|
||||
"""
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
def sign(self, value):
|
||||
"""Signs the given string and also attaches time information."""
|
||||
value = want_bytes(value)
|
||||
timestamp = base64_encode(int_to_bytes(self.get_timestamp()))
|
||||
sep = want_bytes(self.sep)
|
||||
value = value + sep + timestamp
|
||||
return value + sep + self.get_signature(value)
|
||||
|
||||
def unsign(self, value, max_age=None, return_timestamp=False):
|
||||
"""Works like the regular :meth:`.Signer.unsign` but can also
|
||||
validate the time. See the base docstring of the class for
|
||||
the general behavior. If ``return_timestamp`` is ``True`` the
|
||||
timestamp of the signature will be returned as a naive
|
||||
:class:`datetime.datetime` object in UTC.
|
||||
"""
|
||||
try:
|
||||
result = Signer.unsign(self, value)
|
||||
sig_error = None
|
||||
except BadSignature as e:
|
||||
sig_error = e
|
||||
result = e.payload or b""
|
||||
sep = want_bytes(self.sep)
|
||||
|
||||
# If there is no timestamp in the result there is something
|
||||
# seriously wrong. In case there was a signature error, we raise
|
||||
# that one directly, otherwise we have a weird situation in
|
||||
# which we shouldn't have come except someone uses a time-based
|
||||
# serializer on non-timestamp data, so catch that.
|
||||
if sep not in result:
|
||||
if sig_error:
|
||||
raise sig_error
|
||||
raise BadTimeSignature("timestamp missing", payload=result)
|
||||
|
||||
value, timestamp = result.rsplit(sep, 1)
|
||||
try:
|
||||
timestamp = bytes_to_int(base64_decode(timestamp))
|
||||
except Exception:
|
||||
timestamp = None
|
||||
|
||||
# Signature is *not* okay. Raise a proper error now that we have
|
||||
# split the value and the timestamp.
|
||||
if sig_error is not None:
|
||||
raise BadTimeSignature(
|
||||
text_type(sig_error), payload=value, date_signed=timestamp
|
||||
)
|
||||
|
||||
# Signature was okay but the timestamp is actually not there or
|
||||
# malformed. Should not happen, but we handle it anyway.
|
||||
if timestamp is None:
|
||||
raise BadTimeSignature("Malformed timestamp", payload=value)
|
||||
|
||||
# Check timestamp is not older than max_age
|
||||
if max_age is not None:
|
||||
age = self.get_timestamp() - timestamp
|
||||
if age > max_age:
|
||||
raise SignatureExpired(
|
||||
"Signature age %s > %s seconds" % (age, max_age),
|
||||
payload=value,
|
||||
date_signed=self.timestamp_to_datetime(timestamp),
|
||||
)
|
||||
|
||||
if return_timestamp:
|
||||
return value, self.timestamp_to_datetime(timestamp)
|
||||
return value
|
||||
|
||||
def validate(self, signed_value, max_age=None):
|
||||
"""Only validates the given signed value. Returns ``True`` if
|
||||
the signature exists and is valid."""
|
||||
try:
|
||||
self.unsign(signed_value, max_age=max_age)
|
||||
return True
|
||||
except BadSignature:
|
||||
return False
|
||||
|
||||
|
||||
class TimedSerializer(Serializer):
|
||||
"""Uses :class:`TimestampSigner` instead of the default
|
||||
:class:`.Signer`.
|
||||
"""
|
||||
|
||||
default_signer = TimestampSigner
|
||||
|
||||
def loads(self, s, max_age=None, return_timestamp=False, salt=None):
|
||||
"""Reverse of :meth:`dumps`, raises :exc:`.BadSignature` if the
|
||||
signature validation fails. If a ``max_age`` is provided it will
|
||||
ensure the signature is not older than that time in seconds. In
|
||||
case the signature is outdated, :exc:`.SignatureExpired` is
|
||||
raised. All arguments are forwarded to the signer's
|
||||
:meth:`~TimestampSigner.unsign` method.
|
||||
"""
|
||||
s = want_bytes(s)
|
||||
last_exception = None
|
||||
for signer in self.iter_unsigners(salt):
|
||||
try:
|
||||
base64d, timestamp = signer.unsign(s, max_age, return_timestamp=True)
|
||||
payload = self.load_payload(base64d)
|
||||
if return_timestamp:
|
||||
return payload, timestamp
|
||||
return payload
|
||||
# If we get a signature expired it means we could read the
|
||||
# signature but it's invalid. In that case we do not want to
|
||||
# try the next signer.
|
||||
except SignatureExpired:
|
||||
raise
|
||||
except BadSignature as err:
|
||||
last_exception = err
|
||||
raise last_exception
|
||||
|
||||
def loads_unsafe(self, s, max_age=None, salt=None):
|
||||
load_kwargs = {"max_age": max_age}
|
||||
load_payload_kwargs = {}
|
||||
return self._loads_unsafe_impl(s, salt, load_kwargs, load_payload_kwargs)
|
||||
@@ -0,0 +1,65 @@
|
||||
import zlib
|
||||
|
||||
from ._json import _CompactJSON
|
||||
from .encoding import base64_decode
|
||||
from .encoding import base64_encode
|
||||
from .exc import BadPayload
|
||||
from .serializer import Serializer
|
||||
from .timed import TimedSerializer
|
||||
|
||||
|
||||
class URLSafeSerializerMixin(object):
|
||||
"""Mixed in with a regular serializer it will attempt to zlib
|
||||
compress the string to make it shorter if necessary. It will also
|
||||
base64 encode the string so that it can safely be placed in a URL.
|
||||
"""
|
||||
|
||||
default_serializer = _CompactJSON
|
||||
|
||||
def load_payload(self, payload, *args, **kwargs):
|
||||
decompress = False
|
||||
if payload.startswith(b"."):
|
||||
payload = payload[1:]
|
||||
decompress = True
|
||||
try:
|
||||
json = base64_decode(payload)
|
||||
except Exception as e:
|
||||
raise BadPayload(
|
||||
"Could not base64 decode the payload because of an exception",
|
||||
original_error=e,
|
||||
)
|
||||
if decompress:
|
||||
try:
|
||||
json = zlib.decompress(json)
|
||||
except Exception as e:
|
||||
raise BadPayload(
|
||||
"Could not zlib decompress the payload before decoding the payload",
|
||||
original_error=e,
|
||||
)
|
||||
return super(URLSafeSerializerMixin, self).load_payload(json, *args, **kwargs)
|
||||
|
||||
def dump_payload(self, obj):
|
||||
json = super(URLSafeSerializerMixin, self).dump_payload(obj)
|
||||
is_compressed = False
|
||||
compressed = zlib.compress(json)
|
||||
if len(compressed) < (len(json) - 1):
|
||||
json = compressed
|
||||
is_compressed = True
|
||||
base64d = base64_encode(json)
|
||||
if is_compressed:
|
||||
base64d = b"." + base64d
|
||||
return base64d
|
||||
|
||||
|
||||
class URLSafeSerializer(URLSafeSerializerMixin, Serializer):
|
||||
"""Works like :class:`.Serializer` but dumps and loads into a URL
|
||||
safe string consisting of the upper and lowercase character of the
|
||||
alphabet as well as ``'_'``, ``'-'`` and ``'.'``.
|
||||
"""
|
||||
|
||||
|
||||
class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer):
|
||||
"""Works like :class:`.TimedSerializer` but dumps and loads into a
|
||||
URL safe string consisting of the upper and lowercase character of
|
||||
the alphabet as well as ``'_'``, ``'-'`` and ``'.'``.
|
||||
"""
|
||||
Reference in New Issue
Block a user