1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
5
Functions for creating and restoring url-safe signed pickled objects.
7
The format used looks like this:
10
'UydoZWxsbycKcDAKLg.PZjRn5gyN4U33XnSEGVQPPrs9g0'
12
There are two components here, separatad by a '.'. The first component is a
13
URLsafe base64 encoded pickle of the object passed to dumps(). The second
14
component is a base64 encoded SHA1 hash of "$first_component.$secret"
16
Calling signed.loads(s) checks the signature BEFORE unpickling the object -
17
this protects against malformed pickle attacks. If the signature fails, a
18
ValueError subclass is raised (actually a BadSignature):
20
>>> loads('UydoZWxsbycKcDAKLg.PZjRn5gyN4U33XnSEGVQPPrs9g0')
22
>>> loads('UydoZWxsbycKcDAKLg.PZjRn5gyN4U33XnSEGVQPPrs9g0-modified')
23
Traceback (most recent call last):
25
BadSignature: Signature failed: PZjRn5gyN4U33XnSEGVQPPrs9g0-modified
27
There are 65 url-safe characters: the 64 used by url-safe base64 and the '.'.
28
These functions make use of all of them.
35
from django.conf import settings
38
def dumps(obj, secret=None, extra_salt=''):
40
Returns URL-safe, sha1 signed base64 compressed pickle. If secret is
41
None, settings.SECRET_KEY is used instead.
43
extra_salt can be used to further salt the hash, in case you're worried
44
that the NSA might try to brute-force your SHA-1 protected secret.
46
pickled = pickle.dumps(obj)
47
base64d = encode(pickled).strip('=')
48
return sign(base64d, (secret or settings.SECRET_KEY) + extra_salt)
51
def loads(s, secret=None, extra_salt=''):
52
"Reverse of dumps(), raises ValueError if signature fails"
53
if isinstance(s, unicode):
54
s = s.encode('utf8') # base64 works on bytestrings, not on unicodes
56
base64d = unsign(s, (secret or settings.SECRET_KEY) + extra_salt)
59
pickled = decode(base64d)
60
return pickle.loads(pickled)
64
return base64.urlsafe_b64encode(s).strip('=')
68
return base64.urlsafe_b64decode(s + '=' * (len(s) % 4))
71
class BadSignature(ValueError):
72
# Extends ValueError, which makes it more convenient to catch and has
73
# basically the correct semantics.
77
def sign(value, key=None):
78
if isinstance(value, unicode):
80
'sign() needs bytestring, not unicode: %s' % repr(value))
82
key = settings.SECRET_KEY
83
return value + '.' + base64_sha1(value + key)
86
def unsign(signed_value, key=None):
87
if isinstance(signed_value, unicode):
88
raise TypeError('unsign() needs bytestring, not unicode')
90
key = settings.SECRET_KEY
91
if not '.' in signed_value:
92
raise BadSignature('Missing sig (no . found in value)')
93
value, sig = signed_value.rsplit('.', 1)
94
if base64_sha1(value + key) == sig:
97
raise BadSignature('Signature failed: %s' % sig)
101
return encode(hashlib.sha1(s).digest())