From 0fcaae54c318575fb12d03e923e806812b60dad1 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 10 Mar 2026 21:05:52 -0500 Subject: [PATCH] Reliable, secure, serial link protocol added. --- packet/Makefile | 38 +++ packet/README.md | 215 ++++++++++++++ packet/packet.c | 523 +++++++++++++++++++++++++++++++++ packet/packet.h | 68 +++++ rs232/README.md | 212 ++++++++++++++ rs232/rs232.c | 4 +- rs232/rs232.h | 5 +- seclink/Makefile | 38 +++ seclink/README.md | 295 +++++++++++++++++++ seclink/secLink.c | 338 ++++++++++++++++++++++ seclink/secLink.h | 74 +++++ security/Makefile | 38 +++ security/README.md | 259 +++++++++++++++++ security/security.c | 688 ++++++++++++++++++++++++++++++++++++++++++++ security/security.h | 47 +++ 15 files changed, 2838 insertions(+), 4 deletions(-) create mode 100644 packet/Makefile create mode 100644 packet/README.md create mode 100644 packet/packet.c create mode 100644 packet/packet.h create mode 100644 rs232/README.md create mode 100644 seclink/Makefile create mode 100644 seclink/README.md create mode 100644 seclink/secLink.c create mode 100644 seclink/secLink.h create mode 100644 security/Makefile create mode 100644 security/README.md create mode 100644 security/security.c create mode 100644 security/security.h diff --git a/packet/Makefile b/packet/Makefile new file mode 100644 index 0000000..2001eff --- /dev/null +++ b/packet/Makefile @@ -0,0 +1,38 @@ +# Packet Serial Transport Makefile for DJGPP cross-compilation + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar +RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 + +OBJDIR = ../obj/packet +LIBDIR = ../lib + +SRCS = packet.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGET = $(LIBDIR)/libpacket.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) | $(LIBDIR) + $(AR) rcs $@ $(OBJS) + $(RANLIB) $@ + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(LIBDIR): + mkdir -p $(LIBDIR) + +# Dependencies +$(OBJDIR)/packet.o: packet.c packet.h ../rs232/rs232.h + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/packet/README.md b/packet/README.md new file mode 100644 index 0000000..450e79c --- /dev/null +++ b/packet/README.md @@ -0,0 +1,215 @@ +# Packet — Reliable Serial Transport + +Packetized serial transport with HDLC-style framing, CRC-16 error +detection, and a Go-Back-N sliding window protocol for reliable, +ordered delivery over an unreliable serial link. + +## Architecture + +``` + Application + | + [packet] framing, CRC, retransmit, ordering + | + [rs232] raw byte I/O +``` + +The packet layer sits on top of an already-open rs232 COM port. It +does not open or close the serial port itself. + +## Frame Format + +Before byte stuffing: + +``` +[0x7E] [SEQ] [TYPE] [LEN_LO] [LEN_HI] [PAYLOAD...] [CRC_LO] [CRC_HI] +``` + +| Field | Size | Description | +|---------|---------|--------------------------------------| +| `0x7E` | 1 byte | Frame delimiter (flag byte) | +| `SEQ` | 1 byte | Sequence number (wrapping uint8) | +| `TYPE` | 1 byte | Frame type (see below) | +| `LEN` | 2 bytes | Payload length, little-endian | +| Payload | 0-256 | Application data | +| `CRC` | 2 bytes | CRC-16-CCITT over SEQ+TYPE+LEN+payload | + +### Frame Types + +| Type | Value | Description | +|--------|-------|----------------------------------------------| +| `DATA` | 0x00 | Data frame carrying application payload | +| `ACK` | 0x01 | Cumulative acknowledgment (next expected seq) | +| `NAK` | 0x02 | Negative ack (request retransmit from seq) | +| `RST` | 0x03 | Connection reset | + +### Byte Stuffing + +The flag byte (`0x7E`) and escape byte (`0x7D`) are escaped within +frame data: + +- `0x7E` becomes `0x7D 0x5E` +- `0x7D` becomes `0x7D 0x5D` + +## Reliability + +The protocol uses Go-Back-N with a configurable sliding window +(1-8 slots, default 4): + +- **Sender** assigns sequential numbers to each DATA frame and retains + a copy in the retransmit buffer until acknowledged. +- **Receiver** delivers frames in order. Out-of-order frames trigger a + NAK for the expected sequence number. +- **ACK** carries the next expected sequence number (cumulative). +- **NAK** triggers retransmission of the requested frame and all + subsequent unacknowledged frames. +- **Timer-based retransmit** fires after 500 poll cycles if no ACK or + NAK has been received. + +## API Reference + +### Types + +```c +// Receive callback — called for each verified, in-order packet +typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); + +// Opaque connection handle +typedef struct PktConnS PktConnT; +``` + +### Constants + +| Name | Value | Description | +|-----------------------|-------|-------------------------------------| +| `PKT_MAX_PAYLOAD` | 256 | Max payload bytes per packet | +| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size | +| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size | +| `PKT_SUCCESS` | 0 | Success | +| `PKT_ERR_INVALID_PORT`| -1 | Invalid COM port | +| `PKT_ERR_NOT_OPEN` | -2 | Connection not open | +| `PKT_ERR_ALREADY_OPEN`| -3 | Connection already open | +| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block | +| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow | +| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter | +| `PKT_ERR_TX_FULL` | -7 | Transmit window full | +| `PKT_ERR_NO_DATA` | -8 | No data available | + +### Functions + +#### pktOpen + +```c +PktConnT *pktOpen(int com, int windowSize, + PktRecvCallbackT callback, void *callbackCtx); +``` + +Creates a packetized connection over an already-open COM port. + +- `com` — RS232 port index (`RS232_COM1`..`RS232_COM4`) +- `windowSize` — sliding window size (1-8), 0 for default (4) +- `callback` — called from `pktPoll()` for each received packet +- `callbackCtx` — user pointer passed to callback + +Returns a connection handle, or `NULL` on failure. + +#### pktClose + +```c +void pktClose(PktConnT *conn); +``` + +Frees the connection state. Does **not** close the underlying COM port. + +#### pktSend + +```c +int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); +``` + +Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`. + +- `block = true` — waits for window space, polling for ACKs internally +- `block = false` — returns `PKT_ERR_TX_FULL` if the window is full + +The packet is stored in the retransmit buffer until acknowledged. + +#### pktPoll + +```c +int pktPoll(PktConnT *conn); +``` + +Reads available serial data, processes received frames, sends ACKs and +NAKs, and checks retransmit timers. Returns the number of DATA packets +delivered to the callback. + +Must be called frequently (e.g. in your main loop). + +#### pktReset + +```c +int pktReset(PktConnT *conn); +``` + +Resets all sequence numbers and buffers to zero. Sends a RST frame to +the remote side so it resets as well. + +#### pktGetPending + +```c +int pktGetPending(PktConnT *conn); +``` + +Returns the number of unacknowledged packets currently in the transmit +window. Useful for throttling sends in non-blocking mode. + +## Example + +```c +#include "packet.h" +#include "../rs232/rs232.h" + +void onPacket(void *ctx, const uint8_t *data, int len) { + // process received packet +} + +int main(void) { + // Open serial port first + rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE); + + // Create packet connection with default window size + PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL); + + // Send a packet (blocking) + uint8_t msg[] = "Hello, packets!"; + pktSend(conn, msg, sizeof(msg), true); + + // Main loop + while (1) { + int delivered = pktPoll(conn); + // delivered = number of packets received this iteration + } + + pktClose(conn); + rs232Close(RS232_COM1); + return 0; +} +``` + +## CRC + +CRC-16-CCITT (polynomial 0x1021, init 0xFFFF) computed via a 256-entry +lookup table (512 bytes). The CRC covers the SEQ, TYPE, LEN, and +payload fields. + +## Building + +``` +make # builds ../lib/libpacket.a +make clean # removes objects and library +``` + +Requires `librs232.a` at link time. + +Target: DJGPP cross-compiler, 486+ CPU. diff --git a/packet/packet.c b/packet/packet.c new file mode 100644 index 0000000..bce3fe2 --- /dev/null +++ b/packet/packet.c @@ -0,0 +1,523 @@ +// Packetized serial transport with HDLC-style framing and sliding window +// +// Frame format (before byte stuffing): +// [0x7E] [SEQ] [TYPE] [LEN_LO] [LEN_HI] [PAYLOAD...] [CRC_LO] [CRC_HI] +// +// Byte stuffing: +// 0x7E -> 0x7D 0x5E +// 0x7D -> 0x7D 0x5D +// +// CRC-16-CCITT over SEQ+TYPE+LEN+PAYLOAD + +#include +#include +#include +#include +#include +#include "packet.h" +#include "../rs232/rs232.h" + + +// ======================================================================== +// Internal defines +// ======================================================================== + +#define FLAG_BYTE 0x7E +#define ESC_BYTE 0x7D +#define ESC_XOR 0x20 + +// Frame types +#define FRAME_DATA 0x00 +#define FRAME_ACK 0x01 +#define FRAME_NAK 0x02 +#define FRAME_RST 0x03 + +// Header size: SEQ + TYPE + LEN_LO + LEN_HI +#define HEADER_SIZE 4 +// CRC size +#define CRC_SIZE 2 +// Minimum frame size (header + CRC, no payload) +#define MIN_FRAME_SIZE (HEADER_SIZE + CRC_SIZE) +// Maximum raw frame size (header + max payload + CRC) +#define MAX_FRAME_SIZE (HEADER_SIZE + PKT_MAX_PAYLOAD + CRC_SIZE) +// Maximum stuffed frame size (worst case: every byte stuffed + flag) +#define MAX_STUFFED_SIZE (1 + MAX_FRAME_SIZE * 2) + +// Receive buffer must hold at least one max-size stuffed frame +#define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64) + +// Retransmit timeout in poll cycles (caller-dependent; ~50ms worth at typical poll rates) +#define RETRANSMIT_TIMEOUT 500 + +// Receive state machine +#define RX_STATE_HUNT 0 // scanning for FLAG_BYTE +#define RX_STATE_ACTIVE 1 // receiving frame data +#define RX_STATE_ESCAPE 2 // next byte is escaped + + +// ======================================================================== +// Types +// ======================================================================== + +// Transmit window slot +typedef struct { + uint8_t data[PKT_MAX_PAYLOAD]; + int len; + uint8_t seq; + uint32_t timer; +} TxSlotT; + +// Connection state +struct PktConnS { + int com; + int windowSize; + PktRecvCallbackT callback; + void *callbackCtx; + + // Transmit state + uint8_t txNextSeq; // next sequence number to assign + uint8_t txAckSeq; // oldest unacknowledged sequence + TxSlotT txSlots[PKT_MAX_WINDOW]; + int txCount; // number of slots in use + + // Receive state + uint8_t rxExpectSeq; // next expected sequence number + uint8_t rxState; // RX_STATE_* + uint8_t rxFrame[MAX_FRAME_SIZE]; + int rxFrameLen; + + // Poll counter (simple timer) + uint32_t pollCount; +}; + + +// ======================================================================== +// CRC-16-CCITT lookup table +// ======================================================================== + +static const uint16_t sCrcTable[256] = { + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x54A5, + 0xA54A, 0xB56B, 0x8508, 0x9529, 0xE5CE, 0xF5EF, 0xC58C, 0xD5AD, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x4864, 0x5845, 0x6826, 0x7807, 0x08E0, 0x18C1, 0x28A2, 0x38A3, + 0xC94C, 0xD96D, 0xE90E, 0xF92F, 0x89C8, 0x99E9, 0xA98A, 0xB9AB, + 0x5A55, 0x4A74, 0x7A17, 0x6A36, 0x1AD1, 0x0AF0, 0x3A93, 0x2AB2, + 0xDB5D, 0xCB7C, 0xFB1F, 0xEB3E, 0x9BD9, 0x8BF8, 0xBB9B, 0xAB9A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x06B1, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, + 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, + 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 +}; + + +// ======================================================================== +// Static prototypes (alphabetical) +// ======================================================================== + +static uint16_t crcCalc(const uint8_t *data, int len); +static void processFrame(PktConnT *conn, const uint8_t *frame, int len); +static void retransmitCheck(PktConnT *conn); +static void rxProcessByte(PktConnT *conn, uint8_t byte); +static void sendAck(PktConnT *conn, uint8_t seq); +static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len); +static void sendNak(PktConnT *conn, uint8_t seq); +static void sendRst(PktConnT *conn); +static int seqInWindow(uint8_t seq, uint8_t base, int size); +static int txSlotIndex(PktConnT *conn, uint8_t seq); + + +// ======================================================================== +// CRC computation +// ======================================================================== + +static uint16_t crcCalc(const uint8_t *data, int len) { + uint16_t crc = 0xFFFF; + + for (int i = 0; i < len; i++) { + crc = (crc << 8) ^ sCrcTable[(crc >> 8) ^ data[i]]; + } + + return crc; +} + + +// ======================================================================== +// Frame transmission +// ======================================================================== + +static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) { + uint8_t raw[MAX_FRAME_SIZE]; + uint8_t stuffed[MAX_STUFFED_SIZE]; + int rawLen; + uint16_t crc; + int out; + + // Build raw frame: SEQ + TYPE + LEN_LO + LEN_HI + PAYLOAD + raw[0] = seq; + raw[1] = type; + raw[2] = (uint8_t)(len & 0xFF); + raw[3] = (uint8_t)((len >> 8) & 0xFF); + if (payload && len > 0) { + memcpy(&raw[4], payload, len); + } + rawLen = HEADER_SIZE + len; + + // Append CRC + crc = crcCalc(raw, rawLen); + raw[rawLen] = (uint8_t)(crc & 0xFF); + raw[rawLen + 1] = (uint8_t)((crc >> 8) & 0xFF); + rawLen += CRC_SIZE; + + // Byte-stuff into output buffer with leading flag + out = 0; + stuffed[out++] = FLAG_BYTE; + for (int i = 0; i < rawLen; i++) { + if (raw[i] == FLAG_BYTE) { + stuffed[out++] = ESC_BYTE; + stuffed[out++] = FLAG_BYTE ^ ESC_XOR; + } else if (raw[i] == ESC_BYTE) { + stuffed[out++] = ESC_BYTE; + stuffed[out++] = ESC_BYTE ^ ESC_XOR; + } else { + stuffed[out++] = raw[i]; + } + } + + // Send via serial port (blocking write) + rs232Write(conn->com, (const char *)stuffed, out); +} + + +static void sendAck(PktConnT *conn, uint8_t seq) { + sendFrame(conn, seq, FRAME_ACK, 0, 0); +} + + +static void sendNak(PktConnT *conn, uint8_t seq) { + sendFrame(conn, seq, FRAME_NAK, 0, 0); +} + + +static void sendRst(PktConnT *conn) { + sendFrame(conn, 0, FRAME_RST, 0, 0); +} + + +// ======================================================================== +// Sequence number helpers +// ======================================================================== + +static int seqInWindow(uint8_t seq, uint8_t base, int size) { + uint8_t diff = seq - base; + return diff < (uint8_t)size; +} + + +static int txSlotIndex(PktConnT *conn, uint8_t seq) { + uint8_t diff = seq - conn->txAckSeq; + if (diff >= (uint8_t)conn->txCount) { + return -1; + } + return diff; +} + + +// ======================================================================== +// Frame processing +// ======================================================================== + +static void processFrame(PktConnT *conn, const uint8_t *frame, int len) { + uint8_t seq; + uint8_t type; + int payloadLen; + uint16_t rxCrc; + uint16_t calcCrc; + + if (len < MIN_FRAME_SIZE) { + return; + } + + // Verify CRC + calcCrc = crcCalc(frame, len - CRC_SIZE); + rxCrc = frame[len - 2] | ((uint16_t)frame[len - 1] << 8); + if (calcCrc != rxCrc) { + // CRC mismatch — request retransmit of what we expect + sendNak(conn, conn->rxExpectSeq); + return; + } + + seq = frame[0]; + type = frame[1]; + payloadLen = frame[2] | ((int)frame[3] << 8); + + // Validate payload length against actual frame size + if (payloadLen + MIN_FRAME_SIZE != len) { + return; + } + + switch (type) { + case FRAME_DATA: + if (seq == conn->rxExpectSeq) { + // In-order delivery + if (conn->callback) { + conn->callback(conn->callbackCtx, &frame[HEADER_SIZE], payloadLen); + } + conn->rxExpectSeq++; + sendAck(conn, conn->rxExpectSeq); + } else if (seqInWindow(seq, conn->rxExpectSeq, conn->windowSize)) { + // Out of order but in window — NAK the one we want + sendNak(conn, conn->rxExpectSeq); + } + // else: duplicate or out of window, silently discard + break; + + case FRAME_ACK: { + // ACK carries the next expected sequence number (cumulative) + // Advance txAckSeq and free all slots up to seq + while (conn->txCount > 0 && conn->txAckSeq != seq) { + conn->txAckSeq++; + conn->txCount--; + } + break; + } + + case FRAME_NAK: { + // Retransmit from the requested sequence + int idx = txSlotIndex(conn, seq); + if (idx >= 0) { + // Retransmit this slot and all after it (go-back-N) + for (int i = idx; i < conn->txCount; i++) { + TxSlotT *slot = &conn->txSlots[i]; + sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); + slot->timer = conn->pollCount; + } + } + break; + } + + case FRAME_RST: + // Remote requested reset — clear state and respond with RST + conn->txNextSeq = 0; + conn->txAckSeq = 0; + conn->txCount = 0; + conn->rxExpectSeq = 0; + sendRst(conn); + break; + } +} + + +// ======================================================================== +// Receive byte processing (state machine) +// ======================================================================== + +static void rxProcessByte(PktConnT *conn, uint8_t byte) { + switch (conn->rxState) { + case RX_STATE_HUNT: + if (byte == FLAG_BYTE) { + conn->rxState = RX_STATE_ACTIVE; + conn->rxFrameLen = 0; + } + break; + + case RX_STATE_ACTIVE: + if (byte == FLAG_BYTE) { + // End of frame (or start of next) + if (conn->rxFrameLen >= MIN_FRAME_SIZE) { + processFrame(conn, conn->rxFrame, conn->rxFrameLen); + } + // Reset for next frame + conn->rxFrameLen = 0; + } else if (byte == ESC_BYTE) { + conn->rxState = RX_STATE_ESCAPE; + } else { + if (conn->rxFrameLen < MAX_FRAME_SIZE) { + conn->rxFrame[conn->rxFrameLen++] = byte; + } else { + // Frame too large, discard and hunt for next flag + conn->rxState = RX_STATE_HUNT; + } + } + break; + + case RX_STATE_ESCAPE: + conn->rxState = RX_STATE_ACTIVE; + byte ^= ESC_XOR; + if (conn->rxFrameLen < MAX_FRAME_SIZE) { + conn->rxFrame[conn->rxFrameLen++] = byte; + } else { + conn->rxState = RX_STATE_HUNT; + } + break; + } +} + + +// ======================================================================== +// Retransmit check +// ======================================================================== + +static void retransmitCheck(PktConnT *conn) { + for (int i = 0; i < conn->txCount; i++) { + TxSlotT *slot = &conn->txSlots[i]; + if (conn->pollCount - slot->timer >= RETRANSMIT_TIMEOUT) { + sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); + slot->timer = conn->pollCount; + } + } +} + + +// ======================================================================== +// Public functions (alphabetical) +// ======================================================================== + +void pktClose(PktConnT *conn) { + if (conn) { + free(conn); + } +} + + +int pktGetPending(PktConnT *conn) { + if (!conn) { + return PKT_ERR_INVALID_PARAM; + } + return conn->txCount; +} + + +PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx) { + PktConnT *conn; + + if (windowSize <= 0) { + windowSize = PKT_DEFAULT_WINDOW; + } + if (windowSize > PKT_MAX_WINDOW) { + windowSize = PKT_MAX_WINDOW; + } + + conn = (PktConnT *)calloc(1, sizeof(PktConnT)); + if (!conn) { + return 0; + } + + conn->com = com; + conn->windowSize = windowSize; + conn->callback = callback; + conn->callbackCtx = callbackCtx; + conn->txNextSeq = 0; + conn->txAckSeq = 0; + conn->txCount = 0; + conn->rxExpectSeq = 0; + conn->rxState = RX_STATE_HUNT; + conn->rxFrameLen = 0; + conn->pollCount = 0; + + return conn; +} + + +int pktPoll(PktConnT *conn) { + char buf[128]; + int nRead; + int delivered = 0; + + if (!conn) { + return PKT_ERR_INVALID_PARAM; + } + + conn->pollCount++; + + // Read available serial data and feed to state machine + while ((nRead = rs232Read(conn->com, buf, sizeof(buf))) > 0) { + for (int i = 0; i < nRead; i++) { + uint8_t prevExpect = conn->rxExpectSeq; + rxProcessByte(conn, (uint8_t)buf[i]); + if (conn->rxExpectSeq != prevExpect) { + delivered++; + } + } + } + + // Check for retransmit timeouts + retransmitCheck(conn); + + return delivered; +} + + +int pktReset(PktConnT *conn) { + if (!conn) { + return PKT_ERR_INVALID_PARAM; + } + + conn->txNextSeq = 0; + conn->txAckSeq = 0; + conn->txCount = 0; + conn->rxExpectSeq = 0; + conn->rxState = RX_STATE_HUNT; + conn->rxFrameLen = 0; + + sendRst(conn); + + return PKT_SUCCESS; +} + + +int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) { + TxSlotT *slot; + + if (!conn) { + return PKT_ERR_INVALID_PARAM; + } + if (!data || len <= 0 || len > PKT_MAX_PAYLOAD) { + return PKT_ERR_INVALID_PARAM; + } + + // Wait for window space if blocking + if (block) { + while (conn->txCount >= conn->windowSize) { + pktPoll(conn); + } + } else { + if (conn->txCount >= conn->windowSize) { + return PKT_ERR_TX_FULL; + } + } + + // Store in retransmit buffer + slot = &conn->txSlots[conn->txCount]; + memcpy(slot->data, data, len); + slot->len = len; + slot->seq = conn->txNextSeq; + slot->timer = conn->pollCount; + conn->txCount++; + conn->txNextSeq++; + + // Transmit + sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len); + + return PKT_SUCCESS; +} diff --git a/packet/packet.h b/packet/packet.h new file mode 100644 index 0000000..21a4935 --- /dev/null +++ b/packet/packet.h @@ -0,0 +1,68 @@ +// Packetized serial transport with HDLC-style framing and sliding window +// Provides reliable, ordered delivery over an unreliable serial link. + +#ifndef PACKET_H +#define PACKET_H + +#include +#include + +// Maximum payload per packet (excluding header/CRC) +#define PKT_MAX_PAYLOAD 256 + +// Default sliding window size (1-8) +#define PKT_DEFAULT_WINDOW 4 + +// Maximum window size +#define PKT_MAX_WINDOW 8 + +// Error codes +#define PKT_SUCCESS 0 +#define PKT_ERR_INVALID_PORT -1 +#define PKT_ERR_NOT_OPEN -2 +#define PKT_ERR_ALREADY_OPEN -3 +#define PKT_ERR_WOULD_BLOCK -4 +#define PKT_ERR_OVERFLOW -5 +#define PKT_ERR_INVALID_PARAM -6 +#define PKT_ERR_TX_FULL -7 +#define PKT_ERR_NO_DATA -8 + +// Callback for received packets +// ctx: user context pointer +// data: payload (valid only during callback) +// len: payload length +typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); + +// Connection handle (opaque) +typedef struct PktConnS PktConnT; + + +// Open a packetized connection over a COM port. +// com: RS232 COM port index (RS232_COM1..RS232_COM4), must already be open +// windowSize: sliding window size (1..PKT_MAX_WINDOW), 0 for default +// callback: called when a complete, verified packet is received +// callbackCtx: user pointer passed to callback +// Returns connection handle, or 0 on failure. +PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx); + +// Close a packetized connection. Does not close the underlying COM port. +void pktClose(PktConnT *conn); + +// Send a packet. Returns PKT_SUCCESS or error. +// Blocks if the transmit window is full and block is true. +// Returns PKT_ERR_TX_FULL if window is full and block is false. +int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); + +// Poll for incoming data, process received frames, handle retransmits. +// Must be called frequently (e.g. in your main loop). +// Returns the number of valid data packets delivered to the callback. +int pktPoll(PktConnT *conn); + +// Reset the connection state (sequence numbers, buffers). +// Sends a RST frame to the remote side. +int pktReset(PktConnT *conn); + +// Get number of unacknowledged packets in the transmit window. +int pktGetPending(PktConnT *conn); + +#endif diff --git a/rs232/README.md b/rs232/README.md new file mode 100644 index 0000000..879bacc --- /dev/null +++ b/rs232/README.md @@ -0,0 +1,212 @@ +# RS232 — Serial Port Library for DJGPP + +ISR-driven UART communication library supporting up to 4 simultaneous +COM ports with ring buffers and hardware/software flow control. + +Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License), +stripped to DJGPP-only codepaths and restyled. + +## Features + +- ISR-driven receive and transmit with 2048-byte ring buffers +- Auto-detected IRQ from BIOS data area +- 16550 FIFO detection and configurable trigger threshold +- XON/XOFF, RTS/CTS, and DTR/DSR flow control +- DPMI memory locking for ISR safety +- Speeds from 50 to 115200 bps +- 5-8 data bits, N/O/E/M/S parity, 1-2 stop bits + +## API Reference + +### Types + +All functions take a COM port index (`int com`) as their first argument: + +| Constant | Value | Description | +|--------------|-------|-------------| +| `RS232_COM1` | 0 | COM1 | +| `RS232_COM2` | 1 | COM2 | +| `RS232_COM3` | 2 | COM3 | +| `RS232_COM4` | 3 | COM4 | + +### Handshaking Modes + +| Constant | Value | Description | +|--------------------------|-------|---------------------------| +| `RS232_HANDSHAKE_NONE` | 0 | No flow control | +| `RS232_HANDSHAKE_XONXOFF`| 1 | Software (XON/XOFF) | +| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) | +| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) | + +### Error Codes + +| Constant | Value | Description | +|-------------------------------|-------|---------------------------| +| `RS232_SUCCESS` | 0 | Success | +| `RS232_ERR_UNKNOWN` | -1 | Unknown error | +| `RS232_ERR_NOT_OPEN` | -2 | Port not open | +| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open | +| `RS232_ERR_NO_UART` | -4 | No UART detected | +| `RS232_ERR_INVALID_PORT` | -5 | Bad port index | +| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address | +| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number | +| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate | +| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) | +| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character | +| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) | +| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode | +| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold | +| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument | +| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ | +| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed | + +### Functions + +#### Open / Close + +```c +int rs232Open(int com, int32_t bps, int dataBits, char parity, + int stopBits, int handshake); +``` + +Opens a COM port. Detects the UART base address from the BIOS data +area, auto-detects the IRQ, installs the ISR, and configures the port. + +- `bps` — baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400, + 3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200) +- `dataBits` — 5, 6, 7, or 8 +- `parity` — `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark), + `'S'` (space) +- `stopBits` — 1 or 2 +- `handshake` — `RS232_HANDSHAKE_*` constant + +```c +int rs232Close(int com); +``` + +Closes the port, removes the ISR, and restores the original interrupt +vector. + +#### Read / Write + +```c +int rs232Read(int com, char *data, int len); +``` + +Reads up to `len` bytes from the receive buffer. Returns the number of +bytes actually read (0 if the buffer is empty). + +```c +int rs232Write(int com, const char *data, int len); +``` + +Blocking write. Sends `len` bytes, waiting for transmit buffer space +as needed. Returns `RS232_SUCCESS` or an error code. + +```c +int rs232WriteBuf(int com, const char *data, int len); +``` + +Non-blocking write. Copies as many bytes as will fit into the transmit +buffer. Returns the number of bytes actually queued. + +#### Buffer Management + +```c +int rs232ClearRxBuffer(int com); +int rs232ClearTxBuffer(int com); +``` + +Discard all data in the receive or transmit ring buffer. + +#### Getters + +```c +int rs232GetBase(int com); // UART I/O base address +int32_t rs232GetBps(int com); // Current baud rate +int rs232GetCts(int com); // CTS line state (0 or 1) +int rs232GetData(int com); // Data bits setting +int rs232GetDsr(int com); // DSR line state (0 or 1) +int rs232GetDtr(int com); // DTR line state (0 or 1) +int rs232GetHandshake(int com); // Handshaking mode +int rs232GetIrq(int com); // IRQ number +int rs232GetLsr(int com); // Line status register +int rs232GetMcr(int com); // Modem control register +int rs232GetMsr(int com); // Modem status register +char rs232GetParity(int com); // Parity setting ('N','O','E','M','S') +int rs232GetRts(int com); // RTS line state (0 or 1) +int rs232GetRxBuffered(int com); // Bytes in receive buffer +int rs232GetStop(int com); // Stop bits setting +int rs232GetTxBuffered(int com); // Bytes in transmit buffer +``` + +#### Setters + +```c +int rs232Set(int com, int32_t bps, int dataBits, char parity, + int stopBits, int handshake); +``` + +Reconfigure all port parameters at once (port must be open). + +```c +int rs232SetBase(int com, int base); // Override I/O base address +int rs232SetBps(int com, int32_t bps); // Change baud rate +int rs232SetData(int com, int dataBits); // Change data bits +int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR +int rs232SetFifoThreshold(int com, int thr); // FIFO trigger level (1,4,8,14) +int rs232SetHandshake(int com, int handshake); // Change flow control mode +int rs232SetIrq(int com, int irq); // Override IRQ (before Open) +int rs232SetMcr(int com, int mcr); // Write modem control register +int rs232SetParity(int com, char parity); // Change parity +int rs232SetRts(int com, bool rts); // Assert/deassert RTS +int rs232SetStop(int com, int stopBits); // Change stop bits +``` + +## Example + +```c +#include "rs232.h" + +int main(void) { + // Open COM1 at 115200 8N1, no flow control + int rc = rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE); + if (rc != RS232_SUCCESS) { + return 1; + } + + // Blocking send + rs232Write(RS232_COM1, "Hello\r\n", 7); + + // Non-blocking receive + char buf[128]; + int n; + while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) { + // process buf[0..n-1] + } + + rs232Close(RS232_COM1); + return 0; +} +``` + +## Implementation Notes + +- The ISR handles all four COM ports from a single shared handler. + On entry it disables UART interrupts for all open ports, then + re-enables CPU interrupts so higher-priority devices are serviced. +- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing + for zero-branch wraparound. +- Flow control watermarks are at 80% (assert) and 20% (deassert) of + buffer capacity. +- DPMI `__dpmi_lock_linear_region` is used to pin the ISR, ring + buffers, and port state in physical memory. + +## Building + +``` +make # builds ../lib/librs232.a +make clean # removes objects and library +``` + +Target: DJGPP cross-compiler, 486+ CPU. diff --git a/rs232/rs232.c b/rs232/rs232.c index 9293f14..86c5615 100644 --- a/rs232/rs232.c +++ b/rs232/rs232.c @@ -1143,7 +1143,7 @@ int rs232SetData(int com, int dataBits) { } -int rs232SetDtr(int com, int dtr) { +int rs232SetDtr(int com, bool dtr) { Rs232StateT *port = &sComPorts[com]; if (com < COM_MIN || com > COM_MAX) { @@ -1287,7 +1287,7 @@ int rs232SetParity(int com, char parity) { } -int rs232SetRts(int com, int rts) { +int rs232SetRts(int com, bool rts) { Rs232StateT *port = &sComPorts[com]; if (com < COM_MIN || com > COM_MAX) { diff --git a/rs232/rs232.h b/rs232/rs232.h index 0d1549e..af1e7c0 100644 --- a/rs232/rs232.h +++ b/rs232/rs232.h @@ -8,6 +8,7 @@ #define RS232_H #include +#include // COM Ports #define RS232_COM1 0 @@ -77,13 +78,13 @@ int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int int rs232SetBase(int com, int base); int rs232SetBps(int com, int32_t bps); int rs232SetData(int com, int dataBits); -int rs232SetDtr(int com, int dtr); +int rs232SetDtr(int com, bool dtr); int rs232SetFifoThreshold(int com, int threshold); int rs232SetHandshake(int com, int handshake); int rs232SetIrq(int com, int irq); int rs232SetMcr(int com, int mcr); int rs232SetParity(int com, char parity); -int rs232SetRts(int com, int rts); +int rs232SetRts(int com, bool rts); int rs232SetStop(int com, int stopBits); #endif diff --git a/seclink/Makefile b/seclink/Makefile new file mode 100644 index 0000000..5430bd5 --- /dev/null +++ b/seclink/Makefile @@ -0,0 +1,38 @@ +# SecLink Library Makefile for DJGPP cross-compilation + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar +RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 + +OBJDIR = ../obj/seclink +LIBDIR = ../lib + +SRCS = secLink.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGET = $(LIBDIR)/libseclink.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) | $(LIBDIR) + $(AR) rcs $@ $(OBJS) + $(RANLIB) $@ + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(LIBDIR): + mkdir -p $(LIBDIR) + +# Dependencies +$(OBJDIR)/secLink.o: secLink.c secLink.h ../rs232/rs232.h ../packet/packet.h ../security/security.h + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/seclink/README.md b/seclink/README.md new file mode 100644 index 0000000..33d31da --- /dev/null +++ b/seclink/README.md @@ -0,0 +1,295 @@ +# SecLink — Secure Serial Link Library + +SecLink is a convenience wrapper that ties together three lower-level +libraries into a single API for reliable, optionally encrypted serial +communication: + +- **rs232** — ISR-driven UART I/O with ring buffers and flow control +- **packet** — HDLC-style framing with CRC-16 and sliding window reliability +- **security** — 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption + +## Architecture + +``` + Application + | + [secLink] channels, optional encryption + | + [packet] framing, CRC, retransmit, ordering + | + [rs232] ISR-driven UART, ring buffers, flow control + | + UART +``` + +SecLink adds a one-byte header to every packet: + +``` + Bit 7 Bits 6..0 + ----- --------- + Encrypt Channel (0-127) +``` + +This allows mixing encrypted and cleartext traffic on up to 128 +independent logical channels over a single serial link. + +## Lifecycle + +``` +secLinkOpen() Open COM port and packet layer +secLinkHandshake() DH key exchange (blocks until complete) +secLinkSend() Send a packet (encrypted or clear) +secLinkPoll() Receive and deliver packets to callback +secLinkClose() Tear down everything +``` + +The handshake is only required if you intend to send encrypted packets. +Cleartext packets can be sent immediately after `secLinkOpen()`. + +## API Reference + +### Types + +```c +// Receive callback — called for each incoming packet with plaintext +typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel); + +// Opaque connection handle +typedef struct SecLinkS SecLinkT; +``` + +### Constants + +| Name | Value | Description | +|-------------------------|-------|----------------------------------------| +| `SECLINK_MAX_PAYLOAD` | 255 | Max bytes per `secLinkSend()` call | +| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number | +| `SECLINK_SUCCESS` | 0 | Operation succeeded | +| `SECLINK_ERR_PARAM` | -1 | Invalid parameter | +| `SECLINK_ERR_SERIAL` | -2 | Serial port error | +| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed | +| `SECLINK_ERR_HANDSHAKE` | -4 | Key exchange failed | +| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake | +| `SECLINK_ERR_SEND` | -6 | Packet layer send failed | + +### Functions + +#### secLinkOpen + +```c +SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, + int stopBits, int handshake, + SecLinkRecvT callback, void *ctx); +``` + +Opens the COM port via rs232, creates the packet layer, and returns a +link handle. Returns `NULL` on failure. The callback is invoked from +`secLinkPoll()` for each received packet. + +#### secLinkClose + +```c +void secLinkClose(SecLinkT *link); +``` + +Destroys cipher contexts, closes the packet layer and COM port, and +frees all memory. + +#### secLinkHandshake + +```c +int secLinkHandshake(SecLinkT *link); +``` + +Performs a Diffie-Hellman key exchange. Blocks until both sides have +exchanged public keys and derived cipher keys. The RNG must be seeded +(via `secRngSeed()` or `secRngAddEntropy()`) before calling this. + +Each side derives separate TX and RX keys from the shared secret, +using public key ordering to determine directionality. This prevents +CTR counter collisions. + +#### secLinkGetPending + +```c +int secLinkGetPending(SecLinkT *link); +``` + +Returns the number of unacknowledged packets in the transmit window. +Useful for non-blocking send loops to determine if there is room to +send more data. + +#### secLinkIsReady + +```c +bool secLinkIsReady(SecLinkT *link); +``` + +Returns `true` if the handshake is complete and the link is ready for +encrypted communication. + +#### secLinkPoll + +```c +int secLinkPoll(SecLinkT *link); +``` + +Reads available serial data, processes received frames, handles ACKs +and retransmits. Decrypts encrypted packets and delivers plaintext to +the receive callback. Returns the number of packets delivered, or +negative on error. + +Must be called frequently (e.g. in your main loop). + +#### secLinkSend + +```c +int secLinkSend(SecLinkT *link, const uint8_t *data, int len, + uint8_t channel, bool encrypt, bool block); +``` + +Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel. + +- `channel` — logical channel number (0-127) +- `encrypt` — if `true`, encrypts the payload (requires completed handshake) +- `block` — if `true`, waits for transmit window space; if `false`, + returns `SECLINK_ERR_SEND` when the window is full + +Cleartext packets (`encrypt = false`) can be sent before the handshake. + +#### secLinkSendBuf + +```c +int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, + uint8_t channel, bool encrypt); +``` + +Sends an arbitrarily large buffer by splitting it into +`SECLINK_MAX_PAYLOAD`-byte chunks. Always blocks until all data is +sent. Returns `SECLINK_SUCCESS` or the first error encountered. + +## Examples + +### Basic encrypted link + +```c +#include "secLink.h" +#include "../security/security.h" + +void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) { + // handle received plaintext on 'channel' +} + +int main(void) { + // Seed the RNG before handshake + uint8_t entropy[16]; + secRngGatherEntropy(entropy, sizeof(entropy)); + secRngSeed(entropy, sizeof(entropy)); + + // Open link on COM1 at 115200 8N1 + SecLinkT *link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecv, NULL); + if (!link) { + return 1; + } + + // Key exchange (blocks until both sides complete) + if (secLinkHandshake(link) != SECLINK_SUCCESS) { + secLinkClose(link); + return 1; + } + + // Send encrypted data on channel 0 + const char *msg = "Hello, secure world!"; + secLinkSend(link, (const uint8_t *)msg, strlen(msg), 0, true, true); + + // Main loop + while (1) { + secLinkPoll(link); + } + + secLinkClose(link); + return 0; +} +``` + +### Mixed encrypted and cleartext channels + +```c +#define CHAN_CONTROL 0 // cleartext control channel +#define CHAN_DATA 1 // encrypted data channel + +// Send a cleartext status message (no handshake needed) +secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true); + +// Send encrypted payload (requires completed handshake) +secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true); +``` + +### Non-blocking file transfer + +```c +int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize, + uint8_t channel, bool encrypt, int windowSize) { + int offset = 0; + int bytesLeft = fileSize; + + while (bytesLeft > 0) { + secLinkPoll(link); // process ACKs, free window slots + + if (secLinkGetPending(link) < windowSize) { + int chunk = bytesLeft; + if (chunk > SECLINK_MAX_PAYLOAD) { + chunk = SECLINK_MAX_PAYLOAD; + } + + int rc = secLinkSend(link, fileData + offset, chunk, + channel, encrypt, false); + if (rc == SECLINK_SUCCESS) { + offset += chunk; + bytesLeft -= chunk; + } + // SECLINK_ERR_SEND means window full, just retry next iteration + } + + // Application can do other work here: + // update progress bar, check for cancel, etc. + } + + // Drain remaining ACKs + while (secLinkGetPending(link) > 0) { + secLinkPoll(link); + } + + return SECLINK_SUCCESS; +} +``` + +### Blocking bulk transfer + +```c +// Send an entire file in one call (blocks until complete) +secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true); +``` + +## Building + +``` +make # builds ../lib/libseclink.a +make clean # removes objects and library +``` + +Link against all four libraries: + +``` +-lseclink -lpacket -lsecurity -lrs232 +``` + +## Dependencies + +SecLink requires these libraries (all in `../lib/`): + +- `librs232.a` — serial port driver +- `libpacket.a` — packet framing and reliability +- `libsecurity.a` — DH key exchange and XTEA cipher + +Target: DJGPP cross-compiler, 486+ CPU. diff --git a/seclink/secLink.c b/seclink/secLink.c new file mode 100644 index 0000000..7e371c0 --- /dev/null +++ b/seclink/secLink.c @@ -0,0 +1,338 @@ +// Secure serial link — ties rs232, packet, and security into one API +// +// Handshake protocol: +// 1. Both sides send their DH public key (128 bytes) as a single packet +// 2. On receiving the remote key, compute shared secret immediately +// 3. Derive separate TX/RX cipher keys based on public key ordering +// 4. Transition to READY — all subsequent encrypted packets use the keys +// +// Directionality: the side with the lexicographically lower public key +// uses master XOR 0xAA for TX and master XOR 0x55 for RX. The other +// side uses the reverse. This prevents CTR counter collisions. +// +// Channel header byte: bit 7 = encrypted, bits 6..0 = channel (0-127) + +#include +#include +#include +#include +#include "secLink.h" +#include "../rs232/rs232.h" +#include "../packet/packet.h" +#include "../security/security.h" + + +// ======================================================================== +// Internal defines +// ======================================================================== + +#define STATE_INIT 0 +#define STATE_HANDSHAKE 1 +#define STATE_READY 2 + +#define TX_KEY_XOR 0xAA +#define RX_KEY_XOR 0x55 + +#define ENCRYPT_FLAG 0x80 +#define CHANNEL_MASK 0x7F + + +// ======================================================================== +// Types +// ======================================================================== + +struct SecLinkS { + int com; + PktConnT *pkt; + SecDhT *dh; + SecCipherT *txCipher; + SecCipherT *rxCipher; + SecLinkRecvT userCallback; + void *userCtx; + int state; + uint8_t myPub[SEC_DH_KEY_SIZE]; + uint8_t remoteKey[SEC_DH_KEY_SIZE]; + bool gotRemoteKey; +}; + + +// ======================================================================== +// Static prototypes (alphabetical) +// ======================================================================== + +static void completeHandshake(SecLinkT *link); +static void internalRecv(void *ctx, const uint8_t *data, int len); + + +// ======================================================================== +// Static functions (alphabetical) +// ======================================================================== + +static void completeHandshake(SecLinkT *link) { + uint8_t masterKey[SEC_XTEA_KEY_SIZE]; + uint8_t txKey[SEC_XTEA_KEY_SIZE]; + uint8_t rxKey[SEC_XTEA_KEY_SIZE]; + bool weAreLower; + + secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE); + secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE); + + // Derive directional keys so each side encrypts with a unique key + weAreLower = (memcmp(link->myPub, link->remoteKey, SEC_DH_KEY_SIZE) < 0); + for (int i = 0; i < SEC_XTEA_KEY_SIZE; i++) { + txKey[i] = masterKey[i] ^ (weAreLower ? TX_KEY_XOR : RX_KEY_XOR); + rxKey[i] = masterKey[i] ^ (weAreLower ? RX_KEY_XOR : TX_KEY_XOR); + } + + link->txCipher = secCipherCreate(txKey); + link->rxCipher = secCipherCreate(rxKey); + + memset(masterKey, 0, sizeof(masterKey)); + memset(txKey, 0, sizeof(txKey)); + memset(rxKey, 0, sizeof(rxKey)); + + // Destroy DH context — private key no longer needed + secDhDestroy(link->dh); + link->dh = 0; + + link->state = STATE_READY; +} + + +static void internalRecv(void *ctx, const uint8_t *data, int len) { + SecLinkT *link = (SecLinkT *)ctx; + + if (link->state == STATE_HANDSHAKE && !link->gotRemoteKey) { + // During handshake, expect exactly one 128-byte public key + if (len == SEC_DH_KEY_SIZE) { + memcpy(link->remoteKey, data, len); + link->gotRemoteKey = true; + completeHandshake(link); + } + } else if (link->state == STATE_READY || link->state == STATE_INIT) { + // Need at least the channel header byte + if (len < 1) { + return; + } + + uint8_t hdr = data[0]; + uint8_t channel = hdr & CHANNEL_MASK; + bool encrypted = (hdr & ENCRYPT_FLAG) != 0; + const uint8_t *payload = data + 1; + int payloadLen = len - 1; + + if (encrypted && link->state == STATE_READY) { + uint8_t plaintext[PKT_MAX_PAYLOAD]; + if (payloadLen > (int)sizeof(plaintext)) { + payloadLen = (int)sizeof(plaintext); + } + memcpy(plaintext, payload, payloadLen); + secCipherCrypt(link->rxCipher, plaintext, payloadLen); + if (link->userCallback) { + link->userCallback(link->userCtx, plaintext, payloadLen, channel); + } + } else if (!encrypted) { + if (link->userCallback) { + link->userCallback(link->userCtx, payload, payloadLen, channel); + } + } + // encrypted but not READY: silently drop + } +} + + +// ======================================================================== +// Public functions (alphabetical) +// ======================================================================== + +void secLinkClose(SecLinkT *link) { + if (!link) { + return; + } + + if (link->txCipher) { + secCipherDestroy(link->txCipher); + } + if (link->rxCipher) { + secCipherDestroy(link->rxCipher); + } + if (link->dh) { + secDhDestroy(link->dh); + } + if (link->pkt) { + pktClose(link->pkt); + } + rs232Close(link->com); + + memset(link, 0, sizeof(SecLinkT)); + free(link); +} + + +int secLinkGetPending(SecLinkT *link) { + if (!link) { + return SECLINK_ERR_PARAM; + } + + return pktGetPending(link->pkt); +} + + +int secLinkHandshake(SecLinkT *link) { + int len; + int rc; + + if (!link) { + return SECLINK_ERR_PARAM; + } + + // Generate DH keypair + link->dh = secDhCreate(); + if (!link->dh) { + return SECLINK_ERR_ALLOC; + } + + rc = secDhGenerateKeys(link->dh); + if (rc != SEC_SUCCESS) { + return SECLINK_ERR_HANDSHAKE; + } + + // Export our public key + len = SEC_DH_KEY_SIZE; + secDhGetPublicKey(link->dh, link->myPub, &len); + + // Enter handshake state and send our key + link->state = STATE_HANDSHAKE; + link->gotRemoteKey = false; + + rc = pktSend(link->pkt, link->myPub, SEC_DH_KEY_SIZE, true); + if (rc != PKT_SUCCESS) { + return SECLINK_ERR_HANDSHAKE; + } + + // Poll until the callback completes the handshake + while (link->state != STATE_READY) { + pktPoll(link->pkt); + } + + return SECLINK_SUCCESS; +} + + +bool secLinkIsReady(SecLinkT *link) { + if (!link) { + return false; + } + return link->state == STATE_READY; +} + + +SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx) { + SecLinkT *link; + int rc; + + link = (SecLinkT *)calloc(1, sizeof(SecLinkT)); + if (!link) { + return 0; + } + + link->com = com; + link->userCallback = callback; + link->userCtx = ctx; + link->state = STATE_INIT; + + // Open serial port + rc = rs232Open(com, bps, dataBits, parity, stopBits, handshake); + if (rc != RS232_SUCCESS) { + free(link); + return 0; + } + + // Open packet layer with our internal callback + link->pkt = pktOpen(com, 0, internalRecv, link); + if (!link->pkt) { + rs232Close(com); + free(link); + return 0; + } + + return link; +} + + +int secLinkPoll(SecLinkT *link) { + if (!link) { + return SECLINK_ERR_PARAM; + } + + return pktPoll(link->pkt); +} + + +int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block) { + uint8_t buf[PKT_MAX_PAYLOAD]; + int rc; + + if (!link || !data) { + return SECLINK_ERR_PARAM; + } + if (channel > SECLINK_MAX_CHANNEL) { + return SECLINK_ERR_PARAM; + } + if (encrypt && link->state != STATE_READY) { + return SECLINK_ERR_NOT_READY; + } + if (len <= 0 || len > SECLINK_MAX_PAYLOAD) { + return SECLINK_ERR_PARAM; + } + + // Build channel header byte + buf[0] = channel & CHANNEL_MASK; + if (encrypt) { + buf[0] |= ENCRYPT_FLAG; + } + + // Copy payload after header + memcpy(buf + 1, data, len); + + // Encrypt the payload portion only (not the header) + if (encrypt) { + secCipherCrypt(link->txCipher, buf + 1, len); + } + + rc = pktSend(link->pkt, buf, len + 1, block); + if (rc != PKT_SUCCESS) { + return SECLINK_ERR_SEND; + } + + return SECLINK_SUCCESS; +} + + +int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt) { + int offset = 0; + int rc; + + if (!link || !data) { + return SECLINK_ERR_PARAM; + } + if (len <= 0) { + return SECLINK_ERR_PARAM; + } + + while (offset < len) { + int chunk = len - offset; + if (chunk > SECLINK_MAX_PAYLOAD) { + chunk = SECLINK_MAX_PAYLOAD; + } + + rc = secLinkSend(link, data + offset, chunk, channel, encrypt, true); + if (rc != SECLINK_SUCCESS) { + return rc; + } + + offset += chunk; + } + + return SECLINK_SUCCESS; +} diff --git a/seclink/secLink.h b/seclink/secLink.h new file mode 100644 index 0000000..53f2512 --- /dev/null +++ b/seclink/secLink.h @@ -0,0 +1,74 @@ +// Secure serial link — convenience wrapper tying rs232 + packet + security +// +// Usage: +// 1. secLinkOpen() — opens COM port, sets up packet framing +// 2. secLinkHandshake() — DH key exchange (blocks until both sides complete) +// 3. secLinkSend() — send data (optionally encrypted) on a channel +// 4. secLinkPoll() — receive, decrypt if needed, deliver to callback +// 5. secLinkClose() — tear everything down +// +// Each packet carries a one-byte header: bit 7 = encrypted flag, +// bits 6..0 = channel number (0-127). The callback receives plaintext +// regardless of whether encryption was used. + +#ifndef SECLINK_H +#define SECLINK_H + +#include +#include + +// Error codes +#define SECLINK_SUCCESS 0 +#define SECLINK_ERR_PARAM -1 +#define SECLINK_ERR_SERIAL -2 +#define SECLINK_ERR_ALLOC -3 +#define SECLINK_ERR_HANDSHAKE -4 +#define SECLINK_ERR_NOT_READY -5 +#define SECLINK_ERR_SEND -6 + +// Max plaintext payload per send (packet max minus 1-byte channel header) +#define SECLINK_MAX_PAYLOAD 255 + +// Channel limits +#define SECLINK_MAX_CHANNEL 127 + +// Receive callback — delivers plaintext with channel number +typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel); + +// Opaque handle +typedef struct SecLinkS SecLinkT; + + +// Open a secure serial link. Opens the COM port and packet layer. +// Handshake must be called separately before sending encrypted data. +SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx); + +// Close the link. Frees all resources and closes the COM port. +void secLinkClose(SecLinkT *link); + +// Perform DH key exchange. Blocks until both sides have exchanged keys +// and derived cipher keys. RNG must be seeded before calling this. +int secLinkHandshake(SecLinkT *link); + +// Get number of unacknowledged packets in the transmit window. +int secLinkGetPending(SecLinkT *link); + +// Returns true if handshake is complete and link is ready for data. +bool secLinkIsReady(SecLinkT *link); + +// Poll for incoming data. Decrypts if needed and delivers to callback. +// Returns number of packets delivered, or negative on error. +int secLinkPoll(SecLinkT *link); + +// Send data on a channel. If encrypt is true, data is encrypted before +// sending (requires completed handshake). Clear packets can be sent +// without a handshake. len must be 1..SECLINK_MAX_PAYLOAD. +// If block is true, waits for transmit window space. +int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); + +// Send an arbitrarily large buffer by splitting it into SECLINK_MAX_PAYLOAD +// chunks. Always blocks until all data is sent. Returns SECLINK_SUCCESS +// or the first error encountered. +int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt); + +#endif diff --git a/security/Makefile b/security/Makefile new file mode 100644 index 0000000..d6cb5fc --- /dev/null +++ b/security/Makefile @@ -0,0 +1,38 @@ +# Security Library Makefile for DJGPP cross-compilation + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +AR = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ar +RANLIB = LD_LIBRARY_PATH=$(DJGPP_LIBPATH) $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-ranlib +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 + +OBJDIR = ../obj/security +LIBDIR = ../lib + +SRCS = security.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGET = $(LIBDIR)/libsecurity.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) | $(LIBDIR) + $(AR) rcs $@ $(OBJS) + $(RANLIB) $@ + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(LIBDIR): + mkdir -p $(LIBDIR) + +# Dependencies +$(OBJDIR)/security.o: security.c security.h + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/security/README.md b/security/README.md new file mode 100644 index 0000000..532e6a6 --- /dev/null +++ b/security/README.md @@ -0,0 +1,259 @@ +# Security — DH Key Exchange and XTEA-CTR Cipher + +Cryptographic library providing Diffie-Hellman key exchange and XTEA +symmetric encryption, optimized for 486-class DOS hardware running +under DJGPP/DPMI. + +## Components + +### Diffie-Hellman Key Exchange + +- 1024-bit MODP group (RFC 2409 Group 2 safe prime) +- 256-bit private exponents for fast computation on 486 CPUs +- Montgomery multiplication (CIOS variant) for modular exponentiation +- Lazy-initialized Montgomery constants (R^2 mod p, -p0^-1 mod 2^32) + +### XTEA Cipher (CTR Mode) + +- 128-bit key, 64-bit block size, 32 rounds +- CTR mode — encrypt and decrypt are the same XOR operation +- No lookup tables, no key schedule — just shifts, adds, and XORs +- Ideal for constrained environments with small key setup cost + +### Pseudo-Random Number Generator + +- XTEA-CTR based DRBG (deterministic random bit generator) +- Hardware entropy from PIT counter (~10 bits) and BIOS tick count +- Supports additional entropy injection (keyboard timing, mouse, etc.) +- Auto-seeds from hardware on first use if not explicitly seeded + +## Performance + +At serial port speeds, encryption overhead is minimal: + +| Speed | Blocks/sec | CPU cycles/sec | % of 33 MHz 486 | +|----------|------------|----------------|------------------| +| 9600 | 120 | ~240K | < 1% | +| 57600 | 720 | ~1.4M | ~4% | +| 115200 | 1440 | ~2.9M | ~9% | + +DH key exchange takes approximately 0.3s at 66 MHz or 0.6s at 33 MHz +(256-bit private exponent, 1024-bit modulus). + +## API Reference + +### Constants + +| Name | Value | Description | +|---------------------|-------|--------------------------------| +| `SEC_DH_KEY_SIZE` | 128 | DH public key size (bytes) | +| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size (bytes) | +| `SEC_SUCCESS` | 0 | Success | +| `SEC_ERR_PARAM` | -1 | Invalid parameter | +| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived | +| `SEC_ERR_ALLOC` | -3 | Memory allocation failed | + +### Types + +```c +typedef struct SecDhS SecDhT; // Opaque DH context +typedef struct SecCipherS SecCipherT; // Opaque cipher context +``` + +### RNG Functions + +```c +int secRngGatherEntropy(uint8_t *buf, int len); +``` + +Reads hardware entropy sources (PIT counter, BIOS tick count). Returns +the number of bytes written. Provides roughly 20 bits of true entropy. + +```c +void secRngSeed(const uint8_t *entropy, int len); +``` + +Initializes the DRBG with the given entropy. XOR-folds the input into +the XTEA key, derives the counter, and mixes state by generating and +discarding 64 bytes. + +```c +void secRngAddEntropy(const uint8_t *data, int len); +``` + +Mixes additional entropy into the running RNG state without resetting +it. Use this to stir in keyboard timing, mouse jitter, or other +runtime entropy. + +```c +void secRngBytes(uint8_t *buf, int len); +``` + +Generates `len` pseudo-random bytes. Auto-seeds from hardware if not +previously seeded. + +### Diffie-Hellman Functions + +```c +SecDhT *secDhCreate(void); +``` + +Allocates a new DH context. Returns `NULL` on allocation failure. + +```c +int secDhGenerateKeys(SecDhT *dh); +``` + +Generates a 256-bit random private key and computes the corresponding +1024-bit public key (g^private mod p). The RNG should be seeded first. + +```c +int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); +``` + +Exports the public key into `buf`. On entry, `*len` must be at least +`SEC_DH_KEY_SIZE` (128). On return, `*len` is set to 128. + +```c +int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +``` + +Computes the shared secret from the remote side's public key. +Validates that the remote key is in range [2, p-2] to prevent +small-subgroup attacks. + +```c +int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen); +``` + +Derives a symmetric key by XOR-folding the 128-byte shared secret +down to `keyLen` bytes. + +```c +void secDhDestroy(SecDhT *dh); +``` + +Securely zeroes and frees the DH context (private key, shared secret). + +### Cipher Functions + +```c +SecCipherT *secCipherCreate(const uint8_t *key); +``` + +Creates an XTEA-CTR cipher context with the given 16-byte key. Counter +starts at zero. + +```c +void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); +``` + +Encrypts or decrypts `data` in place. CTR mode is symmetric — the +same operation encrypts and decrypts. + +```c +void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi); +``` + +Sets the 64-bit nonce/counter. Call before encrypting if you need a +specific starting counter value. + +```c +void secCipherDestroy(SecCipherT *c); +``` + +Securely zeroes and frees the cipher context. + +## Example + +### Full Key Exchange + +```c +#include "security.h" +#include + +// Seed the RNG +uint8_t entropy[16]; +secRngGatherEntropy(entropy, sizeof(entropy)); +secRngSeed(entropy, sizeof(entropy)); + +// Create DH context and generate keys +SecDhT *dh = secDhCreate(); +secDhGenerateKeys(dh); + +// Export public key to send to remote +uint8_t myPub[SEC_DH_KEY_SIZE]; +int pubLen = SEC_DH_KEY_SIZE; +secDhGetPublicKey(dh, myPub, &pubLen); +// ... send myPub to remote, receive remotePub ... + +// Compute shared secret and derive a 16-byte key +secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE); + +uint8_t key[SEC_XTEA_KEY_SIZE]; +secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE); +secDhDestroy(dh); + +// Create cipher and encrypt +SecCipherT *cipher = secCipherCreate(key); +uint8_t message[] = "Secret message"; +secCipherCrypt(cipher, message, sizeof(message)); +// message is now encrypted + +// Decrypt (same operation — CTR mode is symmetric) +// Reset counter first if using the same cipher context +secCipherSetNonce(cipher, 0, 0); +secCipherCrypt(cipher, message, sizeof(message)); +// message is now plaintext again + +secCipherDestroy(cipher); +``` + +### Standalone Encryption + +```c +// XTEA-CTR can be used independently of DH +uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ }; +SecCipherT *c = secCipherCreate(key); + +uint8_t data[1024]; +// ... fill data ... +secCipherCrypt(c, data, sizeof(data)); // encrypt in place + +secCipherDestroy(c); +``` + +## Implementation Details + +### BigNum Arithmetic + +All modular arithmetic uses a 1024-bit big number type (`BigNumT`) +stored as 32 x `uint32_t` words in little-endian order. Operations: + +- Add, subtract, compare, shift-left-1, bit test +- Montgomery multiplication (CIOS with implicit right-shift) +- Modular exponentiation (left-to-right binary square-and-multiply) + +### Montgomery Multiplication + +The CIOS (Coarsely Integrated Operand Scanning) variant computes +`a * b * R^-1 mod m` in a single pass with implicit division by the +word base. Constants are computed once on first DH use: + +- `R^2 mod p` — via 2048 iterations of shift-and-conditional-subtract +- `-p[0]^-1 mod 2^32` — via Newton's method (5 iterations) + +### Secure Zeroing + +Key material is erased using a volatile-pointer loop that the compiler +cannot optimize away, preventing sensitive data from lingering in +memory. + +## Building + +``` +make # builds ../lib/libsecurity.a +make clean # removes objects and library +``` + +Target: DJGPP cross-compiler, 486+ CPU. diff --git a/security/security.c b/security/security.c new file mode 100644 index 0000000..6ba471c --- /dev/null +++ b/security/security.c @@ -0,0 +1,688 @@ +// Security library: DH key exchange + XTEA-CTR cipher for DJGPP +// +// Diffie-Hellman uses the RFC 2409 Group 2 (1024-bit) safe prime with +// Montgomery multiplication for modular exponentiation. Private exponents +// are 256 bits for fast computation on 486-class hardware. +// +// XTEA in CTR mode provides symmetric encryption. No lookup tables, +// no key schedule — just shifts, adds, and XORs. + +#include +#include +#include +#include +#include +#include +#include +#include "security.h" + + +// ======================================================================== +// Internal defines +// ======================================================================== + +#define BN_BITS 1024 +#define BN_WORDS (BN_BITS / 32) +#define BN_BYTES (BN_BITS / 8) + +#define DH_PRIVATE_BITS 256 +#define DH_PRIVATE_BYTES (DH_PRIVATE_BITS / 8) + +#define XTEA_ROUNDS 32 +#define XTEA_DELTA 0x9E3779B9 + + +// ======================================================================== +// Types +// ======================================================================== + +typedef struct { + uint32_t w[BN_WORDS]; +} BigNumT; + +struct SecDhS { + BigNumT privateKey; + BigNumT publicKey; + BigNumT sharedSecret; + bool hasKeys; + bool hasSecret; +}; + +struct SecCipherS { + uint32_t key[4]; + uint32_t nonce[2]; + uint32_t counter[2]; +}; + +typedef struct { + uint32_t key[4]; + uint32_t counter[2]; + bool seeded; +} RngStateT; + + +// ======================================================================== +// Static globals +// ======================================================================== + +// RFC 2409 Group 2 (1024-bit MODP) prime, little-endian word order +static const BigNumT sDhPrime = { .w = { + 0x39E38FAF, 0xCDB1CEDC, 0x51FF5DB8, 0x85E28A20, + 0x1E9C284F, 0x2BB72AE0, 0x60F89D81, 0x4E664FD5, + 0x45E6F3A1, 0x92F2129E, 0xB8E51B21, 0x35C7D431, + 0x14A0C959, 0x137E2179, 0x5BE0CD19, 0x7A51F1D7, + 0xF25F1468, 0x302B0A6D, 0xCD3A431B, 0xEF9519B3, + 0x8E3404DD, 0x514A0879, 0x3B139B22, 0x020BBEA6, + 0x8A67CC74, 0x29024E08, 0x80DC1CD1, 0xC4C6628B, + 0x2168C234, 0xC90FDAA2, 0xFFFFFFFF, 0xFFFFFFFF +}}; + +// Generator g = 2 +static const BigNumT sDhGenerator = { .w = { 2 } }; + +// Montgomery constants (computed lazily) +static BigNumT sDhR2; // R^2 mod p +static uint32_t sDhM0Inv; // -p[0]^(-1) mod 2^32 +static bool sDhInited = false; + +// RNG state +static RngStateT sRng = { .seeded = false }; + + +// ======================================================================== +// Static prototypes (alphabetical) +// ======================================================================== + +static int bnAdd(BigNumT *result, const BigNumT *a, const BigNumT *b); +static int bnBit(const BigNumT *a, int n); +static int bnBitLength(const BigNumT *a); +static void bnClear(BigNumT *a); +static int bnCmp(const BigNumT *a, const BigNumT *b); +static void bnCopy(BigNumT *dst, const BigNumT *src); +static void bnFromBytes(BigNumT *a, const uint8_t *buf); +static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp, const BigNumT *mod, uint32_t m0inv, const BigNumT *r2); +static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b, const BigNumT *mod, uint32_t m0inv); +static void bnSet(BigNumT *a, uint32_t val); +static int bnShiftLeft1(BigNumT *a); +static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b); +static void bnToBytes(uint8_t *buf, const BigNumT *a); +static uint32_t computeM0Inv(uint32_t m0); +static void computeR2(BigNumT *r2, const BigNumT *m); +static void dhInit(void); +static void secureZero(void *ptr, int len); +static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]); + + +// ======================================================================== +// BigNum functions (alphabetical) +// ======================================================================== + +static int __attribute__((unused)) bnAdd(BigNumT *result, const BigNumT *a, const BigNumT *b) { + uint64_t carry = 0; + + for (int i = 0; i < BN_WORDS; i++) { + uint64_t sum = (uint64_t)a->w[i] + b->w[i] + carry; + result->w[i] = (uint32_t)sum; + carry = sum >> 32; + } + + return (int)carry; +} + + +static int bnBit(const BigNumT *a, int n) { + return (a->w[n / 32] >> (n % 32)) & 1; +} + + +static int bnBitLength(const BigNumT *a) { + for (int i = BN_WORDS - 1; i >= 0; i--) { + if (a->w[i]) { + uint32_t v = a->w[i]; + int bits = i * 32; + while (v) { + bits++; + v >>= 1; + } + return bits; + } + } + return 0; +} + + +static void bnClear(BigNumT *a) { + memset(a->w, 0, sizeof(a->w)); +} + + +static int bnCmp(const BigNumT *a, const BigNumT *b) { + for (int i = BN_WORDS - 1; i >= 0; i--) { + if (a->w[i] > b->w[i]) { + return 1; + } + if (a->w[i] < b->w[i]) { + return -1; + } + } + return 0; +} + + +static void bnCopy(BigNumT *dst, const BigNumT *src) { + memcpy(dst->w, src->w, sizeof(dst->w)); +} + + +static void bnFromBytes(BigNumT *a, const uint8_t *buf) { + for (int i = 0; i < BN_WORDS; i++) { + int j = (BN_WORDS - 1 - i) * 4; + a->w[i] = ((uint32_t)buf[j] << 24) | + ((uint32_t)buf[j + 1] << 16) | + ((uint32_t)buf[j + 2] << 8) | + (uint32_t)buf[j + 3]; + } +} + + +static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp, const BigNumT *mod, uint32_t m0inv, const BigNumT *r2) { + BigNumT montBase; + BigNumT montResult; + BigNumT one; + int bits; + bool started; + + // Convert base to Montgomery form: montBase = base * R mod m + bnMontMul(&montBase, base, r2, mod, m0inv); + + // Initialize montResult to 1 in Montgomery form (= R mod m) + bnClear(&one); + one.w[0] = 1; + bnMontMul(&montResult, &one, r2, mod, m0inv); + + // Left-to-right binary square-and-multiply + bits = bnBitLength(exp); + started = false; + for (int i = bits - 1; i >= 0; i--) { + if (started) { + bnMontMul(&montResult, &montResult, &montResult, mod, m0inv); + } + if (bnBit(exp, i)) { + if (!started) { + bnCopy(&montResult, &montBase); + started = true; + } else { + bnMontMul(&montResult, &montResult, &montBase, mod, m0inv); + } + } + } + + // Convert back from Montgomery form: result = montResult * 1 * R^(-1) mod m + bnClear(&one); + one.w[0] = 1; + bnMontMul(result, &montResult, &one, mod, m0inv); +} + + +static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b, const BigNumT *mod, uint32_t m0inv) { + uint32_t t[BN_WORDS + 1]; + uint32_t u; + uint64_t carry; + uint64_t prod; + uint64_t sum; + + memset(t, 0, sizeof(t)); + + for (int i = 0; i < BN_WORDS; i++) { + // Step 1: t += a[i] * b + carry = 0; + for (int j = 0; j < BN_WORDS; j++) { + prod = (uint64_t)a->w[i] * b->w[j] + t[j] + carry; + t[j] = (uint32_t)prod; + carry = prod >> 32; + } + t[BN_WORDS] += (uint32_t)carry; + + // Step 2: Montgomery reduction factor + u = t[0] * m0inv; + + // Step 3: t = (t + u * mod) >> 32 + // First word: result is zero by construction, take carry only + prod = (uint64_t)u * mod->w[0] + t[0]; + carry = prod >> 32; + // Remaining words: shift result left by one position + for (int j = 1; j < BN_WORDS; j++) { + prod = (uint64_t)u * mod->w[j] + t[j] + carry; + t[j - 1] = (uint32_t)prod; + carry = prod >> 32; + } + sum = (uint64_t)t[BN_WORDS] + carry; + t[BN_WORDS - 1] = (uint32_t)sum; + t[BN_WORDS] = (uint32_t)(sum >> 32); + } + + // Copy result + memcpy(result->w, t, BN_WORDS * sizeof(uint32_t)); + + // Conditional subtract if result >= mod + if (t[BN_WORDS] || bnCmp(result, mod) >= 0) { + bnSub(result, result, mod); + } +} + + +static void bnSet(BigNumT *a, uint32_t val) { + bnClear(a); + a->w[0] = val; +} + + +static int bnShiftLeft1(BigNumT *a) { + uint32_t carry = 0; + + for (int i = 0; i < BN_WORDS; i++) { + uint32_t newCarry = a->w[i] >> 31; + a->w[i] = (a->w[i] << 1) | carry; + carry = newCarry; + } + + return carry; +} + + +static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b) { + uint64_t borrow = 0; + + for (int i = 0; i < BN_WORDS; i++) { + uint64_t diff = (uint64_t)a->w[i] - b->w[i] - borrow; + result->w[i] = (uint32_t)diff; + borrow = (diff >> 63) & 1; + } + + return (int)borrow; +} + + +static void bnToBytes(uint8_t *buf, const BigNumT *a) { + for (int i = 0; i < BN_WORDS; i++) { + int j = (BN_WORDS - 1 - i) * 4; + uint32_t w = a->w[i]; + buf[j] = (uint8_t)(w >> 24); + buf[j + 1] = (uint8_t)(w >> 16); + buf[j + 2] = (uint8_t)(w >> 8); + buf[j + 3] = (uint8_t)(w); + } +} + + +// ======================================================================== +// Helper functions (alphabetical) +// ======================================================================== + +static uint32_t computeM0Inv(uint32_t m0) { + // Newton's method: compute m0^(-1) mod 2^32 + // Converges quadratically: 1 → 2 → 4 → 8 → 16 → 32 correct bits + uint32_t x = 1; + + for (int i = 0; i < 5; i++) { + x = x * (2 - m0 * x); + } + + // Return -m0^(-1) mod 2^32 + return ~x + 1; +} + + +static void computeR2(BigNumT *r2, const BigNumT *m) { + // Compute R^2 mod m where R = 2^1024 + // Method: start with 1, double 2048 times, reduce mod m each step + bnSet(r2, 1); + + for (int i = 0; i < 2 * BN_BITS; i++) { + bnShiftLeft1(r2); + if (bnCmp(r2, m) >= 0) { + bnSub(r2, r2, m); + } + } +} + + +static void dhInit(void) { + if (sDhInited) { + return; + } + + sDhM0Inv = computeM0Inv(sDhPrime.w[0]); + computeR2(&sDhR2, &sDhPrime); + sDhInited = true; +} + + +static void secureZero(void *ptr, int len) { + // Volatile prevents the compiler from optimizing away the zeroing + volatile uint8_t *p = (volatile uint8_t *)ptr; + + for (int i = 0; i < len; i++) { + p[i] = 0; + } +} + + +static void xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]) { + uint32_t v0 = v[0]; + uint32_t v1 = v[1]; + uint32_t sum = 0; + + for (int i = 0; i < XTEA_ROUNDS; i++) { + v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); + sum += XTEA_DELTA; + v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); + } + + v[0] = v0; + v[1] = v1; +} + + +// ======================================================================== +// RNG functions (alphabetical) +// ======================================================================== + +void secRngAddEntropy(const uint8_t *data, int len) { + // XOR additional entropy into the key + for (int i = 0; i < len; i++) { + ((uint8_t *)sRng.key)[i % 16] ^= data[i]; + } + + // Re-mix: encrypt the key with itself + uint32_t block[2]; + block[0] = sRng.key[0] ^ sRng.key[2]; + block[1] = sRng.key[1] ^ sRng.key[3]; + xteaEncryptBlock(block, sRng.key); + sRng.key[0] ^= block[0]; + sRng.key[1] ^= block[1]; + block[0] = sRng.key[2] ^ sRng.key[0]; + block[1] = sRng.key[3] ^ sRng.key[1]; + xteaEncryptBlock(block, sRng.key); + sRng.key[2] ^= block[0]; + sRng.key[3] ^= block[1]; +} + + +void secRngBytes(uint8_t *buf, int len) { + // Auto-seed from hardware if never seeded + if (!sRng.seeded) { + uint8_t entropy[16]; + int got = secRngGatherEntropy(entropy, sizeof(entropy)); + secRngSeed(entropy, got); + } + + uint32_t block[2]; + int pos = 0; + + while (pos < len) { + block[0] = sRng.counter[0]; + block[1] = sRng.counter[1]; + xteaEncryptBlock(block, sRng.key); + + int take = len - pos; + if (take > 8) { + take = 8; + } + memcpy(buf + pos, block, take); + pos += take; + + // Increment counter + if (++sRng.counter[0] == 0) { + sRng.counter[1]++; + } + } +} + + +int secRngGatherEntropy(uint8_t *buf, int len) { + int out = 0; + + // Read PIT channel 0 counter (1.193 MHz, ~10 bits of entropy in LSBs) + outportb(0x43, 0x00); + uint8_t pitLo = inportb(0x40); + uint8_t pitHi = inportb(0x40); + + // BIOS tick count (18.2 Hz) + uint32_t ticks = _farpeekl(_dos_ds, 0x46C); + + if (out < len) { buf[out++] = pitLo; } + if (out < len) { buf[out++] = pitHi; } + if (out < len) { buf[out++] = (uint8_t)(ticks); } + if (out < len) { buf[out++] = (uint8_t)(ticks >> 8); } + if (out < len) { buf[out++] = (uint8_t)(ticks >> 16); } + if (out < len) { buf[out++] = (uint8_t)(ticks >> 24); } + + // Second PIT reading for jitter + outportb(0x43, 0x00); + pitLo = inportb(0x40); + pitHi = inportb(0x40); + if (out < len) { buf[out++] = pitLo; } + if (out < len) { buf[out++] = pitHi; } + + return out; +} + + +void secRngSeed(const uint8_t *entropy, int len) { + memset(&sRng, 0, sizeof(sRng)); + + // XOR-fold entropy into the key + for (int i = 0; i < len; i++) { + ((uint8_t *)sRng.key)[i % 16] ^= entropy[i]; + } + + // Derive counter from key bits + sRng.counter[0] = sRng.key[2] ^ sRng.key[0]; + sRng.counter[1] = sRng.key[3] ^ sRng.key[1]; + sRng.seeded = true; + + // Mix state by generating and discarding 64 bytes + uint8_t discard[64]; + sRng.seeded = true; // prevent recursion in secRngBytes + secRngBytes(discard, sizeof(discard)); + secureZero(discard, sizeof(discard)); +} + + +// ======================================================================== +// DH functions (alphabetical) +// ======================================================================== + +int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) { + BigNumT remote; + BigNumT two; + + if (!dh || !remotePub) { + return SEC_ERR_PARAM; + } + if (len != SEC_DH_KEY_SIZE) { + return SEC_ERR_PARAM; + } + if (!dh->hasKeys) { + return SEC_ERR_NOT_READY; + } + + dhInit(); + + bnFromBytes(&remote, remotePub); + + // Validate remote public key: must be in range [2, p-2] + bnSet(&two, 2); + if (bnCmp(&remote, &two) < 0 || bnCmp(&remote, &sDhPrime) >= 0) { + secureZero(&remote, sizeof(remote)); + return SEC_ERR_PARAM; + } + + // shared = remote^private mod p + bnModExp(&dh->sharedSecret, &remote, &dh->privateKey, &sDhPrime, sDhM0Inv, &sDhR2); + dh->hasSecret = true; + + secureZero(&remote, sizeof(remote)); + return SEC_SUCCESS; +} + + +SecDhT *secDhCreate(void) { + SecDhT *dh = (SecDhT *)calloc(1, sizeof(SecDhT)); + return dh; +} + + +int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen) { + uint8_t secretBytes[BN_BYTES]; + + if (!dh || !key || keyLen <= 0) { + return SEC_ERR_PARAM; + } + if (!dh->hasSecret) { + return SEC_ERR_NOT_READY; + } + if (keyLen > BN_BYTES) { + keyLen = BN_BYTES; + } + + bnToBytes(secretBytes, &dh->sharedSecret); + + // XOR-fold 128-byte shared secret down to keyLen bytes + memset(key, 0, keyLen); + for (int i = 0; i < BN_BYTES; i++) { + key[i % keyLen] ^= secretBytes[i]; + } + + secureZero(secretBytes, sizeof(secretBytes)); + return SEC_SUCCESS; +} + + +void secDhDestroy(SecDhT *dh) { + if (dh) { + secureZero(dh, sizeof(SecDhT)); + free(dh); + } +} + + +int secDhGenerateKeys(SecDhT *dh) { + if (!dh) { + return SEC_ERR_PARAM; + } + + dhInit(); + + // Generate 256-bit random private key + bnClear(&dh->privateKey); + secRngBytes((uint8_t *)dh->privateKey.w, DH_PRIVATE_BYTES); + + // Ensure private key >= 2 + if (bnBitLength(&dh->privateKey) <= 1) { + dh->privateKey.w[0] = 2; + } + + // public = g^private mod p + bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey, &sDhPrime, sDhM0Inv, &sDhR2); + dh->hasKeys = true; + dh->hasSecret = false; + + return SEC_SUCCESS; +} + + +int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len) { + if (!dh || !buf || !len) { + return SEC_ERR_PARAM; + } + if (*len < SEC_DH_KEY_SIZE) { + return SEC_ERR_PARAM; + } + if (!dh->hasKeys) { + return SEC_ERR_NOT_READY; + } + + bnToBytes(buf, &dh->publicKey); + *len = SEC_DH_KEY_SIZE; + return SEC_SUCCESS; +} + + +// ======================================================================== +// Cipher functions (alphabetical) +// ======================================================================== + +SecCipherT *secCipherCreate(const uint8_t *key) { + SecCipherT *c; + + if (!key) { + return 0; + } + + c = (SecCipherT *)calloc(1, sizeof(SecCipherT)); + if (!c) { + return 0; + } + + memcpy(c->key, key, SEC_XTEA_KEY_SIZE); + return c; +} + + +void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) { + uint32_t block[2]; + uint8_t *keystream; + int pos; + int take; + + if (!c || !data || len <= 0) { + return; + } + + keystream = (uint8_t *)block; + pos = 0; + + while (pos < len) { + // Encrypt counter to generate keystream + block[0] = c->counter[0]; + block[1] = c->counter[1]; + xteaEncryptBlock(block, c->key); + + // XOR keystream with data + take = len - pos; + if (take > 8) { + take = 8; + } + for (int i = 0; i < take; i++) { + data[pos + i] ^= keystream[i]; + } + pos += take; + + // Increment counter + if (++c->counter[0] == 0) { + c->counter[1]++; + } + } +} + + +void secCipherDestroy(SecCipherT *c) { + if (c) { + secureZero(c, sizeof(SecCipherT)); + free(c); + } +} + + +void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi) { + if (!c) { + return; + } + + c->nonce[0] = nonceLo; + c->nonce[1] = nonceHi; + c->counter[0] = nonceLo; + c->counter[1] = nonceHi; +} diff --git a/security/security.h b/security/security.h new file mode 100644 index 0000000..b9b050c --- /dev/null +++ b/security/security.h @@ -0,0 +1,47 @@ +// Security library: Diffie-Hellman key exchange + XTEA-CTR cipher +// Targets 486-class hardware with 1024-bit DH (256-bit private exponent) +// and XTEA in CTR mode for symmetric encryption. + +#ifndef SECURITY_H +#define SECURITY_H + +#include +#include + +// Key sizes (bytes) +#define SEC_DH_KEY_SIZE 128 // 1024-bit DH public key +#define SEC_XTEA_KEY_SIZE 16 // 128-bit XTEA key + +// Error codes +#define SEC_SUCCESS 0 +#define SEC_ERR_PARAM -1 +#define SEC_ERR_NOT_READY -2 +#define SEC_ERR_ALLOC -3 + +// Opaque types +typedef struct SecDhS SecDhT; +typedef struct SecCipherS SecCipherT; + + +// RNG — seed before generating keys. Hardware entropy is weak (~20 bits); +// callers should supplement with keyboard timing, mouse jitter, etc. +int secRngGatherEntropy(uint8_t *buf, int len); +void secRngAddEntropy(const uint8_t *data, int len); +void secRngBytes(uint8_t *buf, int len); +void secRngSeed(const uint8_t *entropy, int len); + +// Diffie-Hellman key exchange (1024-bit, RFC 2409 Group 2) +SecDhT *secDhCreate(void); +int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); +int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen); +void secDhDestroy(SecDhT *dh); +int secDhGenerateKeys(SecDhT *dh); +int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); + +// XTEA cipher in CTR mode (encrypt and decrypt are the same operation) +SecCipherT *secCipherCreate(const uint8_t *key); +void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); +void secCipherDestroy(SecCipherT *c); +void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi); + +#endif