# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

.section Libraries
.topic lib.serial
.title Serial Stack
.toc 0 Serial Stack
.index Serial
.index RS-232
.index HDLC
.index XTEA
.index Diffie-Hellman
.index secLink

.h1 Serial Stack

The DVX serial/networking stack provides reliable, optionally encrypted communication over RS-232 serial ports. It is composed of four layers, each building on the one below:

.list
.item rs232 -- ISR-driven UART driver with ring buffers and flow control
.item packet -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)
.item security -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR cipher, DRBG RNG
.item secLink -- Convenience wrapper: channel multiplexing, per-packet encryption
.endlist

Loaded as: bin/libs/kpunch/serial/serial.lib

.h2 Layered Architecture

.code
+--------------------------------------------------+
|  Application                                     |
|                                                  |
|  secLinkSend()  -- send on channel, opt. encrypt |
|  secLinkPoll()  -- receive + dispatch            |
+--------------------------------------------------+
|  secLink  (channel multiplex, TX/RX ciphers)     |
+--------------------------------------------------+
|  packet   (HDLC framing, CRC-16, Go-Back-N ARQ)  |
+--------------------------------------------------+
|  security -- DH key exchange, XTEA-CTR, DRBG     |
|            (used by secLink during handshake)    |
+--------------------------------------------------+
|  rs232    (ISR-driven UART, 2048-byte rings)     |
+--------------------------------------------------+
|  UART hardware (8250 / 16450 / 16550 / 16550A)   |
+--------------------------------------------------+
.endcode

The security layer is invoked by secLink during the Diffie-Hellman handshake and for per-packet cipher operations. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.

.link lib.serial.rs232 Layer 1: RS-232 UART Driver
.link lib.serial.packet Layer 2: Packet Transport
.link lib.serial.security Layer 3: Security (DH + XTEA)
.link lib.serial.seclink Layer 4: Secure Link
.link lib.serial.example End-to-End Example

.topic lib.serial.rs232
.title RS-232 UART Driver
.toc 1 RS-232 UART Driver
.index rs232
.index UART
.index COM Port
.index rs232Open
.index rs232Close
.index rs232Read
.index rs232Write

.h1 RS-232 UART Driver

ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults.

Header: rs232/rs232.h

.h2 Port Constants

.table
  Constant          Value   Description
  --------          -----   -----------
  RS232_COM1        0       First serial port.
  RS232_COM2        1       Second serial port.
  RS232_COM3        2       Third serial port.
  RS232_COM4        3       Fourth serial port.
  RS232_NUM_PORTS   4       Total number of supported COM ports.
.endtable

.h2 Handshake Modes

.table
  Constant                  Value   Description
  --------                  -----   -----------
  RS232_HANDSHAKE_NONE      0       No flow control.
  RS232_HANDSHAKE_XONXOFF   1       Software flow control (XON/XOFF characters).
  RS232_HANDSHAKE_RTSCTS    2       Hardware flow control via RTS/CTS lines.
  RS232_HANDSHAKE_DTRDSR    3       Hardware flow control via DTR/DSR lines.
.endtable

.h2 UART Types

Detected automatically by probing the scratch register and FIFO capability.

.table
  Constant            Value   Description
  --------            -----   -----------
  RS232_UART_UNKNOWN  0       Detection failed.
  RS232_UART_8250     1       Original IBM PC UART. No FIFO.
  RS232_UART_16450    2       Has scratch register, no FIFO.
  RS232_UART_16550    3       Has FIFO but buggy (rare).
  RS232_UART_16550A   4       Working 16-byte FIFO. Most common in 486+ hardware.
.endtable

.h2 Error Codes

