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

// 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)
// DH wire size (SEC_DH_KEY_SIZE) and bignum byte width (BN_BYTES) must agree;
// they describe the same 1024-bit value and are used interchangeably.
typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1];

// R^2 mod m initialization uses repeated doubling (shift-left-by-1 + mod).
// Starting from 1, 2*BN_BITS doublings take us to 2^(2*BN_BITS) mod m = R^2.
#define R2_DOUBLE_ITERATIONS (2 * BN_BITS)

#define DH_PRIVATE_BITS     256
#define DH_PRIVATE_BYTES    (DH_PRIVATE_BITS / 8)

#define XTEA_ROUNDS         32
#define XTEA_DELTA          0x9E3779B9
#define XTEA_BLOCK_SIZE     8           // XTEA operates on 8-byte (two-uint32_t) blocks

// Newton iterations to get 32 correct bits of -m0^(-1) (1->2->4->8->16->32)
#define M0INV_NEWTON_ITERS  5

// DRBG seed warmup: bytes generated and discarded after seeding
#define DRBG_DISCARD_BYTES  64

// Hardware entropy sources for the DRBG seed
#define PIT_CMD_PORT        0x43        // PIT mode/command register
#define PIT_CH0_PORT        0x40        // PIT channel 0 data port
#define PIT_LATCH_CMD       0x00        // Latch counter command for channel 0
#define BIOS_TICK_ADDR      0x46C       // BIOS tick count linear address (18.2 Hz)


// ========================================================================
// 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 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.
// Verified against the RFC: the value is a safe prime (p and (p-1)/2
// both prime).  A previous transcription of this constant had corrupted
// low words, making the modulus composite (factors 127 and 63463) and
// the discrete log breakable via Pohlig-Hellman; do not hand-edit.
static const BigNumT sDhPrime = { .w = {
    0xFFFFFFFF, 0xFFFFFFFF, 0xECE65381, 0x49286651,
    0x7C4B1FE6, 0xAE9F2411, 0x5A899FA5, 0xEE386BFB,
    0xF406B7ED, 0x0BFF5CB6, 0xA637ED6B, 0xF44C42E9,
    0x625E7EC6, 0xE485B576, 0x6D51C245, 0x4FE1356D,
    0xF25F1437, 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 on first DH operation).
// These are expensive to compute (~2048 shift-and-subtract operations for R2)
// but only need to be done once since we always use the same prime.
// R = 2^1024 in Montgomery arithmetic; R^2 mod p is the conversion factor.
// m0inv = -p[0]^(-1) mod 2^32 is the Montgomery reduction constant.
static BigNumT  sDhR2;
static uint32_t sDhM0Inv;
static bool     sDhInited = false;

// RNG state
static RngStateT sRng = { .seeded = false };


// ========================================================================
// Prototypes (alphabetical)
// ========================================================================

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);
SecCipherT     *secCipherCreate(const uint8_t *key);
void            secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
void            secCipherDestroy(SecCipherT *c);
int             secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
SecDhT         *secDhCreate(void);
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);
void            secRngAddEntropy(const uint8_t *data, int len);
void            secRngBytes(uint8_t *buf, int len);
int             secRngGatherEntropy(uint8_t *buf, int len);
void            secRngSeed(const uint8_t *entropy, int len);
static void     secureZero(void *ptr, int len);
static void     xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int32_t len, bool xorMode);
static void     xteaEncryptBlock(uint32_t v[2], const uint32_t key[4]);


// ========================================================================
// Functions (alphabetical)
// ========================================================================

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];
    }
}


// Modular exponentiation using Montgomery multiplication.
//
// Montgomery multiplication replaces expensive modular reduction (division
// by a 1024-bit number) with cheaper additions and right-shifts, at the
// cost of converting operands to/from "Montgomery form" (multiply by R mod m).
// For exponentiation where we do hundreds of multiplications with the same
// modulus, the conversion cost is amortized and the net speedup is ~3-5x
// over schoolbook multiply-then-reduce on a 486.
//
// Uses left-to-right binary (square-and-multiply) scanning of the exponent.
// For a 256-bit private exponent, this is ~256 squarings + ~128 multiplies
// on average (half the bits are 1).
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);
}


