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