ZANDBOX SESSION PROTOCOL 0) Public 1024-byte (8192-bit) safeprime p, q=(p-1)/2, g=2. Client and server share client's secret, uniformly random 48-byte psk. hash(in, outlen, key) is keyed BLAKE2s. Quoted strings ("hello-from-cli", "login-mac", ...) are raw ASCII, no NUL. xor(msg, key, iv, ctr) is original ChaCha20 with 32-byte key, 8-byte iv, 8-byte ctr (not IETF 12+4). iv and ctr start as the raw 8-byte hash outputs; ctr is incremented as LEu64, still 8 bytes. Reads/writes after the initial 1056-byte DH hellos are framed with a cleartext LEu32 length prefix. Client and server speak in lock-step (client first, call-response) and share one {mac,key,iv,ctr} state. The reader does not consume the next record until it has written its reply to the current one. Close = write nothing more; TCP RST. Hang = write nothing more; leave sock hanging (no RST/FIN/nothing). All 1056 hello bytes within 3 s of accept. Else hang. Framed session mismatch or idle timeout (30 s): close. Session cap: 2^31-64 bytes of payload. Single frame cap: 2^28 bytes of payload. 1) Client picks a uniform 48-byte string and interprets it as an unsigned BE integer x with 1 <= x <= q-1 and 2 <= g^x <= p-2 (picks new until true). Really pick a uniformly random string. Server will lazily remember and ignore repeats sooner or later. 2) Client writes exactly 1056 bytes (no frame): g^x (1024-byte unsigned BE) || hash(g^x, 32, hash(psk, 32, "hello-from-cli")). 3) Server reads exactly 1056 bytes. The hello must arrive within 3 s of accept, else hang. Then checks 2 <= g^x <= p-2 and the tag against every psk it knows. No subgroup check: every integer from 2 to p-2 is a power of g=2 (mod p). 4) If the completed hello has a bad g^x, a tag that matches no psk, or a remembered tag: hang. 5) Server is satisfied that client is authenticated (knows the unique secret psk). 6) Server picks y the same way as x. 7) Server writes exactly 1056 bytes (no frame): g^y (1024-byte unsigned BE) || hash(g^y, 32, hash(psk, 32, "hello-from-srv")). 8) Client reads exactly 1056 bytes, checks 2 <= g^y <= p-2 and the tag. Fail: close. 9) Client is satisfied that server is authenticated (knows the unique secret psk). Hello is complete; all later bytes are framed session traffic. 10) Both set z = (g^{xy} mod p) || g^x || g^y || psk (group elements as 1024-byte unsigned BE; psk raw 48 bytes). 11) Both set mac = hash(z, 32, "login-mac") key = hash(z, 32, "login-key") iv = hash(z, 8, "login-iv") ctr = hash(z, 8, "session-ctr") then wipe x/y, g^x, g^y, z. Live state is {mac, key, iv, ctr} only. 12) Session start. Client speaks first, server replies, lock-step. Max 30 s between client commands, else close. 13) secwrite(m) = { if (len(m) > 2^28) error; ct = xor(pad(m), key, iv, ctr); // ctr as 8 bytes tag = hash(ct || ctr || len(ct), 32, mac); // len(ct) as 4 bytes LEu32 write(ct || tag); // frame: LEu32 len(ct || tag) ratchet(); } 14) secread() = { r = read(); // one frame (LEu32 prefix) n = len(r) - 32; // payload len (exclude 32-byte tag) if (n<256 or n>2^28) close; // min bucket 256, max allowed 2^28 if (n<>256 and n<>4096 and (n mod 16384)<>0) close; // allowed buckets ct = r[:-32]; tag = r[-32:]; if (tag != hash(ct || ctr || len(ct), 32, mac)) close; m = xor(ct, key, iv, ctr); ratchet(); return unpad(m); } 15) pad(m) = { need = len(m) + 4; bucket = need<=256 ? 256 : need<=4096 ? 4096 : 16384*ceil(need/16384); return rpad(len(m) || m, bucket, anything); // len(m) as LEu32 } 16) unpad(m) = { b = len(m); if (b!=256 and b!=4096 and (b<16384 or b%16384!=0)) close; n = from_LEu32(m[0:4]); if (len(m) < 4+n) close; return m[4 : 4+n]; // trailing pad ignored } 17) ratchet() = // both sides, after every record { s = mac || key || iv || ctr; mac = hash(s, 32, "ratchet-mac"); key = hash(s, 32, "ratchet-key"); iv = hash(s, 8, "ratchet-iv"); ctr += max(1, ceil(len(ct) / 64)); // LEu64, wraps wipe s and previous mac, key, iv; } 18) p = // Base64, 1024 bytes 3sY/LoWvKEHUu/R3QwF5gJB2JTmNjmcUzrYG2MVyYoZhSX+TbkuG79/Ptnd0prQdewz7IsYlKMYS p45whqwbSs4rqzmQZ9vQwkcexfiPctF57ozrqVUXmd4JQ7b2PeNb6zCY1QNwpzpd1yI6Q2kWeLYQ nlomGf/Qw2DleozzJJyBDUkotJEQx3DgAho3JedMaMwqA1yV2c5utPoFWTK/T8T6iUCtiCBVjV8Z IIS0sfSiG78NaKQFobENbSh66lBRgkT5bv0LZCraNRyMfjs0cClHo6prWFSdPX1jR25rPYz4gbvH mYqe+fDolXGjORXhyv4dZbfGkYPudWqtj2MV+KaamYcB5GOm6EkyqFZO0hekZP9if3CmvrcBjnUJ wqJBxSXn633aAB41FFWl3BVAT70kGwiYU1PD/oa5D7ZUAnjjrK49yIc/GC3+rIF1TSlIVnclSuUb YEKDRtwxsA/pMmcOsbgLPBde98kNg5QNHb4zSlk4qpm0jlV/lNkTzfANFEIHbVez7oYR8+yso0xj 2N42rGBWWFcQ6sJBFnldnL3g6PNQIcIMF/ITCVZek7VtcMPV4f8wDi3wMOhf+sYI/vnP6Z/vuQNM OX29VM6ForSlQfRoPR+UKDbPOJwXJ8h2K+BviEIrwsSTU8/4DSKBP+YVIbczOYsr/vy2blm6eBPD vmS7Bht8pOqWfrxPS+kMlyjNVoPsUAEBP3Pga4D6QMhBKwMPPn97jcQ9XH9AC0yrjXW1NXthOzZw 7EZ4FMs5JQzxt5DtGs/JS834hG0Gx0TRzDtoSc4DgSlM1sNVMA3NqNpl4wOblYm6yVjlld0yBTOC n2dLogRnq1u9x7NLSgH1Oc9rcKPG7ef0fy4YRQvKGu3UFFTShuo2TEqWH7j/8F6/avQTs8FiO7MF 20MpGD3t1gBoLwbiO9Bm5HhtzBhJkF8O+zKWnpVrdfRkVocaMRRpnKBwfPRvO9u8gFnNaBJTHbu0 pNn7YcWpwl8LGfYkylTh41gOS8W9qauSKbmMr418Rf7SsESDYaYHgW5iHGO74svtHmQGZPTpw18U BHgvIuDltYS0+5Dju96c4H8k8t9eW8ohQu7XZK2TTHUY7t2ND/pTr9+bd5fyq8es0Ngp8LbWcIq9 vTpohs7dekiB8DTQUaSf2UWDrvUi1N2kk4gNAg0TBEBFhhcrr+84kPrpRC9aLGZAIOPzZ1c8X49k Jk08Z2bp5rjXWAVr+TwnPzrPYHF/0jTheGGVbVquaVBkOQCU6EgZQu7ooNaM6nR+9EGcs9DDMOq9 9KaVFWtCDs+ye7HuKTMzO6NHM/Zsq5BtCvkRLZdU1gGRRuPCUe6KXvlj0k1VvSbTU0mMMEtPew==