diff options
author | Micah Lee <micah@micahflee.com> | 2014-08-19 00:47:00 +0000 |
---|---|---|
committer | Micah Lee <micah@micahflee.com> | 2014-08-19 00:47:00 +0000 |
commit | 826d643ea052e47e23e3810214f57c489a723186 (patch) | |
tree | 11abbdbfabc64613a1a7bb06c6a40395b0297b22 | |
parent | 0f7fa5d214d9341a28f3c0b5a62f716b3128c95c (diff) | |
download | onionshare-826d643ea052e47e23e3810214f57c489a723186.tar.gz onionshare-826d643ea052e47e23e3810214f57c489a723186.zip |
bundled itsdangerous, for tails support
l--------- | itsdangerous | 1 | ||||
-rw-r--r-- | setup.py | 11 | ||||
-rw-r--r-- | tails/lib/itsdangerous/__init__.py | 1 | ||||
-rw-r--r-- | tails/lib/itsdangerous/itsdangerous.py | 872 |
4 files changed, 884 insertions, 1 deletions
diff --git a/itsdangerous b/itsdangerous new file mode 120000 index 00000000..ac5f0e33 --- /dev/null +++ b/itsdangerous @@ -0,0 +1 @@ +tails/lib/itsdangerous
\ No newline at end of file @@ -16,7 +16,16 @@ def file_list(path): files.append(path+'/'+filename) return files +def get_platform(): + p = platform.system() + if p == 'Linux' and platform.uname()[0:2] == ('Linux', 'amnesia'): + p = 'Tails' + return p + version = open('version').read().strip() +packages = ['onionshare', 'onionshare_gui'] +if get_platform() == 'Tails': + packages.append('itsdangerous') setup( name='onionshare', @@ -28,7 +37,7 @@ setup( url='https://github.com/micahflee/onionshare', license="GPL v3", keywords='onion, share, onionshare, tor, anonymous, web server', - packages=['onionshare', 'onionshare_gui'], + packages=packages, include_package_data=True, scripts=['bin/onionshare', 'bin/onionshare-gui'], data_files=[ diff --git a/tails/lib/itsdangerous/__init__.py b/tails/lib/itsdangerous/__init__.py new file mode 100644 index 00000000..a3411558 --- /dev/null +++ b/tails/lib/itsdangerous/__init__.py @@ -0,0 +1 @@ +from itsdangerous import * diff --git a/tails/lib/itsdangerous/itsdangerous.py b/tails/lib/itsdangerous/itsdangerous.py new file mode 100644 index 00000000..228d1012 --- /dev/null +++ b/tails/lib/itsdangerous/itsdangerous.py @@ -0,0 +1,872 @@ +# -*- 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 |