Mercurial > p > roundup > code
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 |
