mirror of
https://github.com/onionshare/onionshare.git
synced 2024-12-29 09:16:24 -05:00
873 lines
31 KiB
Python
873 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
itsdangerous
|
|
~~~~~~~~~~~~
|
|
|
|
A module that implements various functions to deal with untrusted
|
|
sources. Mainly useful for web applications.
|
|
|
|
:copyright: (c) 2014 by Armin Ronacher and the Django Software Foundation.
|
|
:license: BSD, see LICENSE for more details.
|
|
"""
|
|
|
|
import sys
|
|
import hmac
|
|
import zlib
|
|
import time
|
|
import base64
|
|
import hashlib
|
|
import operator
|
|
from datetime import datetime
|
|
|
|
|
|
PY2 = sys.version_info[0] == 2
|
|
if PY2:
|
|
from itertools import izip
|
|
text_type = unicode
|
|
int_to_byte = chr
|
|
number_types = (int, long, float)
|
|
else:
|
|
from functools import reduce
|
|
izip = zip
|
|
text_type = str
|
|
int_to_byte = operator.methodcaller('to_bytes', 1, 'big')
|
|
number_types = (int, float)
|
|
|
|
|
|
try:
|
|
import simplejson as json
|
|
except ImportError:
|
|
import json
|
|
|
|
|
|
class _CompactJSON(object):
|
|
"""Wrapper around simplejson that strips whitespace.
|
|
"""
|
|
|
|
def loads(self, payload):
|
|
return json.loads(payload)
|
|
|
|
def dumps(self, obj):
|
|
return json.dumps(obj, separators=(',', ':'))
|
|
|
|
|
|
compact_json = _CompactJSON()
|
|
|
|
|
|
# 2011/01/01 in UTC
|
|
EPOCH = 1293840000
|
|
|
|
|
|
def want_bytes(s, encoding='utf-8', errors='strict'):
|
|
if isinstance(s, text_type):
|
|
s = s.encode(encoding, errors)
|
|
return s
|
|
|
|
|
|
def is_text_serializer(serializer):
|
|
"""Checks wheather a serializer generates text or binary."""
|
|
return isinstance(serializer.dumps({}), text_type)
|
|
|
|
|
|
# Starting with 3.3 the standard library has a c-implementation for
|
|
# constant time string compares.
|
|
_builtin_constant_time_compare = getattr(hmac, 'compare_digest', None)
|
|
|
|
|
|
def constant_time_compare(val1, val2):
|
|
"""Returns 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.
|
|
"""
|
|
if _builtin_constant_time_compare is not None:
|
|
return _builtin_constant_time_compare(val1, val2)
|
|
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
|
|
|
|
|
|
class BadData(Exception):
|
|
"""Raised if bad data of any sort was encountered. This is the
|
|
base for all exceptions that itsdangerous is currently using.
|
|
|
|
.. versionadded:: 0.15
|
|
"""
|
|
message = None
|
|
|
|
def __init__(self, message):
|
|
Exception.__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 BadPayload(BadData):
|
|
"""This error is raised in situations when payload is loaded without
|
|
checking the signature first and an exception happend as a result of
|
|
that. The original exception that caused that will be stored on the
|
|
exception as :attr:`original_error`.
|
|
|
|
This can also happen with a :class:`JSONWebSignatureSerializer` that
|
|
is subclassed and uses a different serializer for the payload than
|
|
the expected one.
|
|
|
|
.. 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
|
|
|
|
|
|
class BadSignature(BadData):
|
|
"""This error is raised if a signature does not match. As of
|
|
itsdangerous 0.14 there are helpful attributes on the exception
|
|
instances. You can also catch down the baseclass :exc:`BadData`.
|
|
"""
|
|
|
|
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 for time based signatures that fail. This is a subclass
|
|
of :class:`BadSignature` so you can catch those down as well.
|
|
"""
|
|
|
|
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 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 SignatureExpired(BadTimeSignature):
|
|
"""Signature timestamp is older than required max_age. This is a
|
|
subclass of :exc:`BadTimeSignature` so you can use the baseclass for
|
|
catching the error.
|
|
"""
|
|
|
|
|
|
def base64_encode(string):
|
|
"""base64 encodes a single bytestring (and is tolerant to getting
|
|
called with a unicode string).
|
|
The resulting bytestring is safe for putting into URLs.
|
|
"""
|
|
string = want_bytes(string)
|
|
return base64.urlsafe_b64encode(string).strip(b'=')
|
|
|
|
|
|
def base64_decode(string):
|
|
"""base64 decodes a single bytestring (and is tolerant to getting
|
|
called with a unicode string).
|
|
The result is also a bytestring.
|
|
"""
|
|
string = want_bytes(string, encoding='ascii', errors='ignore')
|
|
return base64.urlsafe_b64decode(string + b'=' * (-len(string) % 4))
|
|
|
|
|
|
def int_to_bytes(num):
|
|
assert num >= 0
|
|
rv = []
|
|
while num:
|
|
rv.append(int_to_byte(num & 0xff))
|
|
num >>= 8
|
|
return b''.join(reversed(rv))
|
|
|
|
|
|
def bytes_to_int(bytestr):
|
|
return reduce(lambda a, b: a << 8 | b, bytearray(bytestr), 0)
|
|
|
|
|
|
class SigningAlgorithm(object):
|
|
"""Subclasses of `SigningAlgorithm` have to implement `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):
|
|
"""This class provides a algorithm that does not perform any signing and
|
|
returns an empty signature.
|
|
"""
|
|
|
|
def get_signature(self, key, value):
|
|
return b''
|
|
|
|
|
|
class HMACAlgorithm(SigningAlgorithm):
|
|
"""This class provides signature generation using HMACs."""
|
|
|
|
#: The digest method to use with the MAC algorithm. This defaults to sha1
|
|
#: but can be changed for 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 bytes and unsign it and validate 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 for any other function in the hashlib module.
|
|
#:
|
|
#: .. versionchanged:: 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 = sep
|
|
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. If you're unhappy with
|
|
the default key derivation choices you can override them here.
|
|
Keep in mind that the key derivation in itsdangerous 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 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):
|
|
"""Just validates the given signed value. Returns `True` if the
|
|
signature exists and is valid, `False` otherwise."""
|
|
try:
|
|
self.unsign(signed_value)
|
|
return True
|
|
except BadSignature:
|
|
return False
|
|
|
|
|
|
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 unsign
|
|
method can rause a :exc:`SignatureExpired` method if the unsigning
|
|
failed because the signature is expired. This exception is a subclass
|
|
of :exc:`BadSignature`.
|
|
"""
|
|
|
|
def get_timestamp(self):
|
|
"""Returns the current timestamp. This implementation returns the
|
|
seconds since 1/1/2011. The function must return an integer.
|
|
"""
|
|
return int(time.time() - EPOCH)
|
|
|
|
def timestamp_to_datetime(self, ts):
|
|
"""Used to convert the timestamp from `get_timestamp` into a
|
|
datetime object.
|
|
"""
|
|
return datetime.utcfromtimestamp(ts + EPOCH)
|
|
|
|
def sign(self, value):
|
|
"""Signs the given string and also attaches a 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 set to `True`
|
|
the timestamp of the signature will be returned as 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 not sep 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 well. We handle it nonetheless
|
|
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):
|
|
"""Just validates the given signed value. Returns `True` if the
|
|
signature exists and is valid, `False` otherwise."""
|
|
try:
|
|
self.unsign(signed_value, max_age=max_age)
|
|
return True
|
|
except BadSignature:
|
|
return False
|
|
|
|
|
|
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
|
|
slightly differently structured 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.
|
|
|
|
Starting with 0.14 you do not need to subclass this class in order to
|
|
switch out or customer the :class:`Signer`. You can instead also pass a
|
|
different class to the constructor as well as keyword arguments as
|
|
dictionary that should be forwarded::
|
|
|
|
s = Serializer(signer_kwargs={'key_derivation': 'hmac'})
|
|
|
|
.. versionchanged:: 0.14:
|
|
The `signer` and `signer_kwargs` parameters were added to the
|
|
constructor.
|
|
"""
|
|
|
|
#: 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
|
|
|
|
def __init__(self, secret_key, salt=b'itsdangerous', serializer=None,
|
|
signer=None, signer_kwargs=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 {}
|
|
|
|
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 is
|
|
always byte based.
|
|
"""
|
|
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 a
|
|
bytestring. If the internal serializer is text based the value
|
|
will automatically be encoded to utf-8.
|
|
"""
|
|
return want_bytes(self.serializer.dumps(obj))
|
|
|
|
def make_signer(self, salt=None):
|
|
"""A method that creates a new instance of the signer to be used.
|
|
The default implementation uses the :class:`Signer` baseclass.
|
|
"""
|
|
if salt is None:
|
|
salt = self.salt
|
|
return self.signer(self.secret_key, salt=salt, **self.signer_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)
|
|
return self.load_payload(self.make_signer(salt).unsign(s))
|
|
|
|
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_okay, payload)`` instead of
|
|
just the payload. The first item will be a boolean that indicates
|
|
if the signature is okay (``True``) or if it failed. This function
|
|
never fails.
|
|
|
|
Use it for debugging only and if you know that your serializer module
|
|
is not exploitable (eg: 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):
|
|
"""Lowlevel 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)
|
|
|
|
|
|
class TimedSerializer(Serializer):
|
|
"""Uses the :class:`TimestampSigner` instead of the default
|
|
:meth:`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
|
|
which is a subclass of :exc:`BadSignature`. All arguments are
|
|
forwarded to the signer's :meth:`~TimestampSigner.unsign` method.
|
|
"""
|
|
base64d, timestamp = self.make_signer(salt) \
|
|
.unsign(s, max_age, return_timestamp=True)
|
|
payload = self.load_payload(base64d)
|
|
if return_timestamp:
|
|
return payload, timestamp
|
|
return payload
|
|
|
|
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)
|
|
|
|
|
|
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 = 'HS256'
|
|
|
|
default_serializer = compact_json
|
|
|
|
def __init__(self, secret_key, salt=None, serializer=None,
|
|
signer=None, signer_kwargs=None, algorithm_name=None):
|
|
Serializer.__init__(self, secret_key, salt, serializer,
|
|
signer, 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, 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)
|
|
if return_header:
|
|
return payload, header
|
|
return payload
|
|
|
|
def dump_payload(self, header, obj):
|
|
base64d_header = base64_encode(self.serializer.dumps(header))
|
|
base64d_payload = base64_encode(self.serializer.dumps(obj))
|
|
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 possibility
|
|
extension like this in the spec. Expiry date is encoded into the header
|
|
similarily as specified in `draft-ietf-oauth-json-web-token
|
|
<http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#expDef`_.
|
|
|
|
The unsign method can raise a :exc:`SignatureExpired` method if the
|
|
unsigning failed because the signature is expired. This exception is a
|
|
subclass of :exc:`BadSignature`.
|
|
"""
|
|
|
|
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)
|
|
|
|
if not (isinstance(header['exp'], number_types)
|
|
and header['exp'] > 0):
|
|
raise BadSignature('expiry date is not an IntDate',
|
|
payload=payload)
|
|
|
|
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())
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
def load_payload(self, payload):
|
|
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)
|
|
|
|
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 ``'.'``.
|
|
"""
|
|
default_serializer = compact_json
|
|
|
|
|
|
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 ``'.'``.
|
|
"""
|
|
default_serializer = compact_json
|