replacement for the standard crypt.crypt
authorThierry Parmentelat <thierry.parmentelat@inria.fr>
Wed, 20 Nov 2024 15:25:22 +0000 (16:25 +0100)
committerThierry Parmentelat <thierry.parmentelat@inria.fr>
Mon, 25 Nov 2024 10:35:49 +0000 (11:35 +0100)
done by introducing our own module PLC.crypt
and testing against the standard crypt.crypt

PLC/Auth.py
PLC/Persons.py
PLC/plc_crypt.py [new file with mode: 0644]

index 927850e..5cea320 100644 (file)
@@ -5,7 +5,6 @@
 # Copyright (C) 2006 The Trustees of Princeton University
 #
 
-import crypt
 from hashlib import sha1 as sha
 import hmac
 import time
@@ -21,6 +20,7 @@ from PLC.Peers import Peer, Peers
 from PLC.Keys import Keys
 from PLC.Boot import notify_owners
 from PLC.Logger import logger
+from PLC.plc_crypt import plc_crypt
 
 class Auth(Parameter):
     """
@@ -319,7 +319,7 @@ class PasswordAuth(Auth):
 
             # Protect against blank passwords in the DB
             if password is None or password[:12] == "" or \
-               crypt.crypt(plaintext, password[:12]) != password:
+               plc_crypt(plaintext, password[:12]) != password:
                 raise PLCAuthenticationFailure(
                     "PasswordAuth: Password verification failed")
 
index 6e1b240..2239c50 100644 (file)
@@ -9,7 +9,6 @@ from hashlib import md5
 import time
 from random import Random
 import re
-import crypt
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter, Mixed
@@ -18,6 +17,7 @@ from PLC.Table import Row, Table
 from PLC.Roles import Role, Roles
 from PLC.Keys import Key, Keys
 from PLC.Messages import Message, Messages
+from PLC.plc_crypt import plc_crypt
 
 class Person(Row):
     """