.table
  Constant                     Value   Description
  --------                     -----   -----------
  RS232_SUCCESS                0       Operation succeeded.
  RS232_ERR_UNKNOWN            -1      Unknown error.
  RS232_ERR_NOT_OPEN           -2      Port is not open.
  RS232_ERR_ALREADY_OPEN       -3      Port is already open.
  RS232_ERR_NO_UART            -4      No UART detected at port address.
  RS232_ERR_INVALID_PORT       -5      Invalid port number.
  RS232_ERR_INVALID_BASE       -6      Invalid base I/O address.
  RS232_ERR_INVALID_IRQ        -7      Invalid IRQ number.
  RS232_ERR_INVALID_BPS        -8      Invalid baud rate.
  RS232_ERR_INVALID_DATA       -9      Invalid data bits value.
  RS232_ERR_INVALID_PARITY     -10     Invalid parity character.
  RS232_ERR_INVALID_STOP       -11     Invalid stop bits value.
  RS232_ERR_INVALID_HANDSHAKE  -12     Invalid handshake mode.
  RS232_ERR_INVALID_FIFO       -13     Invalid FIFO threshold.
  RS232_ERR_NULL_PTR           -14     NULL pointer argument.
  RS232_ERR_IRQ_NOT_FOUND      -15     Could not detect IRQ for port.
  RS232_ERR_LOCK_MEM           -16     DPMI memory lock failed.
.endtable

.h2 rs232Open

Open a COM port with the specified line parameters.

.code
int rs232Open(int com, int32_t bps, int dataBits,
    char parity, int stopBits, int handshake);
.endcode

.table
  Parameter   Description
  ---------   -----------
  com         Port index (RS232_COM1..RS232_COM4).
  bps         Baud rate (e.g. 9600, 19200, 38400, 57600, 115200).
  dataBits    Data bits per character (5, 6, 7, or 8).
  parity      Parity mode: 'N' (none), 'O' (odd), 'E' (even), 'M' (mark), 'S' (space).
  stopBits    Stop bits (1 or 2).
  handshake   Flow control mode (RS232_HANDSHAKE_*).
.endtable

Returns RS232_SUCCESS or a negative error code.

.h2 rs232Close

Close a COM port and release its ISR resources.

.code
int rs232Close(int com);
.endcode

.h2 rs232Read

Non-blocking read from the receive ring buffer.

.code
int rs232Read(int com, char *data, int len);
.endcode

.table
  Parameter   Description
  ---------   -----------
  com         Port index.
  data        Buffer to receive data.
  len         Maximum bytes to read.
.endtable

Returns the number of bytes actually read (may be less than len).

.h2 rs232Write

Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte. Use for small, immediate writes where order matters (the packet layer uses this for frame transmission).

.code
int rs232Write(int com, const char *data, int len);
.endcode

Returns the number of bytes actually written (may be less than len if software flow control is asserted), or a negative error code (RS232_ERR_INVALID_PORT, RS232_ERR_NOT_OPEN, RS232_ERR_NULL_PTR).

.h2 rs232WriteBuf

Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously.

.code
int rs232WriteBuf(int com, const char *data, int len);
.endcode

Returns the number of bytes actually queued (may be less than len if the buffer is full).

.h2 rs232Set

Change all line parameters on an open port. Internally calls rs232SetBps, rs232SetData, rs232SetParity, rs232SetStop, and rs232SetHandshake in sequence. Returns RS232_SUCCESS on success or the first negative error code encountered.

.code
int rs232Set(int com, int32_t bps, int dataBits,
    char parity, int stopBits, int handshake);
.endcode

.h2 Configuration Getters

.table
  Function                   Returns
  --------                   -------
  rs232GetBase(com)          I/O base address.
  rs232GetBps(com)           Current baud rate (int32_t).
  rs232GetData(com)          Data bits setting (5-8).
  rs232GetParity(com)        Parity character: 'n', 'o', 'e', 'm', or 's' (lowercase).
  rs232GetStop(com)          Stop bits setting (1 or 2).
  rs232GetHandshake(com)     Handshake mode (RS232_HANDSHAKE_*).
  rs232GetIrq(com)           IRQ number.
  rs232GetUartType(com)      Detected UART type (RS232_UART_*).
.endtable

rs232GetParity returns a lowercase character on success. Callers comparing against uppercase constants must use either both cases or tolower().

.h2 Status Getters

.table
  Function                   Returns
  --------                   -------
  rs232GetRxBuffered(com)    Bytes waiting in the receive ring buffer.
  rs232GetTxBuffered(com)    Bytes waiting in the transmit ring buffer.
  rs232GetCts(com)           CTS line state (0 or 1).
  rs232GetDsr(com)           DSR line state (0 or 1).
  rs232GetDtr(com)           DTR line state (0 or 1).
  rs232GetRts(com)           RTS line state (0 or 1).
  rs232GetLsr(com)           Line status register value.
  rs232GetMcr(com)           Modem control register value.
  rs232GetMsr(com)           Modem status register value.
.endtable

.h2 Configuration Setters

.table
  Function                          Description
  --------                          -----------
  rs232SetBase(com, base)           Set I/O base address (before open).
  rs232SetBps(com, bps)             Change baud rate.
  rs232SetData(com, dataBits)       Change data bits.
  rs232SetParity(com, parity)       Change parity mode.
  rs232SetStop(com, stopBits)       Change stop bits.
  rs232SetHandshake(com, handshake) Change flow control mode.
  rs232SetIrq(com, irq)             Set IRQ number (before open).
  rs232SetMcr(com, mcr)             Set modem control register directly.
  rs232SetDtr(com, dtr)             Set DTR line state.
  rs232SetRts(com, rts)             Set RTS line state.
  rs232SetFifoThreshold(com, thr)   Set FIFO trigger level (1, 4, 8, or 14 bytes).
.endtable

.h2 Buffer Management

.table
  Function                   Description
  --------                   -----------
  rs232ClearRxBuffer(com)    Discard all data in the receive ring buffer.
  rs232ClearTxBuffer(com)    Discard all data in the transmit ring buffer.
.endtable

.topic lib.serial.packet
.title Packet Transport
.toc 1 Packet Transport
.index packet
.index HDLC
.index CRC-16
.index Go-Back-N
.index pktOpen
.index pktClose
.index pktSend
.index pktPoll

.h1 Packet Transport

Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission.

Header: packet/packet.h

.h2 Constants

.table
  Constant            Value   Description
  --------            -----   -----------
  PKT_MAX_PAYLOAD     255     Maximum bytes per packet payload.
  PKT_DEFAULT_WINDOW  4       Default sliding window size (unacknowledged frames in flight).
  PKT_MAX_WINDOW      8       Maximum sliding window size.
.endtable

.h2 Error Codes

.table
  Constant                Value   Description
  --------                -----   -----------
  PKT_SUCCESS             0       Operation succeeded.
  PKT_ERR_INVALID_PORT    -1      Invalid COM port index.
  PKT_ERR_NOT_OPEN        -2      Connection is not open.
  PKT_ERR_WOULD_BLOCK     -4      Operation would block.
  PKT_ERR_OVERFLOW        -5      Buffer overflow.
  PKT_ERR_INVALID_PARAM   -6      Invalid parameter (NULL handle, bad data pointer, length out of range).
  PKT_ERR_TX_FULL         -7      Transmit window is full (non-blocking send only).
  PKT_ERR_NO_DATA         -8      No data available.
  PKT_ERR_DISCONNECTED    -9      Serial port returned a negative read (disconnect/error).
.endtable

.h2 PktRecvCallbackT

Callback type for received packets.

.code
typedef void (*PktRecvCallbackT)(void *ctx,
    const uint8_t *data, int len);
.endcode

.table
  Parameter   Description
  ---------   -----------
  ctx         User context pointer (passed to pktOpen).
  data        Payload bytes (valid only during the callback).
  len         Payload length.
.endtable

.h2 pktOpen

Open a packetized connection over an already-open COM port. Only one packet connection may exist on a given COM port at a time; a second pktOpen call on the same port returns NULL to avoid silent framing corruption.

.code
PktConnT *pktOpen(int com, int windowSize,
    PktRecvCallbackT callback, void *callbackCtx);
.endcode

