protege_auth.py
doors version 2.5 · 2 KB
| 1 | """ICT Protege WX login handshake maths, reproduced from the controller's secureLogin.js / encryption.js.""" |
| 2 | import hashlib |
| 3 | |
| 4 | |
| 5 | def wx_xor(text, number): |
| 6 | """encryption.js xor(): XOR each UTF-8 byte of text with the bytes of `number` (lowest byte first, |
| 7 | cycling), as 2-digit uppercase hex.""" |
| 8 | bits = bin(int(number))[2:].rjust(32, "0") |
| 9 | pos, out = len(bits), [] |
| 10 | |
| 11 | def substr(start, length): # JavaScript String.prototype.substr semantics |
| 12 | if start < 0: |
| 13 | start = max(len(bits) + start, 0) |
| 14 | return bits[start:start + length] |
| 15 | |
| 16 | for ch in text.encode("utf-8"): |
| 17 | pos = len(bits) - 8 if pos == 0 else pos - 8 |
| 18 | chunk = substr(pos, 8) |
| 19 | out.append(f"{ch ^ (int(chunk, 2) if chunk else 0):02x}") # parseInt("") is NaN -> XOR with 0 |
| 20 | return "".join(out).upper() |
| 21 | |
| 22 | |
| 23 | def sha1(text): |
| 24 | """jsSHA(text, "TEXT") with 8-bit chars: each JS UTF-16 code unit contributes only its low byte.""" |
| 25 | units = text.encode("utf-16-le") |
| 26 | return hashlib.sha1(bytes(units[0::2])).hexdigest() |
| 27 | |
| 28 | |
| 29 | def login_proof(username, password, challenge): |
| 30 | """-> (name_hash, password_hash) sent with CheckPassword.""" |
| 31 | d = sha1(password) |
| 32 | return (sha1(wx_xor(username.lower(), int(challenge) + 1)).upper(), |
| 33 | sha1(wx_xor(d, int(challenge))).upper()) |
| 34 | |
| 35 | |
| 36 | def session_key(password, reply): |
| 37 | """The 16-char WXKey derived from CheckPassword's reply.""" |
| 38 | return sha1(wx_xor(sha1(password), int(reply))).upper()[:16] |