@@ -102,6 +102,7 @@ class Person(Row):
         database.
         """
 
+        # this for crypt.crypt means MD5
         magic = "$1$"
 
         if len(password) > len(magic) and \
@@ -111,7 +112,9 @@ class Person(Row):
             # Generate a somewhat unique 8 character salt string
             salt = str(time.time()) + str(Random().random())
             salt = md5(salt.encode()).hexdigest()[:8]
-            return crypt.crypt(password, magic + salt + "$")
+            # magic:3 salt:8 $:1 means the total salt is 12 characters
+            # as can be seen in Auth.py
+            return plc_crypt(password, magic + salt + "$")
 
     validate_date_created = Row.validate_timestamp
     validate_last_updated = Row.validate_timestamp
diff --git a/PLC/plc_crypt.py b/PLC/plc_crypt.py
new file mode 100644 (file)
index 0000000..f8ba4da
--- /dev/null
@@ -0,0 +1,149 @@
+"""
+compatibility replacement for our needs among the old crypt module
+that was removed in 3.13 (failed to see the earlier warnings...)
+
+were only using the crypt.crypt() function
+plus, we only use MD5 for hashing, and we always provide our salt
+so we only need to replace that
+
+this is stolen from (and a thousands thanks to) the author of this code:
+https://github.com/guffre/python-crypt/blob/main/crypt.py
+"""
+
+import base64
+import hashlib
+
+
+def crypt_base64(buffer):
+    """
+    The custom base64 that is specific to these crypt algorithms
+    I know it looks weird, but this is apparently what the spec is
+    """
+    unix_crypt_base = (
+        b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+    )
+    normal_base = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+    ret = b""
+    if len(buffer) == 16:
+        c1 = list(range(0, 5))
+        c2 = list(range(6, 11))
+        c3 = list(range(12, 16)) + [5]
+    elif len(buffer) == 32:
+        c1 = [(n * 21 + 00) % 30 for n in range(10)]
+        c2 = [(n * 21 + 10) % 30 for n in range(10)]
+        c3 = [(n * 21 + 20) % 30 for n in range(10)]
+    elif len(buffer) == 64:
+        c1 = [(n * 22 + 00) % 63 for n in range(21)]
+        c2 = [(n * 22 + 21) % 63 for n in range(21)]
+        c3 = [(n * 22 + 42) % 63 for n in range(21)]
+    else:
+        return None
+    for block in zip(c1, c2, c3):
+        collect = b"".join(buffer[n : n + 1] for n in block)
+        ret += base64.b64encode(collect)[::-1]
+    if len(buffer) == 16:
+        ret += base64.b64encode(b"00" + buffer[11:12])[::-1][:2]
+    elif len(buffer) == 32:
+        ret += base64.b64encode(b"0" + buffer[31:32] + buffer[30:31])[::-1][:2]
+    elif len(buffer) == 64:
+        ret += base64.b64encode(b"00" + buffer[63:64])[::-1][:2]
+    # Python 2/3 compatability
+    trans = string if bytes == str else bytes
+    ret = ret.translate(trans.maketrans(normal_base, unix_crypt_base))
+    return ret
+
+
+def bin_crypt_md5(key:bytes, salt:bytes) -> bytes:
+
+    salt = salt[:8]
+    rounds = 1000
+
+    # Initialize contexts
+    ctx = hashlib.md5(key + b"$1$" + salt)
+    alt_ctx = hashlib.md5(key + salt + key)
+    alt_result = alt_ctx.digest()
+
+    # Add hash-bytes of second context to first
+    cnt = len(key)
+    while cnt > ctx.digest_size:
+        ctx.update(alt_result)
+        cnt -= ctx.digest_size
+    ctx.update(alt_result[:cnt])
+
+    # Binary mix
+    cnt = len(key)
+    while cnt > 0:
+        if (cnt & 1) != 0:
+            ctx.update(b"\0")
+        else:
+            ctx.update(key[0:1])
+        cnt = cnt >> 1
+
+    alt_result = ctx.digest()
+
+    # Perform rounds of hashing
+    for cnt in range(rounds):
+        ctx = hashlib.md5()
+        if (cnt & 1) != 0:
+            ctx.update(key)
+        else:
+            ctx.update(alt_result)
+        if cnt % 3 != 0:
+            ctx.update(salt)
+        if cnt % 7 != 0:
+            ctx.update(key)
+        if (cnt & 1) != 0:
+            ctx.update(alt_result)
+        else:
+            ctx.update(key)
+        alt_result = ctx.digest()
+
+    # Crypt-base64 the hash digest
+    encoded = crypt_base64(alt_result).decode()
+
+    # Make some nice output
+    hashnumber = 1
+    formatted = "${}${}${}".format(hashnumber, salt.decode(), encoded)
+    return (alt_result, formatted)
+
+
+def crypt_md5(password: str, salt: str) -> str:
+    """
+    same as bin_crypt_md5 but with strings
+    """
+    key = password.encode()
+    salt = salt[3:].encode()
+    return bin_crypt_md5(key, salt)[1]
+
+crypt = crypt_md5
+
+
+# some test cases
+def test_crypt():
+    # these were produced by the crypt.crypt() function
+    BY_STANDARD_CRYPT = [
+        ["password",         "$1$6167a424$9DuVAlIQ8zHRnth8ZUVRk1"],
+        ["password",         "$1$d2b53fd1$nhHpCadNhArACvELo1Ugm0"],
+        ["password1",        "$1$ab75e533$MXdWjzcZ9HR3B5x8uBCkC."],
+        ["password1",        "$1$38520235$sRIMkorcm235lA0V6IKdN1"],
+        ["password123",      "$1$5012ee70$a.557l7kfK/ksQ5dinvIW/"],
+        ["password123",      "$1$f76ad85d$bli2s3/GQ8C5452km3Bdd."],
+        ["tarabiscota",      "$1$b426146a$IkJK0vYHkWGV4KHDJ7wKw/"],
+        ["tarabiscota",      "$1$9bd6a92f$/uObTeHg3wlDwJB7YO8DZ."],
+        ["56890&^*(HGJkjh",  "$1$b5bf816d$HqfMqyXloPHo3mabIiSAt1"],
+        ["56890&^*(HGJkjh",  "$1$a9c16150$n0yY5WatF7noVf8wyTHR51"],
+        [ "a2357A2357", "$1$e846962a$L/RA8n/sWWyvaE7YsjKOp0" ],
+        [ "hello-world", "$1$91ed0406$g2THz.r6/4Zhu3EHZzpfW." ],
+        [ "sodfisj0395u023-0=-a]\\df[skg]", "$1$e51c6cb6$rj.okkN6wwLR8djoiElK3." ],
+        [ "`)(*678(*(67HGJ)))", "$1$762b471f$tUY9KN9QsngZ4hpuEuyWo1" ],
+    ]
+
+    for plaintext_password, expected in BY_STANDARD_CRYPT:
+        computed = crypt(plaintext_password, expected)
+        if computed == expected:
+            print(f"match: {plaintext_password:>30} == {computed}")
+        else:
+            print(f"ERROR: {plaintext_password:>30}: {computed} != {expected} !!!")
+
+if __name__ == "__main__":
+    test_crypt()