.table
  Parameter     Description
  ---------     -----------
  com           COM port index (RS232_COM1..RS232_COM4). Must already be open via rs232Open.
  windowSize    Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW. Values greater than PKT_MAX_WINDOW are clamped.
  callback      Called when a complete, CRC-verified, in-order packet is received. The data pointer is valid only during the callback.
  callbackCtx   User pointer passed to the callback.
.endtable

Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure).

.h2 pktClose

Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed.

.code
void pktClose(PktConnT *conn);
.endcode

.h2 pktSend

Send a data packet. The payload is copied into an internal retransmit slot before transmission, so the caller may reuse the supplied buffer immediately.

.code
int pktSend(PktConnT *conn, const uint8_t *data,
    int len, bool block);
.endcode

.table
  Parameter   Description
  ---------   -----------
  conn        Connection handle from pktOpen.
  data        Payload bytes (1..PKT_MAX_PAYLOAD).
  len         Payload length.
  block       If true, poll pktPoll internally until transmit window has space. If false, return PKT_ERR_TX_FULL immediately when the window is full.
.endtable

Returns PKT_SUCCESS on success, PKT_ERR_INVALID_PARAM for NULL/out-of-range input, PKT_ERR_TX_FULL for a non-blocking call with a full window, or PKT_ERR_DISCONNECTED if the serial port drops during a blocking wait.

.h2 pktPoll

Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop. The callback is invoked synchronously for each complete, CRC-verified, in-order data frame, so the caller should be prepared for reentrant pktSend calls from within the callback.

.code
int pktPoll(PktConnT *conn);
.endcode

Returns the number of valid data packets delivered to the callback this call, or PKT_ERR_INVALID_PARAM if conn is NULL, or PKT_ERR_DISCONNECTED if the serial layer returned a negative read.

.h2 pktReset

Reset the local connection state (sequence numbers, transmit slots, RX state machine) and send a RST frame to the remote side. The remote's RST handler clears its own state and echoes a RST back. Useful for recovering from a desynchronized connection.

.code
int pktReset(PktConnT *conn);
.endcode

Returns PKT_SUCCESS on success or PKT_ERR_INVALID_PARAM if conn is NULL.

.h2 pktCanSend

Check whether there is room in the transmit window for another packet. Returns false when conn is NULL or the transmit window is full.

.code
bool pktCanSend(PktConnT *conn);
.endcode

.h2 pktGetPending

Get the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full).

.code
int pktGetPending(PktConnT *conn);
.endcode

Returns the pending count on success, or PKT_ERR_INVALID_PARAM if conn is NULL.

.topic lib.serial.security
.title Security (DH + XTEA)
.toc 1 Security (DH + XTEA)
.index Security
.index Diffie-Hellman
.index XTEA
.index XTEA-CTR
.index DRBG
.index secDhCreate
.index secCipherCreate
.index secRngBytes

.h1 Security (DH + XTEA)

Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, and an XTEA-CTR DRBG random number generator.

XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache.

Header: security/security.h

.h2 Constants

.table
  Constant           Value   Description
  --------           -----   -----------
  SEC_DH_KEY_SIZE    128     Size of a DH public key in bytes (1024 bits).
  SEC_XTEA_KEY_SIZE  16      Size of an XTEA key in bytes (128 bits).
.endtable

.h2 Error Codes

.table
  Constant           Value   Description
  --------           -----   -----------
  SEC_SUCCESS        0       Operation succeeded.
  SEC_ERR_PARAM      -1      Invalid parameter.
  SEC_ERR_NOT_READY  -2      DH context not ready (keys not generated or secret not computed).
  SEC_ERR_ALLOC      -3      Memory allocation failed.
.endtable

.h2 RNG Functions

The RNG uses XTEA-CTR as a DRBG. Hardware entropy from PIT timer jitter is weak (~20 bits); supplement with keyboard timing or mouse jitter before generating DH keys.

.code
void secRngSeed(const uint8_t *entropy, int len);
int  secRngGatherEntropy(uint8_t *buf, int len);
void secRngAddEntropy(const uint8_t *data, int len);
void secRngBytes(uint8_t *buf, int len);
.endcode

