Reliable, secure, serial link protocol added.

This commit is contained in:
Scott Duensing 2026-03-10 21:05:52 -05:00
parent ab69652a72
commit 0fcaae54c3
15 changed files with 2838 additions and 4 deletions

38
packet/Makefile Normal file
View file

@ -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)

215
packet/README.md Normal file
View file

@ -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.

523
packet/packet.c Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <pc.h>
#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;
}

68
packet/packet.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
// 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

212
rs232/README.md Normal file
View file

@ -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.

View file

@ -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]; Rs232StateT *port = &sComPorts[com];
if (com < COM_MIN || com > COM_MAX) { 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]; Rs232StateT *port = &sComPorts[com];
if (com < COM_MIN || com > COM_MAX) { if (com < COM_MIN || com > COM_MAX) {

View file

@ -8,6 +8,7 @@
#define RS232_H #define RS232_H
#include <stdint.h> #include <stdint.h>
#include <stdbool.h>
// COM Ports // COM Ports
#define RS232_COM1 0 #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 rs232SetBase(int com, int base);
int rs232SetBps(int com, int32_t bps); int rs232SetBps(int com, int32_t bps);
int rs232SetData(int com, int dataBits); 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 rs232SetFifoThreshold(int com, int threshold);
int rs232SetHandshake(int com, int handshake); int rs232SetHandshake(int com, int handshake);
int rs232SetIrq(int com, int irq); int rs232SetIrq(int com, int irq);
int rs232SetMcr(int com, int mcr); int rs232SetMcr(int com, int mcr);
int rs232SetParity(int com, char parity); int rs232SetParity(int com, char parity);
int rs232SetRts(int com, int rts); int rs232SetRts(int com, bool rts);
int rs232SetStop(int com, int stopBits); int rs232SetStop(int com, int stopBits);
#endif #endif

38
seclink/Makefile Normal file
View file

@ -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)

295
seclink/README.md Normal file
View file

@ -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.

338
seclink/secLink.c Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#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;
}

74
seclink/secLink.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
// 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

38
security/Makefile Normal file
View file

@ -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)

259
security/README.md Normal file
View file

@ -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 <string.h>
// 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.

688
security/security.c Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <pc.h>
#include <sys/farptr.h>
#include <go32.h>
#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;
}

47
security/security.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
// 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