~ubuntuone-pqm-team/canonical-identity-provider/trunk

« back to all changes in this revision

Viewing changes to identityprovider/signed.py

  • Committer: Danny Tamez
  • Date: 2010-04-21 15:29:24 UTC
  • Revision ID: danny.tamez@canonical.com-20100421152924-lq1m92tstk2iz75a
Canonical SSO Provider (Open Source) - Initial Commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2010 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
"""
 
5
Functions for creating and restoring url-safe signed pickled objects.
 
6
 
 
7
The format used looks like this:
 
8
 
 
9
>>> dumps("hello")
 
10
'UydoZWxsbycKcDAKLg.PZjRn5gyN4U33XnSEGVQPPrs9g0'
 
11
 
 
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"
 
15
 
 
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):
 
19
 
 
20
>>> loads('UydoZWxsbycKcDAKLg.PZjRn5gyN4U33XnSEGVQPPrs9g0')
 
21
'hello'
 
22
>>> loads('UydoZWxsbycKcDAKLg.PZjRn5gyN4U33XnSEGVQPPrs9g0-modified')
 
23
Traceback (most recent call last):
 
24
...
 
25
BadSignature: Signature failed: PZjRn5gyN4U33XnSEGVQPPrs9g0-modified
 
26
 
 
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.
 
29
"""
 
30
 
 
31
import base64
 
32
import hashlib
 
33
import pickle
 
34
 
 
35
from django.conf import settings
 
36
 
 
37
 
 
38
def dumps(obj, secret=None, extra_salt=''):
 
39
    """
 
40
    Returns URL-safe, sha1 signed base64 compressed pickle. If secret is
 
41
    None, settings.SECRET_KEY is used instead.
 
42
 
 
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.
 
45
    """
 
46
    pickled = pickle.dumps(obj)
 
47
    base64d = encode(pickled).strip('=')
 
48
    return sign(base64d, (secret or settings.SECRET_KEY) + extra_salt)
 
49
 
 
50
 
 
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
 
55
    try:
 
56
        base64d = unsign(s, (secret or settings.SECRET_KEY) + extra_salt)
 
57
    except ValueError:
 
58
        raise
 
59
    pickled = decode(base64d)
 
60
    return pickle.loads(pickled)
 
61
 
 
62
 
 
63
def encode(s):
 
64
    return base64.urlsafe_b64encode(s).strip('=')
 
65
 
 
66
 
 
67
def decode(s):
 
68
    return base64.urlsafe_b64decode(s + '=' * (len(s) % 4))
 
69
 
 
70
 
 
71
class BadSignature(ValueError):
 
72
    # Extends ValueError, which makes it more convenient to catch and has
 
73
    # basically the correct semantics.
 
74
    pass
 
75
 
 
76
 
 
77
def sign(value, key=None):
 
78
    if isinstance(value, unicode):
 
79
        raise TypeError(
 
80
            'sign() needs bytestring, not unicode: %s' % repr(value))
 
81
    if key is None:
 
82
        key = settings.SECRET_KEY
 
83
    return value + '.' + base64_sha1(value + key)
 
84
 
 
85
 
 
86
def unsign(signed_value, key=None):
 
87
    if isinstance(signed_value, unicode):
 
88
        raise TypeError('unsign() needs bytestring, not unicode')
 
89
    if key is None:
 
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:
 
95
        return value
 
96
    else:
 
97
        raise BadSignature('Signature failed: %s' % sig)
 
98
 
 
99
 
 
100
def base64_sha1(s):
 
101
    return encode(hashlib.sha1(s).digest())