From 13ad24b676e307be79fac71bae7089c66cc65b23 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Mon, 13 Apr 2026 16:01:35 -0400 Subject: [PATCH] Add Poly1305 and ChaCha20-Poly1305 AEAD with RFC 8439 tests --- build.sh | 2 + nostr_core/crypto/nostr_chacha20poly1305.c | 192 +++++++++++++++ nostr_core/crypto/nostr_poly1305.c | 262 +++++++++++++++++++++ tests/chacha20poly1305_test.c | 243 +++++++++++++++++++ 4 files changed, 699 insertions(+) create mode 100644 nostr_core/crypto/nostr_chacha20poly1305.c create mode 100644 nostr_core/crypto/nostr_poly1305.c create mode 100644 tests/chacha20poly1305_test.c diff --git a/build.sh b/build.sh index 4d901d9b..ba1fce2d 100755 --- a/build.sh +++ b/build.sh @@ -500,6 +500,8 @@ detect_system_curl SOURCES="nostr_core/crypto/nostr_secp256k1.c" SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c" SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c" +SOURCES="$SOURCES nostr_core/crypto/nostr_poly1305.c" +SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20poly1305.c" SOURCES="$SOURCES cjson/cJSON.c" SOURCES="$SOURCES nostr_core/utils.c" SOURCES="$SOURCES nostr_core/nostr_common.c" diff --git a/nostr_core/crypto/nostr_chacha20poly1305.c b/nostr_core/crypto/nostr_chacha20poly1305.c new file mode 100644 index 00000000..95516b2c --- /dev/null +++ b/nostr_core/crypto/nostr_chacha20poly1305.c @@ -0,0 +1,192 @@ +/* + * nostr_chacha20poly1305.c - ChaCha20-Poly1305 AEAD implementation + * + * RFC 8439 Section 2.8 + */ + +#include +#include +#include +#include + +// ChaCha20 private API (provided by nostr_chacha20.c) +int chacha20_block(const uint8_t key[32], uint32_t counter, + const uint8_t nonce[12], uint8_t output[64]); +int chacha20_encrypt(const uint8_t key[32], uint32_t counter, + const uint8_t nonce[12], const uint8_t* input, + uint8_t* output, size_t length); + +// Poly1305 API (provided by nostr_poly1305.c) +int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg, + size_t msg_len, unsigned char tag[16]); + +static void store64_le(unsigned char out[8], uint64_t v) { + out[0] = (unsigned char)(v & 0xff); + out[1] = (unsigned char)((v >> 8) & 0xff); + out[2] = (unsigned char)((v >> 16) & 0xff); + out[3] = (unsigned char)((v >> 24) & 0xff); + out[4] = (unsigned char)((v >> 32) & 0xff); + out[5] = (unsigned char)((v >> 40) & 0xff); + out[6] = (unsigned char)((v >> 48) & 0xff); + out[7] = (unsigned char)((v >> 56) & 0xff); +} + +static int constant_time_tag_eq(const unsigned char a[16], const unsigned char b[16]) { + unsigned char diff = 0; + size_t i; + for (i = 0; i < 16; i++) { + diff |= (unsigned char)(a[i] ^ b[i]); + } + return diff == 0; +} + +static int build_poly1305_input(const unsigned char *aad, size_t aad_len, + const unsigned char *ciphertext, size_t ct_len, + unsigned char **out, size_t *out_len) { + size_t aad_pad = (16 - (aad_len % 16)) % 16; + size_t ct_pad = (16 - (ct_len % 16)) % 16; + size_t total = aad_len + aad_pad + ct_len + ct_pad + 16; // len(aad)||len(ct) + unsigned char *buf; + size_t pos = 0; + unsigned char len_block[16]; + + if (!out || !out_len) { + return -1; + } + + buf = (unsigned char *)malloc(total); + if (!buf) { + return -1; + } + + if (aad_len > 0 && aad) { + memcpy(buf + pos, aad, aad_len); + pos += aad_len; + } + if (aad_pad > 0) { + memset(buf + pos, 0, aad_pad); + pos += aad_pad; + } + + if (ct_len > 0 && ciphertext) { + memcpy(buf + pos, ciphertext, ct_len); + pos += ct_len; + } + if (ct_pad > 0) { + memset(buf + pos, 0, ct_pad); + pos += ct_pad; + } + + store64_le(len_block, (uint64_t)aad_len); + store64_le(len_block + 8, (uint64_t)ct_len); + memcpy(buf + pos, len_block, sizeof(len_block)); + pos += sizeof(len_block); + + *out = buf; + *out_len = pos; + return 0; +} + +int nostr_chacha20poly1305_encrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *plaintext, size_t pt_len, + unsigned char *ciphertext, + unsigned char tag[16]) { + unsigned char block0[64]; + unsigned char poly_key[32]; + unsigned char *poly_in = NULL; + size_t poly_in_len = 0; + int rc; + + if (!key || !nonce || !ciphertext || !tag || (!plaintext && pt_len != 0) || (!aad && aad_len != 0)) { + return -1; + } + + if (chacha20_block(key, 0, nonce, block0) != 0) { + return -1; + } + memcpy(poly_key, block0, sizeof(poly_key)); + + if (pt_len > 0) { + rc = chacha20_encrypt(key, 1, nonce, plaintext, ciphertext, pt_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + return -1; + } + } + + rc = build_poly1305_input(aad, aad_len, ciphertext, pt_len, &poly_in, &poly_in_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + return -1; + } + + rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, tag); + + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + + return rc; +} + +int nostr_chacha20poly1305_decrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *ciphertext, size_t ct_len, + const unsigned char tag[16], + unsigned char *plaintext) { + unsigned char block0[64]; + unsigned char poly_key[32]; + unsigned char expected_tag[16]; + unsigned char *poly_in = NULL; + size_t poly_in_len = 0; + int rc; + + if (!key || !nonce || !tag || !plaintext || (!ciphertext && ct_len != 0) || (!aad && aad_len != 0)) { + return -1; + } + + if (chacha20_block(key, 0, nonce, block0) != 0) { + return -1; + } + memcpy(poly_key, block0, sizeof(poly_key)); + + rc = build_poly1305_input(aad, aad_len, ciphertext, ct_len, &poly_in, &poly_in_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + return -1; + } + + rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, expected_tag); + if (rc != 0 || !constant_time_tag_eq(tag, expected_tag)) { + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(expected_tag, 0, sizeof(expected_tag)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + return -1; + } + + if (ct_len > 0) { + rc = chacha20_encrypt(key, 1, nonce, ciphertext, plaintext, ct_len); + if (rc != 0) { + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(expected_tag, 0, sizeof(expected_tag)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + return -1; + } + } + + memset(poly_key, 0, sizeof(poly_key)); + memset(block0, 0, sizeof(block0)); + memset(expected_tag, 0, sizeof(expected_tag)); + memset(poly_in, 0, poly_in_len); + free(poly_in); + + return 0; +} diff --git a/nostr_core/crypto/nostr_poly1305.c b/nostr_core/crypto/nostr_poly1305.c new file mode 100644 index 00000000..2fe385c2 --- /dev/null +++ b/nostr_core/crypto/nostr_poly1305.c @@ -0,0 +1,262 @@ +/* + * nostr_poly1305.c - Poly1305 message authentication code + * + * Public-domain style 32-bit implementation based on the poly1305-donna + * approach, adapted for nostr_core_lib naming and conventions. + * + * RFC 8439 Section 2.5 + */ + +#include +#include +#include + +typedef struct { + uint32_t r0, r1, r2, r3, r4; + uint32_t s1, s2, s3, s4; + uint32_t h0, h1, h2, h3, h4; + uint32_t pad0, pad1, pad2, pad3; + size_t leftover; + unsigned char buffer[16]; + unsigned char final; +} nostr_poly1305_ctx_t; + +static uint32_t u8to32_le(const unsigned char *p) { + return ((uint32_t)p[0]) | + ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void u32to8_le(unsigned char *p, uint32_t v) { + p[0] = (unsigned char)(v & 0xff); + p[1] = (unsigned char)((v >> 8) & 0xff); + p[2] = (unsigned char)((v >> 16) & 0xff); + p[3] = (unsigned char)((v >> 24) & 0xff); +} + +static void poly1305_blocks(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) { + const uint32_t r0 = st->r0; + const uint32_t r1 = st->r1; + const uint32_t r2 = st->r2; + const uint32_t r3 = st->r3; + const uint32_t r4 = st->r4; + const uint32_t s1 = st->s1; + const uint32_t s2 = st->s2; + const uint32_t s3 = st->s3; + const uint32_t s4 = st->s4; + uint32_t h0 = st->h0; + uint32_t h1 = st->h1; + uint32_t h2 = st->h2; + uint32_t h3 = st->h3; + uint32_t h4 = st->h4; + const uint32_t hibit = st->final ? 0 : (1U << 24); + + while (bytes >= 16) { + uint32_t t0 = u8to32_le(m + 0); + uint32_t t1 = u8to32_le(m + 4); + uint32_t t2 = u8to32_le(m + 8); + uint32_t t3 = u8to32_le(m + 12); + + uint64_t d0, d1, d2, d3, d4; + uint32_t c; + + h0 += ( t0 ) & 0x3ffffff; + h1 += (((t1 << 6) | (t0 >> 26)) ) & 0x3ffffff; + h2 += (((t2 << 12) | (t1 >> 20))) & 0x3ffffff; + h3 += (((t3 << 18) | (t2 >> 14))) & 0x3ffffff; + h4 += (( t3 >> 8) ) & 0x00ffffff; + h4 += hibit; + + d0 = ((uint64_t)h0 * r0) + ((uint64_t)h1 * s4) + ((uint64_t)h2 * s3) + ((uint64_t)h3 * s2) + ((uint64_t)h4 * s1); + d1 = ((uint64_t)h0 * r1) + ((uint64_t)h1 * r0) + ((uint64_t)h2 * s4) + ((uint64_t)h3 * s3) + ((uint64_t)h4 * s2); + d2 = ((uint64_t)h0 * r2) + ((uint64_t)h1 * r1) + ((uint64_t)h2 * r0) + ((uint64_t)h3 * s4) + ((uint64_t)h4 * s3); + d3 = ((uint64_t)h0 * r3) + ((uint64_t)h1 * r2) + ((uint64_t)h2 * r1) + ((uint64_t)h3 * r0) + ((uint64_t)h4 * s4); + d4 = ((uint64_t)h0 * r4) + ((uint64_t)h1 * r3) + ((uint64_t)h2 * r2) + ((uint64_t)h3 * r1) + ((uint64_t)h4 * r0); + + c = (uint32_t)(d0 >> 26); h0 = (uint32_t)d0 & 0x3ffffff; + d1 += c; + c = (uint32_t)(d1 >> 26); h1 = (uint32_t)d1 & 0x3ffffff; + d2 += c; + c = (uint32_t)(d2 >> 26); h2 = (uint32_t)d2 & 0x3ffffff; + d3 += c; + c = (uint32_t)(d3 >> 26); h3 = (uint32_t)d3 & 0x3ffffff; + d4 += c; + c = (uint32_t)(d4 >> 26); h4 = (uint32_t)d4 & 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; h0 &= 0x3ffffff; + h1 += c; + + m += 16; + bytes -= 16; + } + + st->h0 = h0; + st->h1 = h1; + st->h2 = h2; + st->h3 = h3; + st->h4 = h4; +} + +void nostr_poly1305_init(nostr_poly1305_ctx_t *st, const unsigned char key[32]) { + uint32_t t0, t1, t2, t3; + + t0 = u8to32_le(key + 0); + t1 = u8to32_le(key + 4); + t2 = u8to32_le(key + 8); + t3 = u8to32_le(key + 12); + + st->r0 = ( t0 ) & 0x3ffffff; + st->r1 = (((t1 << 6) | (t0 >> 26)) ) & 0x3ffff03; + st->r2 = (((t2 << 12) | (t1 >> 20))) & 0x3ffc0ff; + st->r3 = (((t3 << 18) | (t2 >> 14))) & 0x3f03fff; + st->r4 = (( t3 >> 8) ) & 0x00fffff; + + st->s1 = st->r1 * 5; + st->s2 = st->r2 * 5; + st->s3 = st->r3 * 5; + st->s4 = st->r4 * 5; + + st->h0 = 0; + st->h1 = 0; + st->h2 = 0; + st->h3 = 0; + st->h4 = 0; + + st->pad0 = u8to32_le(key + 16); + st->pad1 = u8to32_le(key + 20); + st->pad2 = u8to32_le(key + 24); + st->pad3 = u8to32_le(key + 28); + + st->leftover = 0; + st->final = 0; +} + +void nostr_poly1305_update(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) { + size_t i; + + if (st->leftover) { + size_t want = (size_t)(16 - st->leftover); + if (want > bytes) { + want = bytes; + } + for (i = 0; i < want; i++) { + st->buffer[st->leftover + i] = m[i]; + } + bytes -= want; + m += want; + st->leftover += want; + if (st->leftover < 16) { + return; + } + poly1305_blocks(st, st->buffer, 16); + st->leftover = 0; + } + + if (bytes >= 16) { + size_t want = bytes & ~(size_t)0xf; + poly1305_blocks(st, m, want); + m += want; + bytes -= want; + } + + if (bytes) { + for (i = 0; i < bytes; i++) { + st->buffer[st->leftover + i] = m[i]; + } + st->leftover += bytes; + } +} + +void nostr_poly1305_final(nostr_poly1305_ctx_t *st, unsigned char tag[16]) { + uint32_t h0, h1, h2, h3, h4, c; + uint32_t g0, g1, g2, g3, g4; + uint64_t f; + uint32_t mask; + + if (st->leftover) { + size_t i = st->leftover; + st->buffer[i++] = 1; + for (; i < 16; i++) { + st->buffer[i] = 0; + } + st->final = 1; + poly1305_blocks(st, st->buffer, 16); + } + + h0 = st->h0; + h1 = st->h1; + h2 = st->h2; + h3 = st->h3; + h4 = st->h4; + + c = h1 >> 26; h1 &= 0x3ffffff; + h2 += c; + c = h2 >> 26; h2 &= 0x3ffffff; + h3 += c; + c = h3 >> 26; h3 &= 0x3ffffff; + h4 += c; + c = h4 >> 26; h4 &= 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; h0 &= 0x3ffffff; + h1 += c; + + g0 = h0 + 5; + c = g0 >> 26; g0 &= 0x3ffffff; + g1 = h1 + c; + c = g1 >> 26; g1 &= 0x3ffffff; + g2 = h2 + c; + c = g2 >> 26; g2 &= 0x3ffffff; + g3 = h3 + c; + c = g3 >> 26; g3 &= 0x3ffffff; + g4 = h4 + c - (1U << 26); + + mask = (g4 >> 31) - 1U; + g0 &= mask; + g1 &= mask; + g2 &= mask; + g3 &= mask; + g4 &= mask; + mask = ~mask; + h0 = (h0 & mask) | g0; + h1 = (h1 & mask) | g1; + h2 = (h2 & mask) | g2; + h3 = (h3 & mask) | g3; + h4 = (h4 & mask) | g4; + + h0 = ((h0 ) | (h1 << 26)) & 0xffffffff; + h1 = ((h1 >> 6 ) | (h2 << 20)) & 0xffffffff; + h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; + h3 = ((h3 >> 18) | (h4 << 8 )) & 0xffffffff; + + f = (uint64_t)h0 + st->pad0; + h0 = (uint32_t)f; + f = (uint64_t)h1 + st->pad1 + (f >> 32); + h1 = (uint32_t)f; + f = (uint64_t)h2 + st->pad2 + (f >> 32); + h2 = (uint32_t)f; + f = (uint64_t)h3 + st->pad3 + (f >> 32); + h3 = (uint32_t)f; + + u32to8_le(tag + 0, h0); + u32to8_le(tag + 4, h1); + u32to8_le(tag + 8, h2); + u32to8_le(tag + 12, h3); + + memset(st, 0, sizeof(*st)); +} + +int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg, + size_t msg_len, unsigned char tag[16]) { + nostr_poly1305_ctx_t ctx; + + if (!key || (!msg && msg_len != 0) || !tag) { + return -1; + } + + nostr_poly1305_init(&ctx, key); + nostr_poly1305_update(&ctx, msg, msg_len); + nostr_poly1305_final(&ctx, tag); + + return 0; +} diff --git a/tests/chacha20poly1305_test.c b/tests/chacha20poly1305_test.c new file mode 100644 index 00000000..ea4cb869 --- /dev/null +++ b/tests/chacha20poly1305_test.c @@ -0,0 +1,243 @@ +/* + * ChaCha20-Poly1305 / Poly1305 Test Suite - RFC 8439 vectors + */ + +#include +#include +#include +#include + +int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg, + size_t msg_len, unsigned char tag[16]); + +int nostr_chacha20poly1305_encrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *plaintext, size_t pt_len, + unsigned char *ciphertext, + unsigned char tag[16]); + +int nostr_chacha20poly1305_decrypt(const unsigned char key[32], + const unsigned char nonce[12], + const unsigned char *aad, size_t aad_len, + const unsigned char *ciphertext, size_t ct_len, + const unsigned char tag[16], + unsigned char *plaintext); + +static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) { + size_t i; + if (!hex || !out) return -1; + for (i = 0; i < out_len; i++) { + if (sscanf(hex + (i * 2), "%2hhx", &out[i]) != 1) { + return -1; + } + } + return 0; +} + +static int bytes_equal(const uint8_t *a, const uint8_t *b, size_t len) { + return memcmp(a, b, len) == 0; +} + +static void print_hex(const uint8_t *buf, size_t len) { + size_t i; + for (i = 0; i < len; i++) { + printf("%02x", buf[i]); + } +} + +static int test_poly1305_rfc8439_2_5_2(void) { + const char *key_hex = + "85d6be7857556d337f4452fe42d506a8" + "0103808afb0db2fd4abff6af4149f51b"; + const char *msg = "Cryptographic Forum Research Group"; + const char *expected_tag_hex = "a8061dc1305136c6c22b8baf0c0127a9"; + + uint8_t key[32]; + uint8_t tag[16]; + uint8_t expected[16]; + + printf("=== Poly1305 RFC 8439 §2.5.2 ===\n"); + + if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 || + hex_to_bytes(expected_tag_hex, expected, sizeof(expected)) != 0) { + printf("❌ hex parse failed\n\n"); + return 0; + } + + if (nostr_poly1305_mac(key, (const unsigned char *)msg, strlen(msg), tag) != 0) { + printf("❌ nostr_poly1305_mac failed\n\n"); + return 0; + } + + if (!bytes_equal(tag, expected, sizeof(tag))) { + printf("❌ tag mismatch\nExpected: "); + print_hex(expected, sizeof(expected)); + printf("\nGot: "); + print_hex(tag, sizeof(tag)); + printf("\n\n"); + return 0; + } + + printf("✅ passed\n\n"); + return 1; +} + +static int test_aead_rfc8439_appendix_a_5(void) { + const char *key_hex = + "1c9240a5eb55d38af333888604f6b5f0" + "473917c1402b80099dca5cbc207075c0"; + const char *nonce_hex = "000000000102030405060708"; + const char *aad_hex = "f33388860000000000004e91"; + const char *plaintext_hex = + "496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420" + "666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d61792062652075706461" + "7465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + "6e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f20757365" + "20496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f722074" + "6f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f6772" + "6573732e2fe2809d"; + const char *expected_ct_hex = + "64a0861575861af460f062c79be643bd" + "5e805cfd345cf389f108670ac76c8cb2" + "4c6cfc18755d43eea09ee94e382d26b0" + "bdb7b73c321b0100d4f03b7f355894cf" + "332f830e710b97ce98c8a84abd0b9481" + "14ad176e008d33bd60f982b1ff37c855" + "9797a06ef4f0ef61c186324e2b350638" + "3606907b6a7c02b0f9f6157b53c867e4" + "b9166c767b804d46a59b5216cde7a4e9" + "9040c5a40433225ee282a1b0a06c523e" + "af4534d7f83fa1155b0047718cbc546a" + "0d072b04b3564eea1b422273f548271a" + "0bb2316053fa76991955ebd63159434e" + "cebb4e466dae5a1073a6727627097a10" + "49e617d91d361094fa68f0ff77987130" + "305beaba2eda04df997b714d6c6f2c29" + "a6ad5cb4022b02709b"; + const char *expected_tag_hex = "eead9d67890cbb22392336fea1851f38"; + + uint8_t key[32], nonce[12], aad[12], expected_tag[16]; + uint8_t plaintext[1024], ciphertext[1024], decrypted[1024], expected_ct[1024]; + uint8_t tag[16]; + size_t pt_len = strlen(plaintext_hex) / 2; + size_t aad_len = strlen(aad_hex) / 2; + size_t ct_len = strlen(expected_ct_hex) / 2; + + printf("=== ChaCha20-Poly1305 RFC 8439 Appendix A.5 ===\n"); + + if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 || + hex_to_bytes(nonce_hex, nonce, sizeof(nonce)) != 0 || + hex_to_bytes(aad_hex, aad, aad_len) != 0 || + hex_to_bytes(plaintext_hex, plaintext, pt_len) != 0 || + hex_to_bytes(expected_ct_hex, expected_ct, ct_len) != 0 || + hex_to_bytes(expected_tag_hex, expected_tag, sizeof(expected_tag)) != 0) { + printf("❌ hex parse failed\n\n"); + return 0; + } + + if (nostr_chacha20poly1305_encrypt(key, nonce, aad, aad_len, + plaintext, pt_len, ciphertext, tag) != 0) { + printf("❌ encrypt failed\n\n"); + return 0; + } + + if (!bytes_equal(ciphertext, expected_ct, ct_len)) { + size_t i; + size_t mismatch_count = 0; + printf("❌ ciphertext mismatch\n"); + for (i = 0; i < ct_len; i++) { + if (ciphertext[i] != expected_ct[i]) { + if (mismatch_count == 0) { + size_t start = (i > 8) ? (i - 8) : 0; + size_t end = (i + 8 < ct_len) ? (i + 8) : (ct_len - 1); + size_t j; + printf("first mismatch at byte %zu: got=%02x expected=%02x\n", i, ciphertext[i], expected_ct[i]); + printf("context (got): "); + for (j = start; j <= end; j++) printf("%02x", ciphertext[j]); + printf("\ncontext (expected): "); + for (j = start; j <= end; j++) printf("%02x", expected_ct[j]); + printf("\n"); + } + mismatch_count++; + } + } + printf("total mismatched bytes: %zu\n\n", mismatch_count); + return 0; + } + + if (!bytes_equal(tag, expected_tag, sizeof(tag))) { + printf("❌ tag mismatch\n\n"); + return 0; + } + + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, aad_len, + ciphertext, ct_len, tag, decrypted) != 0) { + printf("❌ decrypt failed\n\n"); + return 0; + } + + if (!bytes_equal(decrypted, plaintext, pt_len)) { + printf("❌ decrypted plaintext mismatch\n\n"); + return 0; + } + + printf("✅ passed\n\n"); + return 1; +} + +static int test_aead_tamper_rejects(void) { + uint8_t key[32] = {0}; + uint8_t nonce[12] = {0}; + uint8_t aad[8] = {1,2,3,4,5,6,7,8}; + uint8_t pt[32] = "hello chacha20-poly1305 world"; + uint8_t ct[64] = {0}; + uint8_t tag[16] = {0}; + uint8_t out[64] = {0}; + + printf("=== AEAD tamper rejection ===\n"); + + if (nostr_chacha20poly1305_encrypt(key, nonce, aad, sizeof(aad), pt, strlen((char *)pt), ct, tag) != 0) { + printf("❌ encrypt failed\n\n"); + return 0; + } + + ct[0] ^= 0x01; + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) { + printf("❌ tampered ciphertext accepted\n\n"); + return 0; + } + ct[0] ^= 0x01; + + tag[0] ^= 0x80; + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) { + printf("❌ tampered tag accepted\n\n"); + return 0; + } + tag[0] ^= 0x80; + + aad[0] ^= 0x01; + if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) { + printf("❌ tampered AAD accepted\n\n"); + return 0; + } + + printf("✅ passed\n\n"); + return 1; +} + +int main(void) { + int pass = 1; + + pass &= test_poly1305_rfc8439_2_5_2(); + pass &= test_aead_rfc8439_appendix_a_5(); + pass &= test_aead_tamper_rejects(); + + if (pass) { + printf("🎉 All ChaCha20-Poly1305/Poly1305 tests PASSED\n"); + return 0; + } + + printf("💥 Some tests FAILED\n"); + return 1; +}