Skip to content

src/deniable/deniable.c

/*
 * QR-NSP Volcanic Edition — Deniable Encryption Engine
 * Module 5: Multi-password hidden volumes + honey-encryption
 *
 * Key insight: the entire container is random bytes. Without the
 * correct password, an adversary cannot prove ANY volume exists.
 * Honey-encryption ensures that wrong passwords still yield output
 * that looks like real data, defeating brute-force triage.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#include "qrnsp_deniable.h"
#include "qrnsp_aead.h"
#include "mlkem_params.h"  /* SHA3 wrappers */
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#if defined(__linux__)
#include <sys/random.h>
#endif

/* ─────────────────────────────────────────────
 * Entropy
 * ───────────────────────────────────────────── */

static int
secure_random(uint8_t *out, size_t len)
{
#if defined(__linux__)
    return (getrandom(out, len, 0) == (ssize_t)len) ? 0 : -1;
#else
    FILE *f = fopen("/dev/urandom", "rb");
    if (!f) return -1;
    size_t n = fread(out, 1, len, f);
    fclose(f);
    return (n == len) ? 0 : -1;
#endif
}

/* ─────────────────────────────────────────────
 * PBKDF2-SHA3-256
 *
 * HMAC-SHA3-256 based key derivation.
 * Deliberately slow to resist brute-force.
 *
 * For production: replace with Argon2id (memory-hard).
 * PBKDF2 is used here for zero-dependency compilation.
 * ───────────────────────────────────────────── */

/* HMAC-SHA3-256: H(K ⊕ opad || H(K ⊕ ipad || message)) */
static void
hmac_sha3_256(uint8_t out[32],
              const uint8_t *key, size_t key_len,
              const uint8_t *msg, size_t msg_len)
{
    uint8_t k_pad[136]; /* SHA3-256 block size = rate = 136 bytes */
    uint8_t ipad[136], opad[136];
    uint8_t inner_hash[32];

    /* If key > block size, hash it first */
    uint8_t k_derived[32];
    if (key_len > 136) {
        mlkem_hash_h(k_derived, key, key_len);
        key = k_derived;
        key_len = 32;
    }

    /* Pad key to block size */
    memset(k_pad, 0, 136);
    memcpy(k_pad, key, key_len);

    /* ipad = k_pad ⊕ 0x36, opad = k_pad ⊕ 0x5C */
    for (int i = 0; i < 136; i++) {
        ipad[i] = k_pad[i] ^ 0x36;
        opad[i] = k_pad[i] ^ 0x5C;
    }

    /* Inner hash: H(ipad || msg) */
    uint8_t *inner_buf = malloc(136 + msg_len);
    if (!inner_buf) { memset(out, 0, 32); return; }
    memcpy(inner_buf, ipad, 136);
    memcpy(inner_buf + 136, msg, msg_len);
    mlkem_hash_h(inner_hash, inner_buf, 136 + msg_len);
    free(inner_buf);

    /* Outer hash: H(opad || inner_hash) */
    uint8_t outer_buf[136 + 32];
    memcpy(outer_buf, opad, 136);
    memcpy(outer_buf + 136, inner_hash, 32);
    mlkem_hash_h(out, outer_buf, 136 + 32);

    memset(k_pad, 0, sizeof(k_pad));
    memset(ipad, 0, sizeof(ipad));
    memset(opad, 0, sizeof(opad));
    memset(inner_hash, 0, sizeof(inner_hash));
    memset(k_derived, 0, sizeof(k_derived));
}

/*
 * PBKDF2-HMAC-SHA3-256
 *
 * dk = T1 || T2 || ...
 * Ti = U1 ⊕ U2 ⊕ ... ⊕ Uc
 * U1 = HMAC(password, salt || INT(i))
 * Uj = HMAC(password, U_{j-1})
 */
static void
pbkdf2_sha3(uint8_t *dk, size_t dk_len,
             const uint8_t *password, size_t pass_len,
             const uint8_t *salt, size_t salt_len,
             uint32_t iterations)
{
    uint32_t block_count = (uint32_t)((dk_len + 31) / 32);
    uint8_t *salt_block = malloc(salt_len + 4);
    if (!salt_block) return;
    memcpy(salt_block, salt, salt_len);

    for (uint32_t i = 1; i <= block_count; i++) {
        /* salt || INT_32_BE(i) */
        salt_block[salt_len]     = (uint8_t)(i >> 24);
        salt_block[salt_len + 1] = (uint8_t)(i >> 16);
        salt_block[salt_len + 2] = (uint8_t)(i >> 8);
        salt_block[salt_len + 3] = (uint8_t)(i);

        uint8_t U[32], T[32];
        hmac_sha3_256(U, password, pass_len, salt_block, salt_len + 4);
        memcpy(T, U, 32);

        for (uint32_t j = 1; j < iterations; j++) {
            hmac_sha3_256(U, password, pass_len, U, 32);
            for (int k = 0; k < 32; k++) T[k] ^= U[k];
        }

        /* Copy block to output */
        size_t offset = (i - 1) * 32;
        size_t copy = (dk_len - offset < 32) ? dk_len - offset : 32;
        memcpy(dk + offset, T, copy);

        memset(U, 0, sizeof(U));
        memset(T, 0, sizeof(T));
    }

    memset(salt_block, 0, salt_len + 4);
    free(salt_block);
}

