localeye.cam

← Back

protege_crypto.py

doors version 2.5 · 2 KB

1"""
2ICT Protege WX web-interface encryption, reproduced from the controller's own mcrypt.js / encryption.js.
3
4Rijndael-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
7The key is the session's WXKey string used byte-for-byte (latin-1), NUL-padded to 16/24/32 bytes
8like mcrypt does - so its length picks AES-128/192/256.
9"""
10import random
11
12try:
13 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
14 HAVE_CRYPTO = True
15except ImportError: # optional - only needed for the ICT Protege WX door source
16 HAVE_CRYPTO = False
17
18
19def _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
25def _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
33def _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
38def 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
47def 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")