.table
  Function              Description
  --------              -----------
  secRngSeed            Seed the DRBG from an entropy buffer.
  secRngGatherEntropy   Gather hardware entropy (PIT jitter, BIOS tick count) into a buffer.
  secRngAddEntropy      Mix additional entropy into the DRBG state.
  secRngBytes           Generate pseudorandom bytes.
.endtable

.h2 Diffie-Hellman Key Exchange

1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime.

.code
SecDhT *secDhCreate(void);
int     secDhGenerateKeys(SecDhT *dh);
int     secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
int     secDhComputeSecret(SecDhT *dh,
            const uint8_t *remotePub, int len);
int     secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
void    secDhDestroy(SecDhT *dh);
.endcode

.table
  Function            Description
  --------            -----------
  secDhCreate         Allocate a new DH context.
  secDhGenerateKeys   Generate a private exponent and public key. RNG must be seeded first.
  secDhGetPublicKey   Copy the local public key into buf. *len receives the key size.
  secDhComputeSecret  Compute the shared secret from the remote public key.
  secDhDeriveKey      Derive a symmetric key (XTEA key) from the shared secret via hashing.
  secDhDestroy        Free the DH context and all associated memory.
.endtable

Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhDeriveKey, secDhDestroy.

.h2 XTEA-CTR Cipher

XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.

.code
SecCipherT *secCipherCreate(const uint8_t *key);
void        secCipherSetNonce(SecCipherT *c,
                uint32_t nonceLo, uint32_t nonceHi);
void        secCipherCrypt(SecCipherT *c,
                uint8_t *data, int len);
void        secCipherDestroy(SecCipherT *c);
.endcode

.table
  Function             Description
  --------             -----------
  secCipherCreate      Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
  secCipherSetNonce    Set the initial counter value (nonce). Must be unique per session. Both the stored nonce and the running counter are set to (nonceLo, nonceHi).
  secCipherCrypt       Encrypt or decrypt data in place. The counter increments by one every 8 bytes (one XTEA block).
  secCipherDestroy    Securely zero the cipher context (key and counter state) and free its memory.
.endtable

secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.

.topic lib.serial.seclink
.title Secure Link
.toc 1 Secure Link
.index secLink
.index secLinkOpen
.index secLinkClose
.index secLinkHandshake
.index secLinkSend
.index secLinkPoll

.h1 Secure Link

Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides channel multiplexing (0-127) and per-packet encryption control over a reliable serial link.

Each packet carries a one-byte header: bit 7 is the encrypted flag, bits 6..0 are the channel number. Unencrypted packets can be sent before the handshake completes (e.g., for version negotiation).

Header: seclink/secLink.h

.h2 Constants

.table
  Constant                  Value   Description
  --------                  -----   -----------
  SECLINK_MAX_PAYLOAD       254     Maximum plaintext bytes per send (PKT_MAX_PAYLOAD minus 1-byte channel header).
  SECLINK_MAX_CHANNEL       127     Highest valid channel number.
  SECLINK_NUM_CHANNELS      128     Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1).
  SECLINK_CHAN_BUF_SIZE     4096    Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read.
.endtable

.h2 Error Codes

.table
  Constant                 Value   Description
  --------                 -----   -----------
  SECLINK_SUCCESS          0       Operation succeeded.
  SECLINK_ERR_PARAM        -1      Invalid parameter (NULL pointer, bad channel, bad length).
  SECLINK_ERR_SERIAL       -2      RS-232 layer error.
  SECLINK_ERR_ALLOC        -3      Memory allocation failed.
  SECLINK_ERR_HANDSHAKE    -4      DH handshake failed (key generation, send, or disconnect during exchange).
  SECLINK_ERR_NOT_READY    -5      Handshake not yet completed (encrypted send attempted).
  SECLINK_ERR_SEND         -6      Packet send failed, or non-blocking send when window is full.
.endtable

.h2 SecLinkRecvT

