// net_classic_encstream_test — ClassicStream with _IMPROVED_PACKET_ENCRYPTION_. // A server-side ClassicCipher plays the peer: HANDSHAKE -> GC_KEY_AGREEMENT -> // (stream replies CG_KEY_AGREEMENT) -> GC_KEY_AGREEMENT_COMPLETED. After that // every byte both ways is CTR-encrypted; the test verifies the stream decrypts // an inbound GC_PHASE and that an outbound CG_MOVE decrypts cleanly server-side. #include "../src/net/classic/classic_cipher.h" #include "../src/net/classic/classic_stream.h" #include "../src/net/classic/wire_classic.h" #include #include #include #include using namespace mtnet::classic; static int g_fail = 0; #define CHECK(c, msg) \ do { \ if (!(c)) { \ std::fprintf(stderr, "FAIL: %s\n", msg); \ ++g_fail; \ } \ } while (0) template static std::vector raw(const T &s) { std::vector b(sizeof(T)); std::memcpy(b.data(), &s, sizeof(T)); return b; } static std::vector drain(ClassicStream &s) { std::vector out; uint8_t buf[2048]; for (size_t n; (n = s.take_outgoing(buf, sizeof(buf)));) { out.insert(out.end(), buf, buf + n); } return out; } int main() { ClassicStream cs; cs.set_polarity(true); // client int phase_seen = -1; std::string err; bool cipher_on = false; cs.on_phase = [&](uint8_t p) { phase_seen = p; }; cs.on_error = [&](const std::string &e) { err = e; }; cs.on_cipher_active = [&]() { cipher_on = true; }; ClassicCipher server; // polarity = false // --- handshake (plaintext) --- { Handshake hs{}; hs.header = HDR_HANDSHAKE; hs.time = 1000; hs.delta = 5; cs.feed(raw(hs).data(), sizeof(hs)); CHECK(drain(cs).size() == sizeof(Handshake), "handshake echo (plaintext)"); } // --- GC_KEY_AGREEMENT: server sends its DH2 blob --- { KeyAgreement srv_pkt{}; srv_pkt.header = HDR_KEY_AGREEMENT; size_t dl = sizeof(srv_pkt.data); size_t agreed = server.prepare(srv_pkt.data, &dl); CHECK(agreed != 0, "server DH2 prepare"); srv_pkt.agreed_length = (uint16_t)agreed; srv_pkt.data_length = (uint16_t)dl; cs.feed(raw(srv_pkt).data(), sizeof(srv_pkt)); // stream must have replied with CG_KEY_AGREEMENT (plaintext, its own blob) auto reply = drain(cs); CHECK(reply.size() == sizeof(KeyAgreement), "CG_KEY_AGREEMENT reply is 261 bytes"); CHECK(reply[0] == HDR_KEY_AGREEMENT, "reply header 0xFB"); KeyAgreement cli_pkt{}; std::memcpy(&cli_pkt, reply.data(), sizeof(cli_pkt)); CHECK(err.empty(), "no error during key agreement"); // server consumes the client's blob CHECK(server.activate(false, cli_pkt.agreed_length, cli_pkt.data, cli_pkt.data_length), "server activate with client blob"); CHECK(!cs.cipher_active(), "stream cipher not active until COMPLETED"); } // --- GC_KEY_AGREEMENT_COMPLETED (plaintext) -> both sides go live --- { KeyAgreementCompleted done{}; done.header = HDR_GC_KEY_AGREEMENT_COMPLETED; cs.feed(raw(done).data(), sizeof(done)); server.set_activated(true); CHECK(cs.cipher_active() && cipher_on, "stream cipher active after COMPLETED"); } // --- server -> client: an ENCRYPTED GC_PHASE(GAME) must decrypt + dispatch --- { auto pkt = raw(Phase_{HDR_GC_PHASE, PHASE_GAME}); server.encrypt(pkt.data(), pkt.size()); CHECK(pkt[0] != HDR_GC_PHASE, "GC_PHASE really is ciphertext on the wire"); cs.feed(pkt.data(), pkt.size()); CHECK(phase_seen == PHASE_GAME, "stream decrypted + dispatched GC_PHASE(GAME)"); } // --- client -> server: an outbound CG_MOVE must be ciphertext the server reads --- { cs.set_sequence_mode(true); CGMove mv{}; mv.header = HDR_CG_MOVE; mv.func = 1; mv.x = 12345; mv.y = 67890; cs.send_fixed(&mv, sizeof(mv)); auto wire = drain(cs); CHECK(wire.size() == sizeof(CGMove) + 1, "CG_MOVE + seq byte on the wire"); CHECK(wire[0] != HDR_CG_MOVE, "CG_MOVE header is encrypted on the wire"); server.decrypt(wire.data(), wire.size()); CGMove got{}; std::memcpy(&got, wire.data(), sizeof(got)); CHECK(got.header == HDR_CG_MOVE && got.func == 1 && got.x == 12345 && got.y == 67890, "server decrypts CG_MOVE body"); CHECK(wire[sizeof(CGMove)] == SEQUENCE_TABLE[0], "seq byte decrypts to table[0]"); } // --- pipelined case: COMPLETED + encrypted packet in one feed() --- { ClassicStream cs2; cs2.set_polarity(true); int seen2 = -1; cs2.on_phase = [&](uint8_t p) { seen2 = p; }; ClassicCipher srv2; Handshake hs{}; hs.header = HDR_HANDSHAKE; cs2.feed(raw(hs).data(), sizeof(hs)); drain(cs2); KeyAgreement sp{}; sp.header = HDR_KEY_AGREEMENT; size_t dl = sizeof(sp.data); sp.agreed_length = (uint16_t)srv2.prepare(sp.data, &dl); sp.data_length = (uint16_t)dl; cs2.feed(raw(sp).data(), sizeof(sp)); auto rep = drain(cs2); KeyAgreement cp{}; std::memcpy(&cp, rep.data(), sizeof(cp)); srv2.activate(false, cp.agreed_length, cp.data, cp.data_length); srv2.set_activated(true); // one buffer: [COMPLETED plaintext][encrypted GC_PHASE(SELECT)] std::vector blob = raw(KeyAgreementCompleted{HDR_GC_KEY_AGREEMENT_COMPLETED, {}}); auto enc = raw(Phase_{HDR_GC_PHASE, PHASE_SELECT}); srv2.encrypt(enc.data(), enc.size()); blob.insert(blob.end(), enc.begin(), enc.end()); cs2.feed(blob.data(), blob.size()); CHECK(seen2 == PHASE_SELECT, "pipelined COMPLETED+encrypted packet handled"); } if (g_fail) { std::fprintf(stderr, "%d check(s) failed\n", g_fail); return 1; } std::puts("net_classic_encstream_test OK"); return 0; }