encryption system
pyjiit/encryption.py plus pyjiit/utils.py. How __hit uses headers: Webportal class. Risks: Security considerations.
flowchart LR Utils["utils.generate_date_seq"] --> Key["generate_key"] Utils2["utils.get_random_char_seq"] --> LN["generate_local_name"] Utils --> LN Key --> Crypt["get_crypt AES CBC"] IV["IV"] --> Crypt Crypt --> Enc["encrypt / decrypt"] Enc --> Ser["serialize_payload / deserialize_payload"]
Constants#
IV = b"dcek9wb8frty1pnm" # 16 bytes, never rotated
AES mode is CBC. Padding is Crypto.Util.Padding pad / unpad with block size 16.
Date sequence#
generate_date_seq(date=None) in utils.py:
- Day and month zero-padded to 2 digits, year last two digits, weekday
(date.weekday()+1) % 7(Sunday becomes 0). - Interleave:
day[0] + month[0] + year[0] + weekday + day[1] + month[1] + year[1].
Tuesday 5 March 2024: day 05, month 03, year 24, weekday 2. Sequence 0+0+2+2+5+3+4 = 0022534.
flowchart LR D["dd"] --> I0["d0"] M["mm"] --> A0["m0"] Y["yy"] --> R0["y0"] W["weekday mod"] --> T["t"] D --> I1["d1"] M --> A1["m1"] Y --> R1["y1"] I0 --> Seq["7-char seq"] A0 --> Seq R0 --> Seq T --> Seq I1 --> Seq A1 --> Seq R1 --> Seq
Key#
def generate_key(date=None) -> bytes:
return ("qa8y" + pyjiit.utils.generate_date_seq(date) + "ty1pn").encode()
16 ASCII bytes, so AES-128, not 32-byte AES-256. Docstrings that say 32-byte are wrong.
LocalName#
def generate_local_name(date=None) -> str:
name_bytes = (
pyjiit.utils.get_random_char_seq(4)
+ pyjiit.utils.generate_date_seq(date)
+ pyjiit.utils.get_random_char_seq(5)
).encode()
return base64.b64encode(encrypt(name_bytes)).decode()
get_random_char_seq draws from string.digits + string.ascii_letters.
flowchart LR R4["4 random"] --> Name["16-char name"] Seq["date seq"] --> Name R5["5 random"] --> Name Name --> Enc["encrypt"] Enc --> B64["base64 header"]
serialize / deserialize#
def serialize_payload(payload: dict) -> str:
raw = json.dumps(payload, separators=(",", ":")).encode()
pbytes = encrypt(raw)
return base64.b64encode(pbytes).decode()
def deserialize_payload(payload: str) -> dict:
pbytes = base64.b64decode(payload)
raw = decrypt(pbytes)
return json.loads(raw)
Compact JSON matters. Extra spaces would change the ciphertext and the portal would reject it.
encryption.py has a __main__ that deserialize_payload(sys.argv[1]) and prints the dict.
sequenceDiagram participant WP as Webportal participant Enc as encryption participant API as portal WP->>Enc: serialize_payload(dict) Enc-->>WP: base64 ciphertext WP->>API: POST body plus LocalName Note over Enc: decrypt is available but __hit uses .json() on the HTTP response
__hit does not call deserialize_payload on responses. It parses JSON from requests. Encryption is a request-body trick the portal invented, not a full envelope on every response.
get_crypt#
def get_crypt(key: bytes, iv: bytes):
return AES.new(key, AES.MODE_CBC, iv)
encrypt and decrypt each build a fresh cipher from generate_key() and IV. You cannot reuse a cipher object across calls, and you should not try.