234 lines
8.5 KiB
Python
234 lines
8.5 KiB
Python
|
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)
|