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)