Receive callback type. Delivers decrypted plaintext with the channel number.

.code
typedef void (*SecLinkRecvT)(void *ctx,
    const uint8_t *data, int len, uint8_t channel);
.endcode

.h2 secLinkOpen

Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent.

.code
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits,
    char parity, int stopBits, int handshake,
    SecLinkRecvT callback, void *ctx);
.endcode

.table
  Parameter   Description
  ---------   -----------
  com         COM port index (RS232_COM1..RS232_COM4).
  bps         Baud rate.
  dataBits    Data bits per character (5-8).
  parity      Parity mode ('N', 'O', 'E', 'M', 'S' or lowercase).
  stopBits    Stop bits (1 or 2).
  handshake   Flow control mode (RS232_HANDSHAKE_*).
  callback    Called when a packet is received, after decryption if applicable. Plaintext data pointer is valid only during the callback.
  ctx         User pointer passed to the callback.
.endtable

Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up.

.h2 secLinkClose

Close the link and free all associated resources in reverse of open order: securely destroy TX and RX ciphers (which scrubs key material), destroy any remaining DH context, close the packet layer, close the COM port, and finally free the link struct itself. Safe to call with a NULL link (no-op).

.code
void secLinkClose(SecLinkT *link);
.endcode

.h2 secLinkHandshake

Perform a Diffie-Hellman key exchange and derive the session cipher keys. Blocks, polling the packet layer in a loop, until both sides have swapped public keys. The RNG must be seeded (via secRngSeed, secRngGatherEntropy + secRngSeed, or secRngAddEntropy) before calling this, or the generated private key will be predictable.

.code
int secLinkHandshake(SecLinkT *link);
.endcode

Internally:

.list
.item Creates a DH context via secDhCreate, generates 1024-bit keys via secDhGenerateKeys, and exports the 128-byte public key.
.item Sends the public key as a single packet via pktSend (blocking).
.item Polls pktPoll until the remote's public key arrives and the internal callback completes the handshake (computes the shared secret, derives directional TX and RX cipher keys, transitions to READY, destroys the DH context for forward secrecy).
.endlist

Returns SECLINK_SUCCESS on success, SECLINK_ERR_PARAM on NULL link, SECLINK_ERR_ALLOC on DH context allocation failure, or SECLINK_ERR_HANDSHAKE for DH key generation failure, packet send failure, or serial disconnect during the exchange.

.h3 Directional Key Derivation

To prevent CTR-mode keystream collision, the two sides never use the same TX key. After computing the shared secret, each side derives a master XTEA key via secDhDeriveKey and then XORs it with a direction byte:

.list
.item The side with the lexicographically lower public key uses masterKey XOR 0xAA for TX and masterKey XOR 0x55 for RX.
.item The other side uses the reverse assignment.
.endlist

Both sides initialize their cipher counters to zero and advance monotonically from there.

.h2 secLinkSend

Send data on a channel, optionally encrypted. Prepends a one-byte channel header (bit 7 = encrypt flag, bits 6..0 = channel number), then encrypts the payload only (never the header) when encrypt is true.

.code
int secLinkSend(SecLinkT *link, const uint8_t *data,
    int len, uint8_t channel, bool encrypt, bool block);
.endcode

.table
  Parameter   Description
  ---------   -----------
  link        Connection handle from secLinkOpen.
  data        Plaintext payload (1..SECLINK_MAX_PAYLOAD bytes).
  len         Payload length. Must be in 1..SECLINK_MAX_PAYLOAD.
  channel     Logical channel number (0..SECLINK_MAX_CHANNEL).
  encrypt     If true, encrypt before sending (requires completed handshake).
  block       If true, block until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full.
.endtable

Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND).

Cipher counter safety: the function checks transmit window space BEFORE encrypting the payload. Encrypting first and then failing to send would advance the TX cipher counter while the data never reached the wire, permanently desynchronizing the TX cipher state from the remote's RX cipher. This ordering is critical for correctness when non-blocking sends are used.

