2222__docformat__ = 'restructuredtext'
2323
2424import re , string , random
25+ from base64 import b64encode , b64decode
2526from roundup .anypy .hashlib_ import md5 , sha1
2627try :
2728 import crypt
2829except ImportError :
2930 crypt = None
3031
32+ _bempty = ""
33+ _bjoin = _bempty .join
34+
35+ def getrandbytes (count ):
36+ return _bjoin (chr (random .randint (0 ,255 )) for i in xrange (count ))
37+
38+ #NOTE: PBKDF2 hash is using this variant of base64 to minimize encoding size,
39+ # and have charset that's compatible w/ unix crypt variants
40+ def h64encode (data ):
41+ """encode using variant of base64"""
42+ return b64encode (data , "./" ).strip ("=\n " )
43+
44+ def h64decode (data ):
45+ """decode using variant of base64"""
46+ off = len (data ) % 4
47+ if off == 0 :
48+ return b64decode (data , "./" )
49+ elif off == 1 :
50+ raise ValueError ("invalid bas64 input" )
51+ elif off == 2 :
52+ return b64decode (data + "==" , "./" )
53+ else :
54+ return b64decode (data + "=" , "./" )
55+
56+ try :
57+ from M2Crypto .EVP import pbkdf2 as _pbkdf2
58+ except ImportError :
59+ #no m2crypto - make our own pbkdf2 function
60+ from struct import pack
61+ from hmac import HMAC
62+ try :
63+ from hashlib import sha1
64+ except ImportError :
65+ from sha import new as sha1
66+
67+ def xor_bytes (left , right ):
68+ "perform bitwise-xor of two byte-strings"
69+ return _bjoin (chr (ord (l ) ^ ord (r )) for l , r in zip (left , right ))
70+
71+ def _pbkdf2 (password , salt , rounds , keylen ):
72+ digest_size = 20 # sha1 generates 20-byte blocks
73+ total_blocks = int ((keylen + digest_size - 1 )/ digest_size )
74+ hmac_template = HMAC (password , None , sha1 )
75+ out = _bempty
76+ for i in xrange (1 , total_blocks + 1 ):
77+ hmac = hmac_template .copy ()
78+ hmac .update (salt + pack (">L" ,i ))
79+ block = tmp = hmac .digest ()
80+ for j in xrange (rounds - 1 ):
81+ hmac = hmac_template .copy ()
82+ hmac .update (tmp )
83+ tmp = hmac .digest ()
84+ #TODO: need to speed up this call
85+ block = xor_bytes (block , tmp )
86+ out += block
87+ return out [:keylen ]
88+
89+ def pbkdf2 (password , salt , rounds , keylen ):
90+ """pkcs#5 password-based key derivation v2.0
91+
92+ :arg password: passphrase to use to generate key (if unicode, converted to utf-8)
93+ :arg salt: salt string to use when generating key (if unicode, converted to utf-8)
94+ :param rounds: number of rounds to use to generate key
95+ :arg keylen: number of bytes to generate
96+
97+ If M2Crypto is present, uses it's implementation as backend.
98+
99+ :returns:
100+ raw bytes of generated key
101+ """
102+ if isinstance (password , unicode ):
103+ password = password .encode ("utf-8" )
104+ if isinstance (salt , unicode ):
105+ salt = salt .encode ("utf-8" )
106+ if keylen > 40 :
107+ #NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
108+ # but m2crypto has issues on some platforms above 40,
109+ # and such sizes aren't needed for a password hash anyways...
110+ raise ValueError , "key length too large"
111+ if rounds < 1 :
112+ raise ValueError , "rounds must be positive number"
113+ return _pbkdf2 (password , salt , rounds , keylen )
114+
31115class PasswordValueError (ValueError ):
32116 """ The password value is not valid """
33117 pass
@@ -37,7 +121,33 @@ def encodePassword(plaintext, scheme, other=None):
37121 """
38122 if plaintext is None :
39123 plaintext = ""
40- if scheme == 'SHA' :
124+ if scheme == "PBKDF2" :
125+ if other :
126+ #assume it has format "{rounds}${salt}${digest}"
127+ if isinstance (other , unicode ):
128+ other = other .encode ("ascii" )
129+ try :
130+ rounds , salt , digest = other .split ("$" )
131+ except ValueError :
132+ raise PasswordValueError , "invalid PBKDF2 hash (wrong number of separators)"
133+ if rounds .startswith ("0" ):
134+ raise PasswordValueError , "invalid PBKDF2 hash (zero-padded rounds)"
135+ try :
136+ rounds = int (rounds )
137+ except ValueError :
138+ raise PasswordValueError , "invalid PBKDF2 hash (invalid rounds)"
139+ raw_salt = h64decode (salt )
140+ else :
141+ raw_salt = getrandbytes (20 )
142+ salt = h64encode (raw_salt )
143+ #FIXME: find way to access config, so default rounds
144+ # can be altered for faster/slower hosts via config.ini
145+ rounds = 10000
146+ if rounds < 1000 :
147+ raise PasswordValueError , "invalid PBKDF2 hash (rounds too low)"
148+ raw_digest = pbkdf2 (plaintext , raw_salt , rounds , 20 )
149+ return "%d$%s$%s" % (rounds , salt , h64encode (raw_digest ))
150+ elif scheme == 'SHA' :
41151 s = sha1 (plaintext ).hexdigest ()
42152 elif scheme == 'MD5' :
43153 s = md5 (plaintext ).hexdigest ()
@@ -80,24 +190,26 @@ class Password:
80190 >>> 'not sekrit' != p
81191 1
82192 """
193+ #TODO: code to migrate from old password schemes.
83194
84- default_scheme = 'SHA' # new encryptions use this scheme
195+ default_scheme = 'PBKDF2' # new encryptions use this scheme
196+ known_schemes = [ "PBKDF2" , "SHA" , "MD5" , "crypt" , "plaintext" ]
85197 pwre = re .compile (r'{(\w+)}(.+)' )
86198
87- def __init__ (self , plaintext = None , scheme = None , encrypted = None ):
199+ def __init__ (self , plaintext = None , scheme = None , encrypted = None , strict = False ):
88200 """Call setPassword if plaintext is not None."""
89201 if scheme is None :
90202 scheme = self .default_scheme
91203 if plaintext is not None :
92204 self .setPassword (plaintext , scheme )
93205 elif encrypted is not None :
94- self .unpack (encrypted , scheme )
206+ self .unpack (encrypted , scheme , strict = strict )
95207 else :
96208 self .scheme = self .default_scheme
97209 self .password = None
98210 self .plaintext = None
99211
100- def unpack (self , encrypted , scheme = None ):
212+ def unpack (self , encrypted , scheme = None , strict = False ):
101213 """Set the password info from the scheme:<encryted info> string
102214 (the inverse of __str__)
103215 """
@@ -109,6 +221,8 @@ def unpack(self, encrypted, scheme=None):
109221 else :
110222 # currently plaintext - encrypt
111223 self .setPassword (encrypted , scheme )
224+ if strict and self .scheme not in self .known_schemes :
225+ raise PasswordValueError , "unknown encryption scheme: %r" % (self .scheme ,)
112226
113227 def setPassword (self , plaintext , scheme = None ):
114228 """Sets encrypts plaintext."""
@@ -160,6 +274,22 @@ def test():
160274 assert 'sekrit' == p
161275 assert 'not sekrit' != p
162276
277+ # PBKDF2 - low level function
278+ from binascii import unhexlify
279+ k = pbkdf2 ("password" , "ATHENA.MIT.EDUraeburn" , 1200 , 32 )
280+ assert k == unhexlify ("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13" )
281+
282+ # PBKDF2 - hash function
283+ h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
284+ assert encodePassword ("sekrit" , "PBKDF2" , h ) == h
285+
286+ # PBKDF2 - high level integration
287+ p = Password ('sekrit' , 'PBKDF2' )
288+ assert p == 'sekrit'
289+ assert p != 'not sekrit'
290+ assert 'sekrit' == p
291+ assert 'not sekrit' != p
292+
163293if __name__ == '__main__' :
164294 test ()
165295
0 commit comments