protege_crypto.py
doors version 2.5 · 2 KB
| 1 | """ |
| 2 | ICT Protege WX web-interface encryption, reproduced from the controller's own mcrypt.js / encryption.js. |
| 3 | |
| 4 | Rijndael-128 (AES) in CBC mode: |
| 5 | request = SESSID + HEX( IV(16 printable chars) + AES-CBC(key, IV, PKCS7(utf8(text))) ) |
| 6 | response = HEX( IV + AES-CBC(...) ) -> strip trailing NULs, strip PKCS7, utf8 |
| 7 | The key is the session's WXKey string used byte-for-byte (latin-1), NUL-padded to 16/24/32 bytes |
| 8 | like mcrypt does - so its length picks AES-128/192/256. |
| 9 | """ |
| 10 | import random |
| 11 | |
| 12 | try: |
| 13 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
| 14 | HAVE_CRYPTO = True |
| 15 | except ImportError: # optional - only needed for the ICT Protege WX door source |
| 16 | HAVE_CRYPTO = False |
| 17 | |
| 18 | |
| 19 | def _require_crypto(): |
| 20 | if not HAVE_CRYPTO: |
| 21 | raise RuntimeError("the 'cryptography' package is required for the ICT Protege WX integration " |
| 22 | "- run: sudo -u nvr /opt/localeye/venv/bin/pip install cryptography") |
| 23 | |
| 24 | |
| 25 | def _key(key): |
| 26 | k = key.encode("latin-1") |
| 27 | for size in (16, 24, 32): |
| 28 | if len(k) <= size: |
| 29 | return k + b"\0" * (size - len(k)) |
| 30 | raise ValueError("WX key longer than 32 characters") |
| 31 | |
| 32 | |
| 33 | def _pkcs7(b): |
| 34 | n = 16 - len(b) % 16 # the WX always adds padding (16 bytes when already aligned) |
| 35 | return b + bytes([n]) * n |
| 36 | |
| 37 | |
| 38 | def encrypt(text, key, sessid="", iv=None): |
| 39 | _require_crypto() |
| 40 | iv = iv or "".join(chr(int(75 * random.random() + 48)) for _ in range(16)) # same alphabet as the WX page |
| 41 | iv_b = iv.encode("latin-1") |
| 42 | enc = Cipher(algorithms.AES(_key(key)), modes.CBC(iv_b)).encryptor() |
| 43 | body = enc.update(_pkcs7(str(text).encode("utf-8"))) + enc.finalize() |
| 44 | return sessid + (iv_b + body).hex().upper() |
| 45 | |
| 46 | |
| 47 | def decrypt(hextext, key): |
| 48 | _require_crypto() |
| 49 | raw = bytes.fromhex(hextext.strip()) |
| 50 | iv, body = raw[:16], raw[16:] |
| 51 | body += b"\0" * (-len(body) % 16) # mcrypt zero-pads short input to a whole block |
| 52 | dec = Cipher(algorithms.AES(_key(key)), modes.CBC(iv)).decryptor() |
| 53 | out = (dec.update(body) + dec.finalize()).rstrip(b"\0") |
| 54 | if out: |
| 55 | out = out[:-out[-1]] if 0 < out[-1] <= 16 else out |
| 56 | return out.decode("utf-8", "replace") |