comparison roundup/password.py @ 7201:da751d3a2138

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.
author John Rouillard <rouilj@ieee.org>
date Tue, 28 Feb 2023 15:49:47 -0500
parents 8e8d111fcdcd
children 4d83f9f751ff
comparison
equal deleted inserted replaced
7200:c878d24ea034 7201:da751d3a2138
24 import string 24 import string
25 import sys 25 import sys
26 import warnings 26 import warnings
27 27
28 from base64 import b64encode, b64decode 28 from base64 import b64encode, b64decode
29 from hashlib import md5, sha1 29 from hashlib import md5, sha1, sha512
30 30
31 import roundup.anypy.random_ as random_ 31 import roundup.anypy.random_ as random_
32 32
33 from roundup.anypy.strings import us2s, b2s, s2b 33 from roundup.anypy.strings import us2s, b2s, s2b
34 from roundup.exceptions import RoundupException 34 from roundup.exceptions import RoundupException
88 try: 88 try:
89 from hashlib import pbkdf2_hmac 89 from hashlib import pbkdf2_hmac
90 90
91 def _pbkdf2(password, salt, rounds, keylen): 91 def _pbkdf2(password, salt, rounds, keylen):
92 return pbkdf2_hmac('sha1', password, salt, rounds, keylen) 92 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)
93 except ImportError: 96 except ImportError:
94 # no hashlib.pbkdf2_hmac - make our own pbkdf2 function 97 # no hashlib.pbkdf2_hmac - make our own pbkdf2 function
95 from struct import pack 98 from struct import pack
96 from hmac import HMAC 99 from hmac import HMAC
97 100
98 def xor_bytes(left, right): 101 def xor_bytes(left, right):
99 "perform bitwise-xor of two byte-strings" 102 "perform bitwise-xor of two byte-strings"
100 return _bjoin(bchr(bord(l) ^ bord(r)) 103 return _bjoin(bchr(bord(l) ^ bord(r))
101 for l, r in zip(left, right)) # noqa: E741 104 for l, r in zip(left, right)) # noqa: E741
102 105
103 def _pbkdf2(password, salt, rounds, keylen): 106 def _pbkdf2(password, salt, rounds, keylen, sha=sha1):
104 digest_size = 20 # sha1 generates 20-byte blocks 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
105 total_blocks = int((keylen+digest_size-1)/digest_size) 115 total_blocks = int((keylen+digest_size-1)/digest_size)
106 hmac_template = HMAC(password, None, sha1) 116 hmac_template = HMAC(password, None, sha)
107 out = _bempty 117 out = _bempty
108 for i in range(1, total_blocks+1): 118 for i in range(1, total_blocks+1):
109 hmac = hmac_template.copy() 119 hmac = hmac_template.copy()
110 hmac.update(salt + pack(">L", i)) 120 hmac.update(salt + pack(">L", i))
111 block = tmp = hmac.digest() 121 block = tmp = hmac.digest()
115 tmp = hmac.digest() 125 tmp = hmac.digest()
116 # TODO: need to speed up this call 126 # TODO: need to speed up this call
117 block = xor_bytes(block, tmp) 127 block = xor_bytes(block, tmp)
118 out += block 128 out += block
119 return out[:keylen] 129 return out[:keylen]
130
131 def _pbkdf2_sha512(password, salt, rounds, keylen):
132 return _pbkdf2(password, salt, rounds, keylen, sha=sha512)
120 133
121 134
122 def ssha(password, salt): 135 def ssha(password, salt):
123 ''' Make ssha digest from password and salt. 136 ''' Make ssha digest from password and salt.
124 Based on code of Roberto Aguilar <roberto@baremetal.io> 137 Based on code of Roberto Aguilar <roberto@baremetal.io>
127 shaval = sha1(password) # nosec 140 shaval = sha1(password) # nosec
128 shaval.update(salt) 141 shaval.update(salt)
129 ssha_digest = b2s(b64encode(shaval.digest() + salt).strip()) 142 ssha_digest = b2s(b64encode(shaval.digest() + salt).strip())
130 return ssha_digest 143 return ssha_digest
131 144
145
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)
132 174
133 def pbkdf2(password, salt, rounds, keylen): 175 def pbkdf2(password, salt, rounds, keylen):
134 """pkcs#5 password-based key derivation v2.0 176 """pkcs#5 password-based key derivation v2.0
135 177
136 :arg password: passphrase to use to generate key (if unicode, 178 :arg password: passphrase to use to generate key (if unicode,
183 def encodePassword(plaintext, scheme, other=None, config=None): 225 def encodePassword(plaintext, scheme, other=None, config=None):
184 """Encrypt the plaintext password. 226 """Encrypt the plaintext password.
185 """ 227 """
186 if plaintext is None: 228 if plaintext is None:
187 plaintext = "" 229 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))
188 if scheme == "PBKDF2": 244 if scheme == "PBKDF2":
189 if other: 245 if other:
190 rounds, salt, raw_salt, digest = pbkdf2_unpack(other) 246 rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
191 else: 247 else:
192 raw_salt = random_.token_bytes(20) 248 raw_salt = random_.token_bytes(20)
344 >>> 'sekrit' == p 400 >>> 'sekrit' == p
345 1 401 1
346 >>> 'not sekrit' != p 402 >>> 'not sekrit' != p
347 1 403 1
348 """ 404 """
349 # TODO: code to migrate from old password schemes.
350 405
351 deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"] 406 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
353 410
354 def __init__(self, plaintext=None, scheme=None, encrypted=None, 411 def __init__(self, plaintext=None, scheme=None, encrypted=None,
355 strict=False, config=None): 412 strict=False, config=None):
356 """Call setPassword if plaintext is not None.""" 413 """Call setPassword if plaintext is not None."""
357 if scheme is None: 414 if scheme is None:
468 k = pbkdf2("password", b"ATHENA.MIT.EDUraeburn", 1200, 32) 525 k = pbkdf2("password", b"ATHENA.MIT.EDUraeburn", 1200, 32)
469 assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13") 526 assert k == unhexlify("5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13")
470 527
471 # PBKDF2 - hash function 528 # PBKDF2 - hash function
472 h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE" 529 h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
473 assert encodePassword("sekrit", "PBKDF2", h) == h 530 assert encodePassword("sekrit", "PBKDF2", h, config=config) == h
474 531
475 # PBKDF2 - high level integration 532 # PBKDF2 - high level integration
476 p = Password('sekrit', 'PBKDF2', config=config) 533 p = Password('sekrit', 'PBKDF2', config=config)
477 assert Password(encrypted=str(p)) == 'sekrit' 534 assert Password(encrypted=str(p)) == 'sekrit'
478 assert 'sekrit' == Password(encrypted=str(p)) 535 assert 'sekrit' == Password(encrypted=str(p))
479 assert p == 'sekrit' 536 assert p == 'sekrit'
480 assert p != 'not sekrit' 537 assert p != 'not sekrit'
481 assert 'sekrit' == p 538 assert 'sekrit' == p
482 assert 'not sekrit' != p 539 assert 'not sekrit' != p
483 540
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
484 550
485 if __name__ == '__main__': 551 if __name__ == '__main__':
486 from roundup.configuration import CoreConfig 552 from roundup.configuration import CoreConfig
487 test(CoreConfig()) 553 test(CoreConfig())
488 crypt = None 554 crypt = None

Roundup Issue Tracker: http://roundup-tracker.org/