// Montgomery multiplication: computes (a * b * R^(-1)) mod m without division.
//
// The algorithm processes one word of 'a' per outer iteration (32 words for
// 1024-bit numbers). For each word:
//   1. Accumulate a[i] * b into the temporary product t
//   2. Compute the Montgomery reduction factor u = t[0] * m0inv (mod 2^32)
//   3. Add u * mod to t and shift right by 32 bits (the division by 2^32)
//
// The shift-and-reduce avoids explicit modular reduction. After all 32
// iterations, the result is in [0, 2m), so a single conditional subtraction
// brings it into [0, m). This is the CIOS (Coarsely Integrated Operand
// Scanning) variant, which is cache-friendly because it accesses 'b' and
// 'mod' sequentially in the inner loops.
static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b, const BigNumT *mod, uint32_t m0inv) {
    uint32_t t[BN_WORDS + 1];

    memset(t, 0, sizeof(t));

    for (int i = 0; i < BN_WORDS; i++) {
        uint32_t u;
        uint64_t carry;
        uint64_t prod;
        uint64_t sum;

        // 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);
    }
}


// Compute -m0^(-1) mod 2^32 using Newton's method for modular inverse.
// Starting from x=1 (which is always a valid initial approximation for
// odd m0), each iteration doubles the number of correct bits. After 5
// iterations we have 32 correct bits (1->2->4->8->16->32). This is the
// standard approach for computing the Montgomery constant.
static uint32_t computeM0Inv(uint32_t m0) {
    uint32_t x = 1;

    for (int i = 0; i < M0INV_NEWTON_ITERS; i++) {
        x = x * (2 - m0 * x);
    }

    // Return -m0^(-1) mod 2^32
    return ~x + 1;
}


// Compute R^2 mod m where R = 2^1024. This is the Montgomery domain
// conversion factor. We compute it by repeated doubling (shift left by 1)
// with modular reduction, which is simple but takes 2048 iterations.
// Only done once at initialization time.
static void computeR2(BigNumT *r2, const BigNumT *m) {
    bnSet(r2, 1);

    for (int i = 0; i < R2_DOUBLE_ITERATIONS; i++) {
        int carry = bnShiftLeft1(r2);
        if (carry || 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;
}


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;
}


// CTR-mode encryption/decryption. The counter is encrypted to produce
// an 8-byte keystream block, which is XOR'd with the data. The counter
// increments after each block. Because XOR is its own inverse, the same
// function handles both encryption and decryption.
//
// IMPORTANT: the counter is internal state that advances with each call.
// The secLink layer ensures that TX and RX use separate cipher instances
// with separate counters, and that the same counter value is never reused
// with the same key (which would be catastrophic for CTR mode security).
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len) {
    if (!c || !data || len <= 0) {
        return;
    }

    xteaCtrKeystream(c->counter, c->key, data, len, true);
}


void secCipherDestroy(SecCipherT *c) {
    if (c) {
        secureZero(c, sizeof(SecCipherT));
        free(c);
    }
}


// Compute the shared secret from the remote side's public key.
// Validates that the remote key is in [2, p-2] to prevent small-subgroup
// attacks (keys of 0, 1, or p-1 would produce trivially guessable secrets).
// The shared secret is remote^private mod p, which both sides compute
// independently and arrive at the same value (the DH property).
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;
}


// Derive a symmetric key from the 128-byte shared secret by XOR-folding.
// This is a simple key derivation function: each byte of the secret is
// XOR'd into the output key at position (i % keyLen). For a 16-byte XTEA
// key, each output byte is the XOR of 8 secret bytes, providing good
// mixing. A proper KDF (HKDF, etc.) would be better but adds complexity
// and code size for marginal benefit in this use case.
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;
}


// secureZero the entire struct before freeing to prevent the private key
// from lingering in freed memory where it could be read by a later malloc.
void secDhDestroy(SecDhT *dh) {
    if (dh) {
        secureZero(dh, sizeof(SecDhT));
        free(dh);
    }
}


// Generate a DH keypair: random 256-bit private key, then compute
// public = g^private mod p. The private key is only 256 bits (not 1024)
// to keep exponentiation fast on 486-class hardware. With Montgomery
// multiplication, this takes ~256 squarings + ~128 multiplies, each
// operating on 32-word (1024-bit) numbers.
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;
}


// Mix additional entropy into the RNG state. XOR-folding into the key is
// simple and cannot reduce entropy (XOR with random data is a bijection).
// The re-mix step (encrypting the key with itself) diffuses the new entropy
// across all key bits so that even a single byte of good entropy improves
// the entire key state.
void secRngAddEntropy(const uint8_t *data, int len) {
    for (int i = 0; i < len; i++) {
        ((uint8_t *)sRng.key)[i % SEC_XTEA_KEY_SIZE] ^= 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];
}


