import time
from random import Random
import re
-import crypt
from PLC.Faults import *
from PLC.Parameter import Parameter, Mixed
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):
"""
database.
"""
+ # this for crypt.crypt means MD5
magic = "$1$"
if len(password) > len(magic) and \
# 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
--- /dev/null
+"""
+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()