Sign a string using SHA1, then shrink it using url-safe base65

 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
"""
YOU PROBABLY SHOULDN'T USE THIS

See http://fi.am/entry/urlsafe-base64-encodingdecoding-in-two-lines/

You can do this using Python's built-in base64 library in just two lines of code:

import base64

def uri_b64encode(s):
     return base64.urlsafe_b64encode(s).strip('=')

def uri_b64decode(s):
     return base64.urlsafe_b64decode(s + '=' * (len(s) % 4))

----------------------------------------------------

Utility functions for signing a string using SHA1, then shrinking that SHA1
hash down to as short a string as possible using lossless base65 compression.

>>> data = "Hello"
>>> secret = "sekrit"
>>> sig = sign(data, secret)
>>> sig
'F7wP0YkP663d-n3yRDQVd8p0GC'
>>> verify(data, secret, sig)
True

"""

# Characters that are NOT encoded by urllib.urlencode:
URLSAFE = '-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
BASE10 = "0123456789"

import hashlib

def sign(s, key):
    return base65_sha1(s + ':'  + key)

def verify(s, key, sig):
    return sign(s, key) == sig

def base65_sha1(s):
    return int_to_base65(int(hashlib.sha1(s).hexdigest(), 16))

def sha1_from_base65(s):
    i = base65_to_int(s)
    return hex(i).replace('0x', '')

def int_to_base65(i):
    return baseconvert(str(i).lower().replace('l', ''), BASE10, URLSAFE)

def base65_to_int(s):
    return baseconvert(s, URLSAFE, BASE10)

def baseconvert(number_string, from_digits, to_digits):
    "Convert a number between two bases of arbitrary digits"
    # Inspired by http://code.activestate.com/recipes/111286/
    # Convert number_string (in from_digits encoding) to an integer
    i = 0L
    for digit in str(number_string):
       i = i * len(from_digits) + from_digits.index(digit)
    # Convert integer to to_digits encoding
    res = []
    while i > 0:
        res.insert(0, to_digits[i % len(to_digits)])
        i = i / len(to_digits)
    return ''.join(res)

More like this

  1. web-key: Base64 Shared Secret for Access Control by sbw 3 years, 12 months ago
  2. Drupal password hasher for migration by dgrtwo 1 year, 1 month ago
  3. Custom urlencode that lets you specify safe characters. by ungenio41 2 years, 8 months ago
  4. Django template tag to hash/map a value to a unique web color. by danielsokolowski 1 year, 6 months ago
  5. In-memory XML-RPC server based on URL by diverman 2 years, 11 months ago

Comments

Gulopine (on August 27, 2008):

I suppose I should do a round of upgrades on django-signedcookies anyway, and this would be a very useful addition.

#

carljm (on August 27, 2008):

This has another great use: for sites where you want to use an email address as the primary user identifier (no username), one way of generating unique usernames to satisfy Django's User object is to make a hash of the (unique) email address. The problem is that User.username is only 30 chars, not long enough for a 40-character hash. But a 27-character encoding of the hash just fits!

#

aarond10ster (on August 27, 2008):

Nice snippet! I have one question though.

str(i).lower().replace('L', '')

Whats the point of the replace('L','')? Isn't the string already lowercase by this point?

#

simon (on August 28, 2008):

It turns out you can do this using urlsafe Base64 in a much more efficient way, and resulting in compression that is essentially the same size reduction:

http://fi.am/entry/urlsafe-base64-encodingdecoding-in-two-lines/

aarond10ster - that's a bug, it should be lower case 'l' (for long integer, but we don't want to include that in our hashing). Fixed that now.

#

kcarnold (on August 28, 2008):

Might want to make another full snippet, because the approach in the comments doesn't do the sign functionality (sha1 digest).

#

(Forgotten your password?)