// Generate pseudorandom bytes using XTEA-CTR DRBG. Each call encrypts
// the monotonically increasing counter with the RNG key, producing 8 bytes
// of keystream per block. The counter never repeats (64-bit space), so
// the output is a pseudorandom stream as long as the key has sufficient
// entropy. Auto-seeds from hardware entropy on first use as a safety net.
void secRngBytes(uint8_t *buf, int len) {
    if (!sRng.seeded) {
        uint8_t entropy[SEC_XTEA_KEY_SIZE];
        int     got = secRngGatherEntropy(entropy, sizeof(entropy));
        secRngSeed(entropy, got);
    }

    xteaCtrKeystream(sRng.counter, sRng.key, buf, len, false);
}


// Gather hardware entropy from the PIT (Programmable Interval Timer) and
// BIOS tick count. The PIT runs at 1.193182 MHz, so its LSBs change rapidly
// and provide ~10 bits of entropy per read (depending on timing jitter).
// The BIOS tick at 18.2 Hz adds a few more bits. Two PIT readings with
// the intervening code execution provide some jitter. Total: roughly 20
// bits of real entropy  -- not enough alone, but sufficient to seed the DRBG
// when supplemented by user interaction timing.
int secRngGatherEntropy(uint8_t *buf, int len) {
    int out = 0;
    outportb(PIT_CMD_PORT, PIT_LATCH_CMD);
    uint8_t pitLo = inportb(PIT_CH0_PORT);
    uint8_t pitHi = inportb(PIT_CH0_PORT);

    // BIOS tick count (18.2 Hz)
    uint32_t ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR);

    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(PIT_CMD_PORT, PIT_LATCH_CMD);
    pitLo = inportb(PIT_CH0_PORT);
    pitHi = inportb(PIT_CH0_PORT);
    if (out < len) { buf[out++] = pitLo; }
    if (out < len) { buf[out++] = pitHi; }

    return out;
}


// Initialize the RNG from entropy. The 64-byte discard at the end is
// standard DRBG practice  -- it advances the state past any weak initial
// output that might leak information about the seed material.
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 % SEC_XTEA_KEY_SIZE] ^= 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];

    // Mix state by generating and discarding 64 bytes
    uint8_t discard[DRBG_DISCARD_BYTES];
    sRng.seeded = true; // prevent recursion in secRngBytes
    secRngBytes(discard, sizeof(discard));
    secureZero(discard, sizeof(discard));
}


// Volatile pointer prevents the compiler from optimizing away the zeroing
// as a dead store. Critical for clearing key material  -- without volatile,
// the compiler sees that ptr is about to be freed and removes the memset.
static void secureZero(void *ptr, int len) {
    volatile uint8_t *p = (volatile uint8_t *)ptr;

    for (int i = 0; i < len; i++) {
        p[i] = 0;
    }
}


// Generates XTEA-CTR keystream into out for len bytes, advancing the 64-bit
// counter in place.  When xorMode is true the keystream is XOR'd into out
// (cipher); when false it is copied (DRBG output).  Shared by secCipherCrypt
// and secRngBytes so the counter/keystream logic lives in one place.
static void xteaCtrKeystream(uint32_t counter[2], const uint32_t key[4], uint8_t *out, int32_t len, bool xorMode) {
    uint32_t block[2];
    int32_t  pos = 0;

    while (pos < len) {
        int32_t take;

        block[0] = counter[0];
        block[1] = counter[1];
        xteaEncryptBlock(block, key);

        take = len - pos;

        if (take > XTEA_BLOCK_SIZE) {
            take = XTEA_BLOCK_SIZE;
        }

        if (xorMode) {
            const uint8_t *keystream = (const uint8_t *)block;

            for (int32_t i = 0; i < take; i++) {
                out[pos + i] ^= keystream[i];
            }
        } else {
            memcpy(out + pos, block, take);
        }

        pos += take;

        if (++counter[0] == 0) {
            counter[1]++;
        }
    }
}


// XTEA block cipher: encrypts an 8-byte block in-place. The Feistel network
// uses 32 rounds (vs TEA's 32 or 64). Each round mixes the halves using
// shifts, adds, and XORs  -- no S-boxes, no lookup tables, no key schedule.
// The delta constant (golden ratio * 2^32) ensures each round uses a
// different effective key, preventing slide attacks.
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;
}
