Skip to content

include/qrnsp_stego.h

/*
 * QR-NSP Volcanic Edition — Steganographic Injection Engine
 * Module 3: QUIC PADDING frame replacement
 *
 * Hides encrypted payloads inside QUIC PADDING frames.
 * PADDING frames (RFC 9000 §19.1) are 0x00 bytes that QUIC endpoints
 * add to meet minimum packet sizes. They're the perfect carrier:
 *   - Already present in most QUIC packets
 *   - Variable length (tens to hundreds of bytes)
 *   - Receivers discard them — our injection is invisible to the QUIC stack
 *   - Statistical profile: random after encryption ≈ random padding
 *
 * Wire format of an injected PADDING region:
 *   [MAGIC:2][LEN:2][NONCE:12][CIPHERTEXT:N][TAG:16][REMAINING_PAD:M]
 *
 *   MAGIC  = 2 bytes: session-derived marker (not static — derived from ss)
 *   LEN    = 2 bytes: payload length (big-endian)
 *   NONCE  = 12 bytes: AES-GCM nonce (counter-based)
 *   CT     = N bytes: encrypted payload
 *   TAG    = 16 bytes: AES-GCM authentication tag
 *   PAD    = remaining 0x00 bytes to fill original PADDING region
 *
 * Overhead per injection: 32 bytes (2+2+12+16)
 * Minimum PADDING region: 64 bytes → 32 bytes usable payload
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#ifndef QRNSP_STEGO_H
#define QRNSP_STEGO_H

#include <stdint.h>
#include <stddef.h>

/* ─────────────────────────────────────────────
 * Constants
 * ───────────────────────────────────────────── */

#define STEGO_MAGIC_BYTES    2
#define STEGO_LEN_BYTES      2
#define STEGO_NONCE_BYTES    12
#define STEGO_TAG_BYTES      16
#define STEGO_OVERHEAD       (STEGO_MAGIC_BYTES + STEGO_LEN_BYTES + \
                              STEGO_NONCE_BYTES + STEGO_TAG_BYTES)  /* 32 */
#define STEGO_MIN_PAD_REGION 64   /* Minimum padding to attempt injection */
#define STEGO_MAX_PAYLOAD    1400 /* Maximum single-packet payload        */

/* Session states */
typedef enum {
    STEGO_STATE_IDLE,        /* No active session                    */
    STEGO_STATE_HANDSHAKE,   /* Hybrid KEM exchange in progress      */
    STEGO_STATE_READY,       /* Shared secret established, can inject*/
    STEGO_STATE_ERROR        /* Unrecoverable error                  */
} stego_state_t;

/* Direction */
typedef enum {
    STEGO_DIR_INJECT,        /* Writing payloads into PADDING        */
    STEGO_DIR_EXTRACT        /* Reading payloads from PADDING        */
} stego_dir_t;

/* ─────────────────────────────────────────────
 * Session Context
 *
 * One context per active covert channel.
 * Thread-safe: each session gets its own context.
 * ───────────────────────────────────────────── */

typedef struct {
    /* State */
    stego_state_t state;

    /* Cryptographic material (from hybrid KEM) */
    uint8_t  shared_secret[32]; /* From hybrid_encapsulate/decapsulate */
    uint8_t  tx_key[32];        /* Derived: SHA3-256(ss || "tx")       */
    uint8_t  rx_key[32];        /* Derived: SHA3-256(ss || "rx")       */
    uint8_t  magic[2];          /* Derived: SHA3-256(ss || "magic")[0:2]*/

    /* Nonce counters (monotonic, never reuse) */
    uint64_t tx_nonce_ctr;
    uint64_t rx_nonce_ctr;

    /* Statistics */
    uint64_t tx_bytes;           /* Total payload bytes injected       */
    uint64_t rx_bytes;           /* Total payload bytes extracted      */
    uint64_t tx_packets;         /* Packets with injected payload      */
    uint64_t rx_packets;         /* Packets with extracted payload     */
    uint64_t padding_regions_seen;
    uint64_t padding_too_small;  /* Regions too small to use           */
} stego_session_t;

/* ─────────────────────────────────────────────
 * Result codes
 * ───────────────────────────────────────────── */

typedef enum {
    STEGO_OK = 0,
    STEGO_ERR_NO_SESSION,     /* Session not initialized              */
    STEGO_ERR_NO_PADDING,     /* No padding region found in packet    */
    STEGO_ERR_PAD_TOO_SMALL,  /* Padding region too small for payload */
    STEGO_ERR_PAYLOAD_TOO_BIG,/* Payload exceeds available space      */
    STEGO_ERR_MAGIC_MISMATCH, /* Not a QR-NSP packet (wrong magic)    */
    STEGO_ERR_AUTH_FAIL,      /* AEAD authentication failed           */
    STEGO_ERR_NONCE_REUSE,    /* Nonce counter regression detected    */
    STEGO_ERR_CRYPTO,         /* Generic crypto error                 */
} stego_result_t;

/* ─────────────────────────────────────────────
 * API
 * ───────────────────────────────────────────── */

/*
 * Initialize session from hybrid KEM shared secret.
 * Derives tx_key, rx_key, and magic marker.
 * role: 0 = initiator (tx_key from "init_tx"), 1 = responder (keys swapped)
 */
int stego_session_init(stego_session_t *sess,
                       const uint8_t shared_secret[32],
                       int role);

/*
 * Inject payload into a QUIC packet's PADDING region.
 *
 * pkt:     mutable packet buffer (UDP payload from XDP ring)
 * pkt_len: packet length
 * payload: data to hide
 * pay_len: payload length
 *
 * Returns STEGO_OK on success. Packet is modified in-place.
 * The function finds the first PADDING region ≥ STEGO_MIN_PAD_REGION,
 * encrypts the payload, and writes the stego frame in place of the padding.
 */
stego_result_t stego_inject(stego_session_t *sess,
                            uint8_t *pkt, size_t pkt_len,
                            const uint8_t *payload, size_t pay_len);

/*
 * Extract payload from a QUIC packet's PADDING region.
 *
 * pkt:     packet buffer (read-only)
 * pkt_len: packet length
 * out:     output buffer for extracted payload
 * out_cap: output buffer capacity
 * out_len: actual extracted length (set on success)
 *
 * Returns STEGO_OK if a valid stego frame was found and decrypted.
 * Returns STEGO_ERR_MAGIC_MISMATCH if this isn't a QR-NSP packet (normal).
 */
stego_result_t stego_extract(stego_session_t *sess,
                             const uint8_t *pkt, size_t pkt_len,
                             uint8_t *out, size_t out_cap,
                             size_t *out_len);

/*
 * Probe: check if a packet contains QR-NSP steganographic data
 * without attempting full decryption. Fast path for filtering.
 *
 * Returns 1 if magic matches, 0 otherwise.
 */
int stego_probe(const stego_session_t *sess,
                const uint8_t *pkt, size_t pkt_len);

/*
 * Destroy session and zeroize all key material.
 */
void stego_session_destroy(stego_session_t *sess);

/*
 * Query maximum injectable payload for a given packet.
 * Returns 0 if no suitable padding region exists.
 */
size_t stego_max_payload(const uint8_t *pkt, size_t pkt_len);

#endif /* QRNSP_STEGO_H */