diff 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
line wrap: on
line diff
--- a/roundup/password.py	Mon Feb 27 12:17:11 2023 -0500
+++ b/roundup/password.py	Tue Feb 28 15:49:47 2023 -0500
@@ -26,7 +26,7 @@
 import warnings
 
 from base64 import b64encode, b64decode
-from hashlib import md5, sha1
+from hashlib import md5, sha1, sha512
 
 import roundup.anypy.random_ as random_
 
@@ -90,6 +90,9 @@
 
     def _pbkdf2(password, salt, rounds, keylen):
         return pbkdf2_hmac('sha1', password, salt, rounds, keylen)
+
+    def _pbkdf2_sha512(password, salt, rounds, keylen):
+        return pbkdf2_hmac('sha512', password, salt, rounds, keylen)
 except ImportError:
     # no hashlib.pbkdf2_hmac - make our own pbkdf2 function
     from struct import pack
@@ -100,10 +103,17 @@
         return _bjoin(bchr(bord(l) ^ bord(r))
                       for l, r in zip(left, right))  # noqa: E741
 
-    def _pbkdf2(password, salt, rounds, keylen):
-        digest_size = 20  # sha1 generates 20-byte blocks
+    def _pbkdf2(password, salt, rounds, keylen, sha=sha1):
+        if sha not in [sha1, sha512]:
+            raise ValueError(
+                "Invalid sha value passed to _pbkdf2: %s" % sha)
+        if sha == sha512:
+            digest_size = 64  # sha512 generates 64-byte blocks.
+        else:
+            digest_size = 20  # sha1 generates 20-byte blocks
+
         total_blocks = int((keylen+digest_size-1)/digest_size)
-        hmac_template = HMAC(password, None, sha1)
+        hmac_template = HMAC(password, None, sha)
         out = _bempty
         for i in range(1, total_blocks+1):
             hmac = hmac_template.copy()
@@ -117,6 +127,9 @@
                 block = xor_bytes(block, tmp)
             out += block
         return out[:keylen]
+ 
+    def _pbkdf2_sha512(password, salt, rounds, keylen):
+       return _pbkdf2(password, salt, rounds, keylen, sha=sha512)
 
 
 def ssha(password, salt):
@@ -130,6 +143,35 @@
     return ssha_digest
 
 
+def pbkdf2_sha512(password, salt, rounds, keylen):
+    """PBKDF2-HMAC-SHA512 password-based key derivation
+
+    :arg password: passphrase to use to generate key (if unicode,
+     converted to utf-8)
+    :arg salt: salt bytes to use when generating key
+    :param rounds: number of rounds to use to generate key
+    :arg keylen: number of bytes to generate
+
+    If hashlib supports pbkdf2, uses it's implementation as backend.
+
+    Unlike pbkdf2, this uses sha512 not sha1 as it's hash.
+
+    :returns:
+        raw bytes of generated key
+    """
+    password = s2b(us2s(password))
+    if keylen > 64:
+        # This statement may be old. - not seeing issues in testing
+        # with keylen > 40.
+        #
+        # NOTE: pbkdf2 allows up to (2**31-1)*20 bytes,
+        # but m2crypto has issues on some platforms above 40,
+        # and such sizes aren't needed for a password hash anyways...
+        raise ValueError("key length too large")
+    if rounds < 1:
+        raise ValueError("rounds must be positive number")
+    return _pbkdf2_sha512(password, salt, rounds, keylen)
+
 def pbkdf2(password, salt, rounds, keylen):
     """pkcs#5 password-based key derivation v2.0
 
@@ -185,6 +227,20 @@
     """
     if plaintext is None:
         plaintext = ""
+    if scheme == "PBKDF2S5":  # sha512 variant
+        if other:
+            rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
+        else:
+            raw_salt = random_.token_bytes(20)
+            salt = h64encode(raw_salt)
+            if config:
+                rounds = config.PASSWORD_PBKDF2_DEFAULT_ROUNDS
+            else:
+                rounds = 300000 # sha512 secure with fewer rounds than sha1
+        if rounds < 1000:
+            raise PasswordValueError("invalid PBKDF2 hash (rounds too low)")
+        raw_digest = pbkdf2_sha512(plaintext, raw_salt, rounds, 64)
+        return "%d$%s$%s" % (rounds, salt, h64encode(raw_digest))
     if scheme == "PBKDF2":
         if other:
             rounds, salt, raw_salt, digest = pbkdf2_unpack(other)
@@ -346,10 +402,11 @@
     >>> 'not sekrit' != p
     1
     """
-    # TODO: code to migrate from old password schemes.
 
     deprecated_schemes = ["SHA", "MD5", "crypt", "plaintext"]
-    known_schemes = ["PBKDF2", "SSHA"] + deprecated_schemes
+    experimental_schemes = [ "PBKDF2S5" ]
+    known_schemes = ["PBKDF2", "SSHA"] + experimental_schemes + \
+                    deprecated_schemes
 
     def __init__(self, plaintext=None, scheme=None, encrypted=None,
                  strict=False, config=None):
@@ -470,7 +527,7 @@
 
     # PBKDF2 - hash function
     h = "5000$7BvbBq.EZzz/O0HuwX3iP.nAG3s$g3oPnFFaga2BJaX5PoPRljl4XIE"
-    assert encodePassword("sekrit", "PBKDF2", h) == h
+    assert encodePassword("sekrit", "PBKDF2", h, config=config) == h
 
     # PBKDF2 - high level integration
     p = Password('sekrit', 'PBKDF2', config=config)
@@ -481,6 +538,15 @@
     assert 'sekrit' == p
     assert 'not sekrit' != p
 
+    # PBKDF2S5 - high level integration
+    p = Password('sekrit', 'PBKDF2S5', config=config)
+    print(p)
+    assert Password(encrypted=str(p)) == 'sekrit'
+    assert 'sekrit' == Password(encrypted=str(p))
+    assert p == 'sekrit'
+    assert p != 'not sekrit'
+    assert 'sekrit' == p
+    assert 'not sekrit' != p
 
 if __name__ == '__main__':
     from roundup.configuration import CoreConfig

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