diff --git a/django/core/signing.py b/django/core/signing.py
index 6fc76bc..61faee2 100644
a
|
b
|
start of the base64 JSON.
|
32 | 32 | There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'. |
33 | 33 | These functions make use of all of them. |
34 | 34 | """ |
| 35 | |
35 | 36 | from __future__ import unicode_literals |
36 | 37 | |
37 | 38 | import base64 |
… |
… |
from django.conf import settings
|
43 | 44 | from django.core.exceptions import ImproperlyConfigured |
44 | 45 | from django.utils import baseconv |
45 | 46 | from django.utils.crypto import constant_time_compare, salted_hmac |
46 | | from django.utils.encoding import smart_bytes |
| 47 | from django.utils.encoding import force_str, force_text |
47 | 48 | from django.utils.importlib import import_module |
48 | 49 | |
49 | 50 | |
… |
… |
class SignatureExpired(BadSignature):
|
62 | 63 | |
63 | 64 | |
64 | 65 | def b64_encode(s): |
65 | | return base64.urlsafe_b64encode(smart_bytes(s)).decode('ascii').strip('=') |
| 66 | return base64.urlsafe_b64encode(s).strip(b'=') |
66 | 67 | |
67 | 68 | |
68 | 69 | def b64_decode(s): |
69 | | pad = '=' * (-len(s) % 4) |
70 | | return base64.urlsafe_b64decode(smart_bytes(s + pad)).decode('ascii') |
| 70 | pad = b'=' * (-len(s) % 4) |
| 71 | return base64.urlsafe_b64decode(s + pad) |
71 | 72 | |
72 | 73 | |
73 | 74 | def base64_hmac(salt, value, key): |
… |
… |
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer,
|
116 | 117 | value or re-using a salt value across different parts of your |
117 | 118 | application without good cause is a security risk. |
118 | 119 | """ |
119 | | data = serializer().dumps(obj) |
| 120 | data = serializer().dumps(obj).encode() |
120 | 121 | |
121 | 122 | # Flag for if it's been compressed or not |
122 | 123 | is_compressed = False |
123 | 124 | |
124 | 125 | if compress: |
125 | 126 | # Avoid zlib dependency unless compress is being used |
126 | | compressed = zlib.compress(smart_bytes(data)) |
| 127 | compressed = zlib.compress(data) |
127 | 128 | if len(compressed) < (len(data) - 1): |
128 | 129 | data = compressed |
129 | 130 | is_compressed = True |
130 | 131 | base64d = b64_encode(data) |
131 | 132 | if is_compressed: |
132 | | base64d = '.' + base64d |
| 133 | base64d = b'.' + base64d |
133 | 134 | return TimestampSigner(key, salt=salt).sign(base64d) |
134 | 135 | |
135 | 136 | |
… |
… |
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, ma
|
137 | 138 | """ |
138 | 139 | Reverse of dumps(), raises BadSignature if signature fails |
139 | 140 | """ |
140 | | base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age) |
| 141 | # TimestampSigner.unsign always returns unicode but base64 and zlib |
| 142 | # compression operate on bytes, so we decode() here. |
| 143 | base64d = TimestampSigner(key, salt=salt).unsign(s, max_age=max_age).encode() |
141 | 144 | decompress = False |
142 | | if base64d[0] == '.': |
| 145 | if base64d[0] == b'.': |
143 | 146 | # It's compressed; uncompress it first |
144 | 147 | base64d = base64d[1:] |
145 | 148 | decompress = True |
146 | 149 | data = b64_decode(base64d) |
147 | 150 | if decompress: |
148 | 151 | data = zlib.decompress(data) |
149 | | return serializer().loads(data) |
| 152 | return serializer().loads(data.decode()) |
150 | 153 | |
151 | 154 | |
152 | 155 | class Signer(object): |
| 156 | |
153 | 157 | def __init__(self, key=None, sep=':', salt=None): |
154 | | self.sep = sep |
155 | | self.key = key or settings.SECRET_KEY |
156 | | self.salt = salt or ('%s.%s' % |
157 | | (self.__class__.__module__, self.__class__.__name__)) |
| 158 | # Use of native strings in all versions of Python |
| 159 | self.sep = str(sep) |
| 160 | self.key = str(key or settings.SECRET_KEY) |
| 161 | self.salt = str(salt or |
| 162 | '%s.%s' % (self.__class__.__module__, self.__class__.__name__)) |
158 | 163 | |
159 | 164 | def signature(self, value): |
160 | | return base64_hmac(self.salt + 'signer', value, self.key) |
| 165 | signature = base64_hmac(self.salt + 'signer', value, self.key) |
| 166 | # Convert the signature from bytes to str only on Python 3 |
| 167 | return force_str(signature) |
161 | 168 | |
162 | 169 | def sign(self, value): |
163 | | return '%s%s%s' % (value, self.sep, self.signature(value)) |
| 170 | value = force_str(value) |
| 171 | return str('%s%s%s') % (value, self.sep, self.signature(value)) |
164 | 172 | |
165 | 173 | def unsign(self, signed_value): |
| 174 | signed_value = force_str(signed_value) |
166 | 175 | if not self.sep in signed_value: |
167 | 176 | raise BadSignature('No "%s" found in value' % self.sep) |
168 | 177 | value, sig = signed_value.rsplit(self.sep, 1) |
169 | 178 | if constant_time_compare(sig, self.signature(value)): |
170 | | return value |
| 179 | return force_text(value) |
171 | 180 | raise BadSignature('Signature "%s" does not match' % sig) |
172 | 181 | |
173 | 182 | |
… |
… |
class TimestampSigner(Signer):
|
177 | 186 | return baseconv.base62.encode(int(time.time())) |
178 | 187 | |
179 | 188 | def sign(self, value): |
180 | | value = '%s%s%s' % (value, self.sep, self.timestamp()) |
181 | | return '%s%s%s' % (value, self.sep, self.signature(value)) |
| 189 | value = force_str(value) |
| 190 | value = str('%s%s%s') % (value, self.sep, self.timestamp()) |
| 191 | return super(TimestampSigner, self).sign(value) |
182 | 192 | |
183 | 193 | def unsign(self, value, max_age=None): |
184 | 194 | result = super(TimestampSigner, self).unsign(value) |
diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py
index 7145ec8..05c8b3d 100644
a
|
b
|
import time
|
4 | 4 | |
5 | 5 | from django.core import signing |
6 | 6 | from django.test import TestCase |
| 7 | from django.utils.encoding import force_str |
7 | 8 | from django.utils import six |
8 | | from django.utils.encoding import force_text |
9 | 9 | |
10 | 10 | |
11 | 11 | class TestSigner(TestCase): |
… |
… |
class TestSigner(TestCase):
|
22 | 22 | self.assertEqual( |
23 | 23 | signer.signature(s), |
24 | 24 | signing.base64_hmac(signer.salt + 'signer', s, |
25 | | 'predictable-secret') |
| 25 | 'predictable-secret').decode() |
26 | 26 | ) |
27 | 27 | self.assertNotEqual(signer.signature(s), signer2.signature(s)) |
28 | 28 | |
… |
… |
class TestSigner(TestCase):
|
32 | 32 | self.assertEqual( |
33 | 33 | signer.signature('hello'), |
34 | 34 | signing.base64_hmac('extra-salt' + 'signer', |
35 | | 'hello', 'predictable-secret')) |
| 35 | 'hello', 'predictable-secret').decode() |
| 36 | ) |
36 | 37 | self.assertNotEqual( |
37 | 38 | signing.Signer('predictable-secret', salt='one').signature('hello'), |
38 | 39 | signing.Signer('predictable-secret', salt='two').signature('hello')) |
… |
… |
class TestSigner(TestCase):
|
40 | 41 | def test_sign_unsign(self): |
41 | 42 | "sign/unsign should be reversible" |
42 | 43 | signer = signing.Signer('predictable-secret') |
43 | | examples = ( |
| 44 | examples = [ |
44 | 45 | 'q;wjmbk;wkmb', |
45 | 46 | '3098247529087', |
46 | 47 | '3098247:529:087:', |
47 | 48 | 'jkw osanteuh ,rcuh nthu aou oauh ,ud du', |
48 | 49 | '\u2019', |
49 | | ) |
| 50 | ] |
| 51 | if not six.PY3: |
| 52 | examples.append(b'a byte string') |
50 | 53 | for example in examples: |
51 | | self.assertNotEqual( |
52 | | force_text(example), force_text(signer.sign(example))) |
53 | | self.assertEqual(example, signer.unsign(signer.sign(example))) |
| 54 | signed = signer.sign(example) |
| 55 | self.assertIsInstance(signed, str) |
| 56 | self.assertNotEqual(force_str(example), signed) |
| 57 | self.assertEqual(example, signer.unsign(signed)) |
54 | 58 | |
55 | 59 | def unsign_detects_tampering(self): |
56 | 60 | "unsign should raise an exception if the value has been tampered with" |