/* ─────────────────────────────────────────────
 * Key Derivation: password + salt → (key, nonce, volume_offset)
 *
 * We derive 76 bytes:
 *   key[32] || nonce[12] || volume_locator[32]
 *
 * volume_locator determines WHERE in the container this volume
 * lives. This is the key deniability property: without the password,
 * you don't know where to look, and every position looks random.
 * ───────────────────────────────────────────── */

#define KDF_OUTPUT_LEN  76

static void
derive_volume_params(uint8_t out[KDF_OUTPUT_LEN],
                     const char *password, size_t pass_len,
                     const uint8_t salt[DENY_SALT_BYTES],
                     uint32_t iterations)
{
    pbkdf2_sha3(out, KDF_OUTPUT_LEN,
                (const uint8_t *)password, pass_len,
                salt, DENY_SALT_BYTES,
                iterations);
}

/*
 * Compute volume offset within container from the locator.
 * Maps 32-byte locator to an offset in [header_end, container_end - volume_size]
 */
static size_t
compute_volume_offset(const uint8_t locator[32],
                      size_t container_size,
                      size_t header_size,
                      size_t volume_size)
{
    /* Use first 8 bytes of locator as uint64, modulo available space */
    uint64_t loc = 0;
    for (int i = 0; i < 8; i++)
        loc |= ((uint64_t)locator[i]) << (i * 8);

    size_t available = container_size - header_size - volume_size;
    if (available == 0) return header_size;

    /* Align to 16-byte boundary for cache friendliness */
    size_t offset = header_size + (size_t)(loc % available);
    offset &= ~(size_t)0xF;
    if (offset < header_size) offset = header_size;
    if (offset + volume_size > container_size)
        offset = container_size - volume_size;

    return offset;
}

/* ─────────────────────────────────────────────
 * Honey-Encryption: Distribution-Transforming Encoder
 *
 * Maps any AES-GCM "decryption" (which produces random-looking
 * bytes for wrong keys) through a DTE that outputs text matching
 * a target distribution.
 *
 * Approach: use the "decrypted" bytes as a seed for a Markov chain
 * generator. The output looks like English/JSON/code regardless of
 * whether the key was correct.
 * ───────────────────────────────────────────── */

/* Simple order-1 character-level English Markov chain.
 * Transition table: given prev char class, emit next char.
 * 5 classes: vowel(0), consonant(1), space(2), punctuation(3), digit(4) */

static const char vowels[]     = "aeiouy";
static const char consonants[] = "bcdfghjklmnpqrstvwxz";
static const char spaces[]     = " ";
static const char punct[]      = ".,;:!?'-";
static const char digits[]     = "0123456789";

/* Transition probabilities (simplified, 5×5 matrix as cumulative %) */
/* Rows: current class, Cols: next class threshold */
static const uint8_t english_transitions[5][5] = {
    /* From vowel:     vowel  cons  space  punct  digit */
    {   15,   80,   95,   100,  100 },  /* Mostly consonants after vowels */
    /* From consonant: */
    {   55,   75,   93,    99,  100 },  /* Mostly vowels after consonants */
    /* From space:     */
    {   20,   78,   80,    82,  100 },  /* Start of word: cons > vowel */
    /* From punct:     */
    {   10,   20,   90,    92,  100 },  /* Space after punctuation */
    /* From digit:     */
    {    5,   10,   40,    45,  100 },  /* More digits after digits */
};

static int
char_class(char c)
{
    if (c >= 'a' && c <= 'z') {
        if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u'||c=='y') return 0;
        return 1;
    }
    if (c == ' ' || c == '\n') return 2;
    if (c >= '0' && c <= '9') return 4;
    return 3;
}

static char
pick_char(int class_id, uint8_t random_byte)
{
    /* Map random byte to character within the class */
    switch (class_id) {
    case 0: return vowels[random_byte % 6];
    case 1: return consonants[random_byte % 20];
    case 2: return ' ';
    case 3: return punct[random_byte % 8];
    case 4: return digits[random_byte % 10];
    default: return ' ';
    }
}

