2626import warnings
2727
2828from base64 import b64encode , b64decode
29- from hashlib import md5 , sha1
29+ from hashlib import md5 , sha1 , sha512
3030
3131import roundup .anypy .random_ as random_
3232
@@ -90,6 +90,9 @@ def h64decode(data):
9090
9191 def _pbkdf2 (password , salt , rounds , keylen ):
9292 return pbkdf2_hmac ('sha1' , password , salt , rounds , keylen )
93+
94+ def _pbkdf2_sha512 (password , salt , rounds , keylen ):
95+ return pbkdf2_hmac ('sha512' , password , salt , rounds , keylen )
9396except ImportError :
9497 # no hashlib.pbkdf2_hmac - make our own pbkdf2 function
9598 from struct import pack
@@ -100,10 +103,17 @@ def xor_bytes(left, right):
100103 return _bjoin (bchr (bord (l ) ^ bord (r ))
101104 for l , r in zip (left , right )) # noqa: E741
102105
103- def _pbkdf2 (password , salt , rounds , keylen ):
104- digest_size = 20 # sha1 generates 20-byte blocks
106+ def _pbkdf2 (password , salt , rounds , keylen , sha = sha1 ):
107+ if sha not in [sha1 , sha512 ]:
108+ raise ValueError (
109+ "Invalid sha value passed to _pbkdf2: %s" % sha )
110+ if sha == sha512 :
111+ digest_size = 64 # sha512 generates 64-byte blocks.
112+ else :
113+ digest_size = 20 # sha1 generates 20-byte blocks
114+
105115 total_blocks = int ((keylen + digest_size - 1 )/ digest_size )
106- hmac_template = HMAC (password , None , sha1 )
116+ hmac_template = HMAC (password , None , sha )
107117 out = _bempty
108118 for i in range (1 , total_blocks + 1 ):
109119 hmac = hmac_template .copy ()
@@ -117,6 +127,9 @@ def _pbkdf2(password, salt, rounds, keylen):
117127 block = xor_bytes (block , tmp )
118128 out += block
119129 return out [:keylen ]
130+
131+ def _pbkdf2_sha512 (password , salt , rounds , keylen ):
132+ return _pbkdf2 (password , salt , rounds , keylen , sha = sha512 )
120133
121134
122135def ssha (password , salt ):
@@ -130,6 +143,35 @@ def ssha(password, salt):
130143 return ssha_digest
131144
132145
146+ def pbkdf2_sha512 (password , salt , rounds , keylen ):
147+ """PBKDF2-HMAC-SHA512 password-based key derivation
148+
149+ :arg password: passphrase to use to generate key (if unicode,
150+ converted to utf-8)
151+ :arg salt: salt bytes to use when generating key
152+ :param rounds: number of rounds to use to generate key
153+ :arg keylen: number of bytes to generate
154+
155+ If hashlib supports pbkdf2, uses it's implementation as backend.
156+
157+ Unlike pbkdf2, this uses sha512 not sha1 as it's hash.
158+
159+ :returns:
160+ raw bytes of generated key
161+ """
162+ password = s2b (us2s (password ))
163+ if keylen > 64 :
164+ # This statement may be old. - not seeing issues in testing
165+ # with keylen > 40.
166+ #
167+ # NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
168+ # but m2crypto has issues on some platforms above 40,
169+ # and such sizes aren't needed for a password hash anyways...
170+ raise ValueError ("key length too large" )
171+ if rounds < 1 :
172+ raise ValueError ("rounds must be positive number" )
173+ return _pbkdf2_sha512 (password , salt , rounds , keylen )
174+
133175def pbkdf2 (password , salt , rounds , keylen ):
134176 """pkcs#5 password-based key derivation v2.0
135177
@@ -185,6 +227,20 @@ def encodePassword(plaintext, scheme, other=None, config=None):
185227 """
186228 if plaintext is None :
187229 plaintext = ""
230+ if scheme == "PBKDF2S5" : # sha512 variant
231+ if other :
232+ rounds , salt , raw_salt , digest = pbkdf2_unpack (other )
233+ else :
234+ raw_salt = random_ .token_bytes (20 )
235+ salt = h64encode (raw_salt )
236+ if config :
237+ rounds = config .PASSWORD_PBKDF2_DEFAULT_ROUNDS
238+ else :
239+ rounds = 300000 # sha512 secure with fewer rounds than sha1
240+ if rounds < 1000 :
241+ raise PasswordValueError ("invalid PBKDF2 hash (rounds too low)" )
242+ raw_digest = pbkdf2_sha512 (plaintext , raw_salt , rounds , 64 )
243+ return "%d$%s$%s" % (rounds , salt , h64encode (raw_digest ))
188244 if scheme == "PBKDF2" :
189245 if other :
190246 rounds , salt , raw_salt , digest = pbkdf2_unpack (other )
@@ -346,10 +402,11 @@ class Password(JournalPassword):
346402 >>> 'not sekrit' != p
347403 1
348404 """
349- # TODO: code to migrate from old password schemes.
350405
351406 deprecated_schemes = ["SHA" , "MD5" , "crypt" , "plaintext" ]
352- known_schemes = ["PBKDF2" , "SSHA" ] + deprecated_schemes
407+ experimental_schemes = [ "PBKDF2S5" ]
408+ known_schemes = ["PBKDF2" , "SSHA" ] + experimental_schemes + \
409+ deprecated_schemes
353410
354411 def __init__ (self , plaintext = None , scheme = None , encrypted = None ,
355412 strict = False , config = None ):
@@ -470,7 +527,7 @@ def test(config=None):
470527
471528 # PBKDF2 - hash function
472529 h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
473- assert encodePassword ("sekrit" , "PBKDF2" , h ) == h
530+ assert encodePassword ("sekrit" , "PBKDF2" , h , config = config ) == h
474531
475532 # PBKDF2 - high level integration
476533 p = Password ('sekrit' , 'PBKDF2' , config = config )
@@ -481,6 +538,15 @@ def test(config=None):
481538 assert 'sekrit' == p
482539 assert 'not sekrit' != p
483540
541+ # PBKDF2S5 - high level integration
542+ p = Password ('sekrit' , 'PBKDF2S5' , config = config )
543+ print (p )
544+ assert Password (encrypted = str (p )) == 'sekrit'
545+ assert 'sekrit' == Password (encrypted = str (p ))
546+ assert p == 'sekrit'
547+ assert p != 'not sekrit'
548+ assert 'sekrit' == p
549+ assert 'not sekrit' != p
484550
485551if __name__ == '__main__' :
486552 from roundup .configuration import CoreConfig
0 commit comments