.h2 secLinkSendBuf

Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent.

.code
int secLinkSendBuf(SecLinkT *link, const uint8_t *data,
    int len, uint8_t channel, bool encrypt);
.endcode

Returns SECLINK_SUCCESS or the first error encountered.

.h2 secLinkPoll

Poll for incoming data. Delegates to pktPoll, which drains the RS-232 ring buffer, processes received frames, checks retransmit timers, and dispatches each complete packet to an internal callback that strips the channel header, decrypts when the encrypt flag is set, and forwards plaintext to the user callback.

.code
int secLinkPoll(SecLinkT *link);
.endcode

Returns the number of packets delivered to the callback this call, SECLINK_ERR_PARAM if link is NULL, or one of the pktPoll error codes (PKT_ERR_DISCONNECTED, PKT_ERR_INVALID_PARAM). Must be called frequently in the main loop.

.h2 secLinkIsReady

Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready.

.code
bool secLinkIsReady(SecLinkT *link);
.endcode

Returns true only when link is non-NULL and the internal state is STATE_READY (that is, secLinkHandshake has completed). Returns false on NULL or before the handshake.

.h2 secLinkGetPending

Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle.

.code
int secLinkGetPending(SecLinkT *link);
.endcode

Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL.

.topic lib.serial.example
.title End-to-End Example
.toc 1 End-to-End Example
.index Serial Example
.index BBS Client

.h1 End-to-End Example

Below is a complete secure BBS client that opens COM1, exchanges DH keys, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: open the link, seed the RNG, run the handshake, then enter a poll+send loop.

.code
#include "rs232/rs232.h"
#include "packet/packet.h"
#include "security/security.h"
#include "seclink/secLink.h"
#include <stdio.h>
#include <string.h>

#define CHAN_TERMINAL 0

static void onPacket(void *ctx, const uint8_t *data,
                     int len, uint8_t channel)
{
    (void)ctx;
    if (channel == CHAN_TERMINAL) {
        fwrite(data, 1, len, stdout);
        fflush(stdout);
    }
}


int main(void) {
    // Seed the RNG from hardware entropy plus any user-derived
    // randomness (keyboard timing, etc.).
    uint8_t entropy[16];
    int got = secRngGatherEntropy(entropy, sizeof(entropy));
    secRngSeed(entropy, got);

    // Open the link at 115200 8N1 with RTS/CTS flow control.
    SecLinkT *link = secLinkOpen(
        RS232_COM1, 115200, 8, 'N', 1,
        RS232_HANDSHAKE_RTSCTS,
        onPacket, NULL);
    if (!link) {
        fprintf(stderr, "secLinkOpen failed\n");
        return 1;
    }

    // Run the DH key exchange.  Blocks until both sides
    // have swapped public keys and derived cipher keys.
    if (secLinkHandshake(link) != SECLINK_SUCCESS) {
        fprintf(stderr, "Handshake failed\n");
        secLinkClose(link);
        return 1;
    }

    // Send an encrypted banner and then relay keyboard input.
    const char *hello = "Hello, secure BBS!\r\n";
    secLinkSend(link, (const uint8_t *)hello,
                (int)strlen(hello), CHAN_TERMINAL, true, true);

    while (!feof(stdin)) {
        // Deliver any incoming packets to onPacket().
        secLinkPoll(link);

        // If the user typed something, forward it encrypted.
        int ch = getchar();
        if (ch == EOF) {
            break;
        }
        uint8_t byte = (uint8_t)ch;
        secLinkSend(link, &byte, 1, CHAN_TERMINAL, true, true);
    }

    secLinkClose(link);
    return 0;
}
.endcode

Notes:

.list
.item secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.
.item The RNG must be seeded before secLinkHandshake. secRngGatherEntropy provides roughly 20 bits of hardware entropy; supplement with user interaction timing for cryptographic use.
.item secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.
.item For bulk transfers larger than SECLINK_MAX_PAYLOAD (254 bytes), use secLinkSendBuf which splits the data into chunks automatically.
.endlist