static void
honey_generate_english(uint8_t *out, size_t len, const uint8_t *seed, size_t seed_len)
{
    int prev_class = 2; /* Start as if after space */
    size_t si = 0;
    uint8_t word_len = 0;

    for (size_t i = 0; i < len; i++) {
        /* Use seed bytes as entropy source */
        uint8_t r = seed[si % seed_len];
        si++;
        uint8_t r2 = seed[si % seed_len];
        si++;

        /* Pick next class based on transition table */
        uint8_t pct = r % 100;
        int next_class = 4;
        for (int c = 0; c < 5; c++) {
            if (pct < english_transitions[prev_class][c]) {
                next_class = c;
                break;
            }
        }

        /* Force word boundaries (no words > 12 chars) */
        if (next_class <= 1) {
            word_len++;
            if (word_len > 3 + (r2 % 10)) {
                next_class = 2;
                word_len = 0;
            }
        } else {
            word_len = 0;
        }

        /* Capitalize after period+space */
        char c = pick_char(next_class, r2);
        if (i > 1 && out[i-1] == ' ' && i > 2 &&
            (out[i-2] == '.' || out[i-2] == '!' || out[i-2] == '?')) {
            if (c >= 'a' && c <= 'z') c -= 32;
        }
        /* Capitalize first character */
        if (i == 0 && c >= 'a' && c <= 'z') c -= 32;

        out[i] = (uint8_t)c;
        prev_class = char_class(c);
    }
}

static void
honey_generate_json(uint8_t *out, size_t len, const uint8_t *seed, size_t seed_len)
{
    /* Generate valid-looking JSON */
    const char *keys[] = {"id","name","email","status","count","active","type","value"};
    const char *vals[] = {"null","true","false","42","\"pending\"","\"ok\"","0.5","[]"};
    int nkeys = 8;

    size_t pos = 0;
    size_t si = 0;

    if (pos < len) out[pos++] = '{';

    int first = 1;
    while (pos + 30 < len) {
        if (!first && pos < len) out[pos++] = ',';
        first = 0;

        uint8_t r = seed[si++ % seed_len];
        const char *k = keys[r % nkeys];
        const char *v = vals[seed[si++ % seed_len] % nkeys];

        if (pos < len) out[pos++] = '"';
        for (const char *p = k; *p && pos < len; p++) out[pos++] = (uint8_t)*p;
        if (pos < len) out[pos++] = '"';
        if (pos < len) out[pos++] = ':';
        for (const char *p = v; *p && pos < len; p++) out[pos++] = (uint8_t)*p;
    }

    if (pos < len) out[pos++] = '}';
    while (pos < len) out[pos++] = ' '; /* Pad with spaces */
}

static void
honey_generate(uint8_t *out, size_t len,
               const uint8_t *seed, size_t seed_len,
               honey_dist_t dist)
{
    switch (dist) {
    case HONEY_DIST_ENGLISH_TEXT:
        honey_generate_english(out, len, seed, seed_len);
        break;
    case HONEY_DIST_JSON:
        honey_generate_json(out, len, seed, seed_len);
        break;
    case HONEY_DIST_CODE:
        /* Use English generator with different character distribution */
        honey_generate_english(out, len, seed, seed_len);
        /* Sprinkle code-like characters */
        for (size_t i = 0; i < len; i++) {
            if (seed[i % seed_len] < 10) out[i] = '{';
            else if (seed[i % seed_len] < 20) out[i] = '}';
            else if (seed[i % seed_len] < 25) out[i] = '(';
            else if (seed[i % seed_len] < 30) out[i] = ')';
            else if (seed[i % seed_len] < 33) out[i] = ';';
            else if (seed[i % seed_len] < 36) out[i] = '=';
        }
        break;
    case HONEY_DIST_CSV:
        /* Generate CSV-like rows */
        {
            size_t pos = 0;
            size_t si = 0;
            while (pos < len) {
                /* 3-6 fields per row */
                int fields = 3 + (seed[si++ % seed_len] % 4);
                for (int f = 0; f < fields && pos < len; f++) {
                    if (f > 0 && pos < len) out[pos++] = ',';
                    int flen = 2 + (seed[si++ % seed_len] % 8);
                    for (int c = 0; c < flen && pos < len; c++) {
                        uint8_t r = seed[si++ % seed_len];
                        if (r < 100) out[pos++] = '0' + (r % 10);
                        else out[pos++] = 'a' + (r % 26);
                    }
                }
                if (pos < len) out[pos++] = '\n';
            }
        }
        break;
    case HONEY_DIST_BINARY:
    default:
        /* Just use raw seed bytes — looks like encrypted/compressed data */
        for (size_t i = 0; i < len; i++)
            out[i] = seed[i % seed_len];
        break;
    }
}

/* ═════════════════════════════════════════════
 * Seal: create multi-volume deniable container
 * ═════════════════════════════════════════════ */

size_t
deny_required_size(const deny_volume_t *volumes, int n_volumes)
{
    size_t total = DENY_SALT_BYTES; /* Salt at start */

    for (int i = 0; i < n_volumes; i++) {
        total += DENY_VOLUME_OVERHEAD + volumes[i].plaintext_len;
    }

    /* Add 50% padding for deniability (volumes shouldn't fill container) */
    total = total * 3 / 2;

    /* Round up to 4K boundary */
    total = (total + 4095) & ~(size_t)4095;
    if (total < DENY_CONTAINER_4K) total = DENY_CONTAINER_4K;

    return total;
}

deny_result_t
deny_seal(const deny_volume_t *volumes, int n_volumes,
          deny_container_t *container)
{
    if (n_volumes < 1 || n_volumes > DENY_MAX_PASSWORDS)
        return DENY_ERR_TOO_MANY_VOLUMES;

    size_t cap = container->capacity;

    /* Check capacity */
    size_t needed = DENY_SALT_BYTES;
    for (int i = 0; i < n_volumes; i++)
        needed += DENY_VOLUME_OVERHEAD + volumes[i].plaintext_len;
    if (needed > cap)
        return DENY_ERR_CAPACITY;

    uint8_t *buf = container->data;

    /* ── Step 1: Fill entire container with random bytes ── */
    /* This is critical: every byte must be random BEFORE writing volumes.
     * After seal, the container is random + encrypted volumes (also random).
     * An adversary cannot distinguish volume regions from padding. */
    if (secure_random(buf, cap) != 0)
        return DENY_ERR_KDF;

    /* ── Step 2: Write salt (first 32 bytes) ── */
    /* Salt is already random from step 1 — this is the public part */
    uint8_t *salt = buf;
    /* (salt stays as-is from the random fill) */

    /* ── Step 3: For each volume, derive key and write encrypted data ── */
    for (int v = 0; v < n_volumes; v++) {
        /* Derive key + nonce + locator from password */
        uint8_t kdf_out[KDF_OUTPUT_LEN];
        derive_volume_params(kdf_out,
                             volumes[v].password, volumes[v].password_len,
                             salt, DENY_KDF_ITERATIONS);

        uint8_t *vol_key   = kdf_out;       /* 32 bytes */
        uint8_t *vol_nonce = kdf_out + 32;   /* 12 bytes */
        uint8_t *locator   = kdf_out + 44;   /* 32 bytes */

        /* Compute volume size and offset */
        size_t vol_size = DENY_VOLUME_OVERHEAD + volumes[v].plaintext_len;
        size_t vol_offset = compute_volume_offset(locator, cap,
                                                  DENY_SALT_BYTES, vol_size);

        /* ── Write volume: [nonce:12][length:4][ciphertext:N][tag:16] ── */
        uint8_t *dst = buf + vol_offset;

        /* Nonce (from KDF, not random — deterministic for reproducibility) */
        memcpy(dst, vol_nonce, DENY_NONCE_BYTES);
        dst += DENY_NONCE_BYTES;

        /* Plaintext length (big-endian, 4 bytes) */
        uint32_t pt_len = (uint32_t)volumes[v].plaintext_len;
        uint8_t len_bytes[4] = {
            (uint8_t)(pt_len >> 24), (uint8_t)(pt_len >> 16),
            (uint8_t)(pt_len >> 8),  (uint8_t)(pt_len)
        };

        /* Encrypt: plaintext = length_prefix || actual_plaintext */
        uint8_t *pt_buf = malloc(4 + pt_len);
        if (!pt_buf) { memset(kdf_out, 0, sizeof(kdf_out)); return DENY_ERR_ENCRYPT; }
        memcpy(pt_buf, len_bytes, 4);
        memcpy(pt_buf + 4, volumes[v].plaintext, pt_len);

        uint8_t tag[DENY_TAG_BYTES];

        /* AAD = salt (binds volume to this container) */
        if (aead_encrypt(dst, tag, pt_buf, 4 + pt_len,
                         salt, DENY_SALT_BYTES,
                         vol_nonce, vol_key) != 0) {
            free(pt_buf);
            memset(kdf_out, 0, sizeof(kdf_out));
            return DENY_ERR_ENCRYPT;
        }

        dst += 4 + pt_len;
        memcpy(dst, tag, DENY_TAG_BYTES);

        free(pt_buf);
        memset(kdf_out, 0, sizeof(kdf_out));
    }

    container->used = cap;
    return DENY_OK;
}

/* ═════════════════════════════════════════════
 * Open: attempt to decrypt a volume
 * ═════════════════════════════════════════════ */

deny_result_t
deny_open(const deny_container_t *container,
          const char *password, size_t pass_len,
          uint8_t *out, size_t out_cap,
          size_t *out_len)
{
    *out_len = 0;
    const uint8_t *buf = container->data;
    size_t cap = container->capacity;
    const uint8_t *salt = buf;

    /* Derive key + nonce + locator */
    uint8_t kdf_out[KDF_OUTPUT_LEN];
    derive_volume_params(kdf_out, password, pass_len,
                         salt, DENY_KDF_ITERATIONS);

    uint8_t *vol_key   = kdf_out;
    uint8_t *vol_nonce = kdf_out + 32;
    uint8_t *locator   = kdf_out + 44;

    /*
     * Try multiple possible volume sizes. Since we don't store the
     * volume size anywhere (that would leak metadata), we try
     * plausible sizes from the computed offset.
     *
     * Strategy: read nonce from the computed offset, then try
     * decrypting increasing payload sizes until auth succeeds or
     * we exceed the container.
     */

    /* Maximum possible volume from this offset */
    size_t max_vol = cap - DENY_SALT_BYTES;

    /* Try the most likely location first */
    for (size_t try_pt_len = 4; try_pt_len <= max_vol && try_pt_len <= out_cap + 64; try_pt_len++) {
        size_t vol_size = DENY_NONCE_BYTES + try_pt_len + DENY_TAG_BYTES;
        size_t vol_offset = compute_volume_offset(locator, cap,
                                                  DENY_SALT_BYTES, vol_size);

        if (vol_offset + vol_size > cap) continue;

        const uint8_t *src = buf + vol_offset;
        const uint8_t *ct_nonce = src;
        const uint8_t *ct = src + DENY_NONCE_BYTES;
        const uint8_t *tag = src + DENY_NONCE_BYTES + try_pt_len;

        /* Verify nonce matches our derived nonce (quick reject) */
        if (memcmp(ct_nonce, vol_nonce, DENY_NONCE_BYTES) != 0)
            continue;

        /* Attempt decryption */
        uint8_t *pt_buf = malloc(try_pt_len);
        if (!pt_buf) continue;

        int ret = aead_decrypt(pt_buf, ct, try_pt_len, tag,
                               salt, DENY_SALT_BYTES,
                               vol_nonce, vol_key);

        if (ret == 0) {
            /* Authentication succeeded! Parse length prefix. */
            uint32_t real_len = ((uint32_t)pt_buf[0] << 24) |
                                ((uint32_t)pt_buf[1] << 16) |
                                ((uint32_t)pt_buf[2] << 8)  |
                                ((uint32_t)pt_buf[3]);

            if (real_len <= try_pt_len - 4 && real_len <= out_cap) {
                memcpy(out, pt_buf + 4, real_len);
                *out_len = real_len;
                free(pt_buf);
                memset(kdf_out, 0, sizeof(kdf_out));
                return DENY_OK;
            }
        }

        free(pt_buf);
    }

    /* ── No volume found — generate honey-encrypted plausible output ── */
    /*
     * This is the deniability guarantee: wrong passwords don't return
     * "error", they return convincing fake data.
     *
     * We use the derived key as a seed for the honey generator.
     * Different wrong passwords → different plausible outputs.
     */
    size_t honey_len = 64 + (kdf_out[0] % 192); /* Variable plausible length */
    if (honey_len > out_cap) honey_len = out_cap;

    honey_generate(out, honey_len, kdf_out, KDF_OUTPUT_LEN, HONEY_DIST_ENGLISH_TEXT);
    *out_len = honey_len;

    memset(kdf_out, 0, sizeof(kdf_out));
    return DENY_ERR_HONEY;
}

/* ═════════════════════════════════════════════
 * Probe (timing-sensitive — use carefully)
 * ═════════════════════════════════════════════ */

int
deny_probe(const deny_container_t *container,
           const char *password, size_t pass_len)
{
    uint8_t tmp[1024];
    size_t tmp_len;
    deny_result_t r = deny_open(container, password, pass_len,
                                tmp, sizeof(tmp), &tmp_len);
    memset(tmp, 0, sizeof(tmp));
    return (r == DENY_OK) ? 1 : 0;
}