Skip to content

Commit 47dd34b

Browse files
committed
issue2551253 - Modify password PBKDF2 method to use SHA512
Added new PBKDF2S5 using PBKDF2 with SHA512 rather than the original PBKDF2 which used SHA1. Currently changes to interfaces.py are required to use it. If we choose to adopt it, need to decide if mechanisms will be available via config.ini to choose methods and force migration.
1 parent 518d8da commit 47dd34b

File tree

2 files changed

+75
-7
lines changed

2 files changed

+75
-7
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Features:
105105
details. (Ralf Schlatterbeck)
106106
- issue2551243: schema-dump.py enhanced with anti-CSRF headers. Flake8
107107
cleanup and python2 support. (John Rouillard)
108+
- issue2551253 - new password hash PBDKF2-SHA512 added. Not available
109+
by default. See issue ticket for details.
108110

109111
2022-07-13 2.2.0
110112

roundup/password.py

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import warnings
2727

2828
from base64 import b64encode, b64decode
29-
from hashlib import md5, sha1
29+
from hashlib import md5, sha1, sha512
3030

3131
import 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)
9396
except 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

122135
def 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+
133175
def 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

485551
if __name__ == '__main__':
486552
from roundup.configuration import CoreConfig

0 commit comments

Comments
 (0)