summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/index.rst13
-rw-r--r--itsdangerous.py148
-rw-r--r--tests.py34
3 files changed, 108 insertions, 87 deletions
diff --git a/docs/index.rst b/docs/index.rst
index 9309211..986e75b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -231,11 +231,14 @@ signature was correct.
Python 3 Notes
--------------
-On Python 3 the return serialized versions are represented as bytes.
-However in many cases (for instance when using the URL safe serializers)
-the data fits into ASCII so it can trivially be converted into unicode
-strings by adding an ``.encode('ascii')``. It also assumes that custom
-serializers operate on bytes as well.
+On Python 3 the interface that itsdangerous provides can be confusing at
+first. Depending on the internal serializer it wraps the return value of
+the functions can alter between unicode strings or bytes objects. The
+internal signer is always byte based.
+
+This is done to allow the module to operate on different serializers
+independent of how they are implemented. The module decides on the
+type of the serializer by doing a test serialization of an empty object.
.. include:: ../CHANGES
diff --git a/itsdangerous.py b/itsdangerous.py
index 909af12..5d36e86 100644
--- a/itsdangerous.py
+++ b/itsdangerous.py
@@ -32,27 +32,24 @@ else:
int_to_byte = operator.methodcaller('to_bytes', 1, 'big')
-# On Python 3 the builtin JSON module does not match the
-# behavior of the JSON module from simplejson. We need to
-# wrap it and encode to utf-8 the return value. This behavior
-# does not match the behavior of the standalone simplejson
-# module. If we find simplejson installed we just use it
-# unchanged.
try:
- import simplejson
+ import simplejson as json
except ImportError:
- import json as simplejson
- if not PY2:
- _json = simplejson
- class simplejson(object):
+ import json
- @staticmethod
- def dumps(obj, *args, **kwargs):
- return _json.dumps(obj, *args, **kwargs).encode('utf-8')
- @staticmethod
- def loads(obj, *args, **kwargs):
- return _json.loads(obj.decode('utf-8'), *args, **kwargs)
+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
@@ -65,6 +62,11 @@ def want_bytes(s, encoding='utf-8', errors='strict'):
return s
+def is_text_serializer(serializer):
+ """Checks weather a serializer generates text or binary."""
+ return isinstance(serializer.dumps({}), text_type)
+
+
def constant_time_compare(val1, val2):
"""Returns True if the two strings are equal, False otherwise.
@@ -230,8 +232,8 @@ class HMACAlgorithm(SigningAlgorithm):
class Signer(object):
- """This class can sign a string and unsign it and validate the
- signature provided.
+ """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
@@ -268,8 +270,7 @@ class Signer(object):
digest_method=None, algorithm=None):
self.secret_key = want_bytes(secret_key)
self.sep = sep
- self.salt = b'itsdangerous.Signer' if salt is None \
- else want_bytes(salt)
+ 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
@@ -287,14 +288,15 @@ class Signer(object):
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(self.salt + self.secret_key).digest()
+ return self.digest_method(salt + self.secret_key).digest()
elif self.key_derivation == 'django-concat':
- return self.digest_method(self.salt + b'signer' +
+ 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(self.salt)
+ mac.update(salt)
return mac.digest()
elif self.key_derivation == 'none':
return self.secret_key
@@ -321,7 +323,7 @@ class Signer(object):
value, sig = signed_value.rsplit(sep, 1)
if constant_time_compare(sig, self.get_signature(value)):
return value
- raise BadSignature('Signature "%s" does not match' % sig,
+ raise BadSignature('Signature %r does not match' % sig,
payload=value)
def validate(self, signed_value):
@@ -429,18 +431,19 @@ class TimestampSigner(Signer):
class Serializer(object):
"""This class provides a serialization interface on top of the
- signer. It provides a similar API to json/pickle/simplejson 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.
+ 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 for dumping and loading.
+ 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::
+ 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'})
@@ -451,7 +454,7 @@ class Serializer(object):
#: 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 = simplejson
+ default_serializer = json
#: The default :class:`Signer` class that is being used by this
#: serializer.
@@ -466,21 +469,26 @@ class Serializer(object):
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 implementation uses simplejson.
- 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.
+ """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:
- payload = want_bytes(payload)
+ 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 '
@@ -488,8 +496,9 @@ class Serializer(object):
original_error=e)
def dump_payload(self, obj):
- """Dumps the encoded object into a bytestring. This implementation
- uses simplejson.
+ """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))
@@ -502,22 +511,29 @@ class Serializer(object):
return self.signer(self.secret_key, salt=salt, **self.signer_kwargs)
def dumps(self, obj, salt=None):
- """Returns URL-safe, signed base64 compressed JSON string.
-
- If compress is True (the default) checks if compressing using zlib can
- save some space. Prepends a '.' to signify compression. This is included
- in the signature, to protect against zip bombs.
+ """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.
"""
- return self.make_signer(salt).sign(self.dump_payload(obj))
+ payload = self.dump_payload(obj)
+ if isinstance(payload, text_type):
+ payload = payload.encode('utf-8')
+ 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."""
+ """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):
@@ -606,13 +622,12 @@ class JSONWebSignatureSerializer(Serializer):
#: The default algorithm to use for signature generation
default_algorithm = 'HS256'
- def __init__(self, secret_key, salt=None, signer_kwargs=None, algorithm_name=None):
- self.secret_key = want_bytes(secret_key)
- self.salt = salt if salt is None \
- else want_bytes(salt)
- self.serializer = compact_json
- self.signer = Signer
- self.signer_kwargs = signer_kwargs or {}
+ 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
@@ -630,7 +645,7 @@ class JSONWebSignatureSerializer(Serializer):
raise BadPayload('Could not base64 decode the payload because of '
'an exception', original_error=e)
header = Serializer.load_payload(self, json_header,
- serializer=simplejson)
+ serializer=json)
if not isinstance(header, dict):
raise BadPayload('Header payload is not a JSON object')
payload = Serializer.load_payload(self, json_payload)
@@ -677,7 +692,7 @@ class JSONWebSignatureSerializer(Serializer):
return a tuple of payload and header.
"""
payload, header = self.load_payload(
- self.make_signer(salt, self.algorithm).unsign(s),
+ self.make_signer(salt, self.algorithm).unsign(want_bytes(s)),
return_header=True)
if header.get('alg') != self.algorithm_name:
raise BadSignature('Algorithm mismatch')
@@ -697,7 +712,6 @@ class URLSafeSerializerMixin(object):
"""
def load_payload(self, payload):
- payload = want_bytes(payload)
decompress = False
if payload[0] == b'.':
payload = payload[1:]
@@ -728,20 +742,6 @@ class URLSafeSerializerMixin(object):
return base64d
-class _CompactJSON(object):
- """Wrapper around simplejson that strips whitespace.
- """
-
- def loads(self, payload):
- return simplejson.loads(payload)
-
- def dumps(self, obj):
- return want_bytes(simplejson.dumps(obj, separators=(',', ':')))
-
-
-compact_json = _CompactJSON()
-
-
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
diff --git a/tests.py b/tests.py
index e1b6585..429a2f4 100644
--- a/tests.py
+++ b/tests.py
@@ -3,7 +3,22 @@ import pickle
import hashlib
import unittest
from datetime import datetime
+
import itsdangerous as idmod
+from itsdangerous import want_bytes, text_type, PY2
+
+
+# Helper function for some unsafe string manipulation on encoded
+# data. This is required for Python 3 but would break on Python 2
+if PY2:
+ def _coerce_string(reference_string, value):
+ return value
+else:
+ def _coerce_string(reference_string, value):
+ assert isinstance(value, text_type), 'rhs needs to be a string'
+ if type(reference_string) != type(value):
+ value = value.encode('utf-8')
+ return value
class SerializerTestCase(unittest.TestCase):
@@ -22,17 +37,18 @@ class SerializerTestCase(unittest.TestCase):
self.assertEqual(o, s.loads(value))
def test_decode_detects_tampering(self):
+ s = self.make_serializer('Test')
+
transforms = (
lambda s: s.upper(),
- lambda s: s + b'a',
- lambda s: b'a' + s[1:],
- lambda s: s.replace(b'.', b''),
+ lambda s: s + _coerce_string(s, 'a'),
+ lambda s: _coerce_string(s, 'a') + s[1:],
+ lambda s: s.replace(_coerce_string(s, '.'), _coerce_string(s, '')),
)
value = {
'foo': 'bar',
'baz': 1,
}
- s = self.make_serializer('Test')
encoded = s.dumps(value)
self.assertEqual(value, s.loads(encoded))
for transform in transforms:
@@ -56,9 +72,10 @@ class SerializerTestCase(unittest.TestCase):
ts = s.dumps(value)
try:
- s.loads(ts + b'x')
+ s.loads(ts + _coerce_string(ts, 'x'))
except idmod.BadSignature as e:
- self.assertEqual(e.payload, ts.rsplit(b'.', 1)[0])
+ self.assertEqual(want_bytes(e.payload),
+ want_bytes(ts).rsplit(b'.', 1)[0])
self.assertEqual(s.load_payload(e.payload), value)
else:
self.fail('Did not get bad signature')
@@ -143,7 +160,8 @@ class TimedSerializerTestCase(SerializerTestCase):
except idmod.SignatureExpired as e:
self.assertEqual(e.date_signed,
datetime.utcfromtimestamp(time.time()))
- self.assertEqual(e.payload, ts.rsplit(b'.', 2)[0])
+ self.assertEqual(want_bytes(e.payload),
+ want_bytes(ts).rsplit(b'.', 2)[0])
self.assertEqual(s.load_payload(e.payload), value)
else:
self.fail('Did not get expiration')
@@ -209,7 +227,7 @@ class URLSafeSerializerMixin(object):
{'a': 'dictionary'}, 42, 42.5)
s = self.make_serializer('Test')
for o in objects:
- value = s.dumps(o)
+ value = want_bytes(s.dumps(o))
self.assertTrue(set(value).issubset(set(allowed)))
self.assertNotEqual(o, value)
self.assertEqual(o, s.loads(value))