DVX_GUI/seclink/README.md
2026-03-20 20:00:05 -05:00

450 lines
15 KiB
Markdown

# SecLink -- Secure Serial Link Library
SecLink is the top-level API for the DVX serial/networking stack. It
composes three lower-level libraries into a single interface for reliable,
optionally encrypted, channel-multiplexed serial communication:
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
SecLink adds channel multiplexing and per-packet encryption control on
top of the packet layer's reliable delivery.
## Architecture
```
Application
|
| secLinkSend() send data on a channel, optionally encrypted
| secLinkPoll() receive, decrypt, deliver to callback
| secLinkHandshake() DH key exchange (blocking)
|
[SecLink] channel header, encrypt/decrypt, key management
|
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
[RS232] ISR-driven UART, 2048-byte ring buffers
|
UART Hardware
```
### Channel Multiplexing
SecLink prepends a one-byte header to every packet's payload before
handing it to the packet layer:
```
Bit 7 Bits 6..0
----- ---------
Encrypt Channel (0-127)
```
This allows up to 128 independent logical channels over a single serial
link. Each channel can carry a different type of traffic (terminal data,
file transfer, control messages, etc.) without needing separate framing
or sequencing per stream. The receive callback includes the channel
number so the application can dispatch accordingly.
The encrypt flag (bit 7) tells the receiver whether the payload portion
of this packet is encrypted. The channel header byte itself is always
sent in the clear.
### Mixed Clear and Encrypted Traffic
Unencrypted packets can be sent before or after the DH handshake. This
enables a startup protocol (version negotiation, capability exchange)
before keys are established. Encrypted packets require a completed
handshake -- attempting to send an encrypted packet before the handshake
returns `SECLINK_ERR_NOT_READY`.
On the receive side, encrypted packets arriving before the handshake is
complete are silently dropped. Cleartext packets are delivered regardless
of handshake state.
## Lifecycle
```
secLinkOpen() Open COM port and packet layer
secLinkHandshake() DH key exchange (blocks until both sides complete)
secLinkSend() Send data on a channel (encrypted or cleartext)
secLinkPoll() Receive and deliver packets to callback
secLinkClose() Tear down everything (ciphers, packet, COM port)
```
### Handshake Protocol
The DH key exchange uses the packet layer's reliable delivery, so lost
packets are automatically retransmitted. Both sides can send their public
key simultaneously -- there is no initiator/responder distinction.
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
2. Both sides send their 128-byte public key as a single packet
3. On receiving the remote's public key, each side immediately computes
the shared secret (`remote^private mod p`)
4. Each side derives separate TX and RX cipher keys from the master key
5. Cipher contexts are created and the link transitions to READY state
6. The DH context (containing the private key) is destroyed immediately
**Directional key derivation:**
The side with the lexicographically lower public key uses
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
side uses the reverse assignment. This is critical for CTR mode security:
if both sides used the same key and counter, they would produce identical
keystreams, and XORing two ciphertexts would reveal the XOR of the
plaintexts. The XOR-derived directional keys ensure each direction has a
unique keystream even though both sides start their counters at zero.
**Forward secrecy:**
The DH context (containing the private key and shared secret) is
destroyed immediately after deriving the session cipher keys. Even if
the application's long-term state is compromised later, past session
keys cannot be recovered from memory.
## Payload Size
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
(254 bytes). This is the packet layer's 255-byte maximum minus the
1-byte channel header that SecLink prepends.
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
automatically splits the data into 254-byte chunks and sends each one
with blocking delivery.
## API Reference
### Types
```c
// Receive callback -- delivers plaintext with channel number
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
uint8_t channel);
// Opaque connection handle
typedef struct SecLinkS SecLinkT;
```
The receive callback is invoked from `secLinkPoll()` for each incoming
packet. Encrypted packets are decrypted before delivery -- the callback
always receives plaintext regardless of whether encryption was used on
the wire. The `data` pointer is valid only during the callback.
### Constants
| Name | Value | Description |
|-------------------------|-------|---------------------------------------|
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
### 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 with default
window size (4), and returns a link handle. The callback is invoked from
`secLinkPoll()` for each received packet (decrypted if applicable).
Returns `NULL` on failure (serial port error, packet layer allocation
error, or memory allocation failure). On failure, all partially
initialized resources are cleaned up.
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
- `bps` -- baud rate (50 through 115200)
- `dataBits` -- 5, 6, 7, or 8
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
- `stopBits` -- 1 or 2
- `handshake` -- `RS232_HANDSHAKE_*` constant
- `callback` -- receive callback function
- `ctx` -- user pointer passed through to the callback
#### secLinkClose
```c
void secLinkClose(SecLinkT *link);
```
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
destroys the DH context if still present, closes the packet layer, closes
the COM port, zeroes the link structure, and frees 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.
Internally:
1. Creates a DH context and generates keys
2. Sends the 128-byte public key via the packet layer (blocking)
3. Polls the packet layer in a loop until the remote's public key arrives
4. Computes the shared secret and derives directional cipher keys
5. Destroys the DH context (forward secrecy)
6. Transitions the link to READY state
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
(DH key generation failure, send failure, or serial disconnect during
the exchange).
#### 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` (254) bytes on the given channel.
- `channel` -- logical channel number (0-127)
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
- `block` -- if `true`, waits for transmit window space. If `false`,
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
**Cipher counter safety:** The function checks transmit window space
BEFORE encrypting the payload. If it encrypted first and then the send
failed, the cipher counter would advance without the data being sent,
permanently desynchronizing the TX cipher state from the remote's RX
cipher. This ordering is critical for correctness.
The channel header byte is prepended to the data, and only the payload
portion (not the header) is encrypted.
Returns `SECLINK_SUCCESS` or an error code.
#### 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 (254-byte) chunks. Always blocks until all
data is sent. The receiver sees multiple packets on the same channel and
must reassemble if needed.
Returns `SECLINK_SUCCESS` or the first error encountered.
#### secLinkPoll
```c
int secLinkPoll(SecLinkT *link);
```
Delegates to `pktPoll()` to read serial data, process frames, handle
ACKs and retransmits. Received packets are routed through an internal
callback that:
- During handshake: expects a 128-byte DH public key
- When ready: strips the channel header, decrypts the payload if the
encrypt flag is set, and forwards plaintext to the user callback
Returns the number of packets delivered, or a negative error code.
Must be called frequently (every iteration of your main loop).
#### secLinkGetPending
```c
int secLinkGetPending(SecLinkT *link);
```
Returns the number of unacknowledged packets in the transmit window.
Delegates directly to `pktGetPending()`. Useful for non-blocking send
loops to determine when there is room to send more data.
#### secLinkIsReady
```c
bool secLinkIsReady(SecLinkT *link);
```
Returns `true` if the DH handshake is complete and the link is ready for
encrypted communication. Cleartext sends do not require the link to be
ready.
## Usage 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, no flow control
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
RS232_HANDSHAKE_NONE, onRecv, NULL);
if (!link) {
return 1;
}
// DH 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
// Cleartext status message (no handshake needed)
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
// 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 offset = 0;
int bytesLeft = fileSize;
while (bytesLeft > 0) {
secLinkPoll(link); // process ACKs, free window slots
if (secLinkGetPending(link) < 4) { // window has room
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, 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);
```
## Internal State Machine
SecLink maintains a three-state internal state machine:
| State | Value | Description |
|--------------|-------|----------------------------------------------|
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
Transitions:
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
- `HANDSHAKE -> READY`: when the remote's public key is received and
cipher keys are derived
- Any state -> cleanup: when `secLinkClose()` is called
Cleartext packets can be sent and received in any state. Encrypted
packets require `STATE_READY`.
## Building
```
make # builds ../lib/libseclink.a
make clean # removes objects and library
```
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
Link against all four libraries in this order:
```
-lseclink -lpacket -lsecurity -lrs232
```
## Files
- `secLink.h` -- Public API header (types, constants, function prototypes)
- `secLink.c` -- Complete implementation (handshake, send, receive, state
machine)
- `Makefile` -- DJGPP cross-compilation build rules
## Dependencies
SecLink requires these libraries (all built into `../lib/`):
| Library | Purpose |
|------------------|---------------------------------------------|
| `librs232.a` | Serial port driver (ISR, ring buffers) |
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |