Skip to content

tests/test_deniable.c

/*
 * QR-NSP Volcanic Edition — Module 5 Test: Deniable Encryption
 * Multi-password hidden volumes with honey-encryption.
 *
 * Build:
 *   gcc -O2 -march=native -I include -o test_deniable \
 *       tests/test_deniable.c src/deniable/deniable.c \
 *       src/aead/aes256gcm.c src/crypto/symmetric.c
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#include "qrnsp_deniable.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

/* ─────────────────────────────────────────────
 * Helpers
 * ───────────────────────────────────────────── */

static void
hexdump_short(const char *label, const uint8_t *data, size_t len)
{
    printf("  %s [%zu]: ", label, len);
    size_t show = (len > 40) ? 40 : len;
    for (size_t i = 0; i < show; i++) printf("%02x", data[i]);
    if (len > 40) printf("...");
    printf("\n");
}

static void
print_text(const char *label, const uint8_t *data, size_t len)
{
    printf("  %s [%zu]: \"", label, len);
    size_t show = (len > 60) ? 60 : len;
    for (size_t i = 0; i < show; i++) {
        char c = (char)data[i];
        if (c >= 32 && c < 127) putchar(c);
        else putchar('.');
    }
    if (len > 60) printf("...");
    printf("\"\n");
}

/* ─────────────────────────────────────────────
 * Test 1: Single volume seal + open
 * ───────────────────────────────────────────── */

static int
test_single_volume(void)
{
    printf("[test] Single volume seal/open... ");

    const char *password = "resistance2026";
    const char *secret = "Meeting at dock 7, midnight. Bring the documents.";

    deny_volume_t vol = {
        .password     = password,
        .password_len = strlen(password),
        .plaintext    = (const uint8_t *)secret,
        .plaintext_len = strlen(secret),
        .honey_type   = HONEY_DIST_ENGLISH_TEXT,
    };

    /* Compute required size and allocate */
    size_t req = deny_required_size(&vol, 1);
    uint8_t *buf = calloc(1, req);
    deny_container_t container = { .data = buf, .capacity = req };

    /* Seal */
    clock_t t0 = clock();
    deny_result_t res = deny_seal(&vol, 1, &container);
    clock_t t1 = clock();

    if (res != DENY_OK) {
        printf("FAIL (seal: %d)\n", res);
        free(buf);
        return -1;
    }
    printf("\n    seal: %.2fs, container: %zu bytes\n    ",
           (double)(t1 - t0) / CLOCKS_PER_SEC, req);

    /* Open with correct password */
    uint8_t out[1024];
    size_t out_len;

    clock_t t2 = clock();
    res = deny_open(&container, password, strlen(password),
                    out, sizeof(out), &out_len);
    clock_t t3 = clock();

    if (res != DENY_OK) {
        printf("FAIL (open: %d)\n", res);
        free(buf);
        return -1;
    }

    if (out_len != strlen(secret) || memcmp(out, secret, out_len) != 0) {
        printf("FAIL (mismatch)\n");
        print_text("expected", (const uint8_t *)secret, strlen(secret));
        print_text("got     ", out, out_len);
        free(buf);
        return -1;
    }

    printf("OK (open: %.2fs)\n", (double)(t3 - t2) / CLOCKS_PER_SEC);
    print_text("decrypted", out, out_len);
    free(buf);
    return 0;
}

/* ─────────────────────────────────────────────
 * Test 2: Two volumes (decoy + real)
 * ───────────────────────────────────────────── */

static int
test_dual_volume(void)
{
    printf("[test] Dual volume (decoy + real)... ");

    const char *decoy_pass = "shopping_list";
    const char *decoy_data = "Milk, eggs, bread, butter, cheese";

    const char *real_pass = "fr33d0m_n0w!";
    const char *real_data = "Agent contacts: Alpha=48.2N,16.4E Bravo=51.5N,0.1W";

    deny_volume_t vols[2] = {
        {
            .password = decoy_pass, .password_len = strlen(decoy_pass),
            .plaintext = (const uint8_t *)decoy_data,
            .plaintext_len = strlen(decoy_data),
            .honey_type = HONEY_DIST_ENGLISH_TEXT,
        },
        {
            .password = real_pass, .password_len = strlen(real_pass),
            .plaintext = (const uint8_t *)real_data,
            .plaintext_len = strlen(real_data),
            .honey_type = HONEY_DIST_ENGLISH_TEXT,
        },
    };

    size_t req = deny_required_size(vols, 2);
    uint8_t *buf = calloc(1, req);
    deny_container_t container = { .data = buf, .capacity = req };

    deny_result_t res = deny_seal(vols, 2, &container);
    if (res != DENY_OK) {
        printf("FAIL (seal: %d)\n", res);
        free(buf);
        return -1;
    }

    printf("\n    container: %zu bytes\n", req);

    /* Open with DECOY password (what you show the adversary) */
    uint8_t out[1024];
    size_t out_len;

    res = deny_open(&container, decoy_pass, strlen(decoy_pass),
                    out, sizeof(out), &out_len);
    if (res == DENY_OK && out_len == strlen(decoy_data) &&
        memcmp(out, decoy_data, out_len) == 0) {
        print_text("    decoy", out, out_len);
    } else {
        printf("    WARN: decoy open returned %d (may overlap with real)\n", res);
    }

    /* Open with REAL password (the secret payload) */
    res = deny_open(&container, real_pass, strlen(real_pass),
                    out, sizeof(out), &out_len);
    if (res == DENY_OK && out_len == strlen(real_data) &&
        memcmp(out, real_data, out_len) == 0) {
        print_text("    real ", out, out_len);
        printf("    OK\n");
        free(buf);
        return 0;
    }

    printf("    FAIL (real volume: res=%d, len=%zu)\n", res, out_len);
    free(buf);
    return -1;
}

/* ─────────────────────────────────────────────
 * Test 3: Wrong password → honey-encrypted output
 * ───────────────────────────────────────────── */

static int
test_honey_encryption(void)
{
    printf("[test] Honey-encryption (wrong password)... ");

    const char *real_pass = "correct_horse_battery";
    const char *secret = "Nuclear codes: 00000000";

    deny_volume_t vol = {
        .password = real_pass, .password_len = strlen(real_pass),
        .plaintext = (const uint8_t *)secret,
        .plaintext_len = strlen(secret),
        .honey_type = HONEY_DIST_ENGLISH_TEXT,
    };

    size_t req = deny_required_size(&vol, 1);
    uint8_t *buf = calloc(1, req);
    deny_container_t container = { .data = buf, .capacity = req };
    deny_seal(&vol, 1, &container);

    /* Try 3 wrong passwords — each should return plausible-looking text */
    const char *wrong[] = {"wrong_pass_1", "password123", "letmein"};
    int ok = 1;

    printf("\n");
    for (int i = 0; i < 3; i++) {
        uint8_t out[1024];
        size_t out_len;
        deny_result_t res = deny_open(&container, wrong[i], strlen(wrong[i]),
                                      out, sizeof(out), &out_len);

        if (res != DENY_ERR_HONEY) {
            printf("    FAIL: wrong pass '%s' returned %d (expected HONEY)\n",
                   wrong[i], res);
            ok = 0;
            continue;
        }

        printf("    pass='%s' → honey ", wrong[i]);
        print_text("", out, out_len);

        /* Verify output is non-empty and looks like text */
        if (out_len == 0) {
            printf("    FAIL: empty honey output\n");
            ok = 0;
        }
    }

    if (ok) printf("    OK (3/3 wrong passwords produced plausible honey text)\n");

    free(buf);
    return ok ? 0 : -1;
}

/* ─────────────────────────────────────────────
 * Test 4: Container is uniformly random-looking
 * ───────────────────────────────────────────── */

static int
test_container_randomness(void)
{
    printf("[test] Container byte distribution (χ² test)... ");

    const char *pass = "entropy_test";
    const char *data = "This is secret data that should be invisible";

    deny_volume_t vol = {
        .password = pass, .password_len = strlen(pass),
        .plaintext = (const uint8_t *)data,
        .plaintext_len = strlen(data),
        .honey_type = HONEY_DIST_BINARY,
    };

    size_t req = DENY_CONTAINER_64K;
    uint8_t *buf = calloc(1, req);
    deny_container_t container = { .data = buf, .capacity = req };
    deny_seal(&vol, 1, &container);

    /* Byte frequency analysis (skip salt) */
    size_t freq[256] = {0};
    for (size_t i = DENY_SALT_BYTES; i < req; i++)
        freq[buf[i]]++;

    /* χ² test: compare against uniform distribution */
    double expected = (double)(req - DENY_SALT_BYTES) / 256.0;
    double chi2 = 0.0;
    for (int i = 0; i < 256; i++) {
        double diff = (double)freq[i] - expected;
        chi2 += (diff * diff) / expected;
    }

    /* χ²(255) at p=0.01 ≈ 310.5. Good randomness → χ² ≈ 255 ± 23 */
    printf("χ²=%.1f (expected ~255, reject if >310)\n", chi2);

    if (chi2 > 350.0) {
        printf("    WARN: Container may not look sufficiently random\n");
        /* Not a hard fail — χ² is probabilistic */
    }

    free(buf);
    return 0;
}

/* ─────────────────────────────────────────────
 * Test 5: Different honey outputs for different wrong passwords
 * ───────────────────────────────────────────── */

static int
test_honey_diversity(void)
{
    printf("[test] Honey output diversity... ");

    const char *pass = "real_deal";
    const char *data = "secret";

    deny_volume_t vol = {
        .password = pass, .password_len = strlen(pass),
        .plaintext = (const uint8_t *)data,
        .plaintext_len = strlen(data),
        .honey_type = HONEY_DIST_ENGLISH_TEXT,
    };

    size_t req = deny_required_size(&vol, 1);
    uint8_t *buf = calloc(1, req);
    deny_container_t container = { .data = buf, .capacity = req };
    deny_seal(&vol, 1, &container);

    /* Generate honey outputs for 5 wrong passwords */
    uint8_t honey[5][256];
    size_t honey_lens[5];
    const char *wrong[] = {"aaa","bbb","ccc","ddd","eee"};

    for (int i = 0; i < 5; i++) {
        deny_open(&container, wrong[i], strlen(wrong[i]),
                  honey[i], sizeof(honey[i]), &honey_lens[i]);
    }

    /* Verify all outputs are different */
    int all_different = 1;
    for (int i = 0; i < 5; i++) {
        for (int j = i + 1; j < 5; j++) {
            if (honey_lens[i] == honey_lens[j] &&
                memcmp(honey[i], honey[j], honey_lens[i]) == 0) {
                printf("FAIL (identical honey for '%s' and '%s')\n",
                       wrong[i], wrong[j]);
                all_different = 0;
            }
        }
    }

    if (all_different)
        printf("OK (5 unique honey outputs)\n");

    free(buf);
    return all_different ? 0 : -1;
}

/* ─────────────────────────────────────────────
 * Test 6: Performance benchmark (KDF cost)
 * ───────────────────────────────────────────── */

static int
test_kdf_performance(void)
{
    printf("[bench] KDF performance (seal + open):\n");

    const char *pass = "benchmark_password";
    const char *data = "benchmark payload data for timing";

    deny_volume_t vol = {
        .password = pass, .password_len = strlen(pass),
        .plaintext = (const uint8_t *)data,
        .plaintext_len = strlen(data),
        .honey_type = HONEY_DIST_BINARY,
    };

    size_t req = deny_required_size(&vol, 1);
    uint8_t *buf = calloc(1, req);
    deny_container_t container = { .data = buf, .capacity = req };

    /* Seal timing */
    clock_t t0 = clock();
    deny_seal(&vol, 1, &container);
    clock_t t1 = clock();

    /* Open timing */
    uint8_t out[1024];
    size_t out_len;
    clock_t t2 = clock();
    deny_open(&container, pass, strlen(pass), out, sizeof(out), &out_len);
    clock_t t3 = clock();

    printf("    seal: %.3fs  open: %.3fs  (iterations=%d)\n",
           (double)(t1 - t0) / CLOCKS_PER_SEC,
           (double)(t3 - t2) / CLOCKS_PER_SEC,
           DENY_KDF_ITERATIONS);

    free(buf);
    return 0;
}

/* ─────────────────────────────────────────────
 * Main
 * ───────────────────────────────────────────── */

int
main(void)
{
    printf("╔══════════════════════════════════════════════╗\n");
    printf("║  QR-NSP Module 5: Deniable Encryption Test  ║\n");
    printf("║  Multi-Password KDF + Honey-Encryption       ║\n");
    printf("╚══════════════════════════════════════════════╝\n\n");

    int failures = 0;

    failures += (test_single_volume() != 0);
    failures += (test_dual_volume() != 0);
    failures += (test_honey_encryption() != 0);
    failures += (test_container_randomness() != 0);
    failures += (test_honey_diversity() != 0);
    test_kdf_performance();

    printf("\n%s: %d test(s) failed\n",
           failures ? "FAILED" : "ALL PASSED", failures);

    return failures;
}