Files
sovereign_browser/tests/test_bookmarks_tree.c

145 lines
6.1 KiB
C

/*
* test_bookmarks_tree.c — unit tests for the bookmark tree/HMAC d-tag logic
*
* Tests the pure pieces that don't require a signer, database, or relay
* connection:
* - HMAC-SHA256 d-tag derivation is deterministic (same key + path → same hex)
* - HMAC-SHA256 d-tag derivation is opaque (different paths → uncorrelated)
* - is_hmac_d_tag correctly distinguishes 64-hex HMAC tags from legacy
* plaintext directory names
*
* The full trie operations (add/remove/move/rename/delete) require a signer
* and database, so they are exercised via the browser's MCP tools at runtime
* rather than in this unit test.
*
* Build (standalone, links against nostr_core_lib):
* cc -std=c99 -Wall -Wextra -g -I./nostr_core_lib \
* tests/test_bookmarks_tree.c \
* nostr_core_lib/libnostr_core_x64.a \
* -lsecp256k1 -lssl -lcrypto -lm -o tests/test_bookmarks_tree
* ./tests/test_bookmarks_tree
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core_lib/nostr_core/nostr_core.h"
static int failures = 0;
static int passes = 0;
#define CHECK(cond, msg) do { \
if (cond) { passes++; } \
else { failures++; fprintf(stderr, "FAIL: %s\n", msg); } \
} while (0)
/* Mirror of BOOKMARKS_HMAC_KEY_LABEL in src/bookmarks.c. */
#define LABEL "sovereign-browser/bookmarks-folder-id-v1"
/* Mirror of path_to_d_tag + compute_hmac_key in src/bookmarks.c. */
static char *path_to_d_tag(const unsigned char *privkey, const char *path) {
unsigned char hmac_key[32];
if (nostr_hmac_sha256(privkey, 32,
(const unsigned char *)LABEL, strlen(LABEL),
hmac_key) != 0) {
return NULL;
}
unsigned char mac[32];
if (nostr_hmac_sha256(hmac_key, 32,
(const unsigned char *)path, strlen(path),
mac) != 0) {
return NULL;
}
char *hex = malloc(65);
if (hex == NULL) return NULL;
nostr_bytes_to_hex(mac, 32, hex);
hex[64] = '\0';
return hex;
}
/* Mirror of is_hmac_d_tag in src/bookmarks.c. */
static int is_hmac_d_tag(const char *s) {
if (s == NULL || strlen(s) != 64) return 0;
for (const char *p = s; *p; p++) {
if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f')))
return 0;
}
return 1;
}
int main(void) {
/* Use a fixed test privkey (32 bytes of 0x01..0x20). */
unsigned char privkey[32];
for (int i = 0; i < 32; i++) privkey[i] = (unsigned char)(i + 1);
/* ── Determinism: same path → same d tag ────────────────────────── */
char *d1 = path_to_d_tag(privkey, "Work/Projects/Secret");
char *d2 = path_to_d_tag(privkey, "Work/Projects/Secret");
CHECK(d1 != NULL, "path_to_d_tag returned NULL");
CHECK(d2 != NULL, "path_to_d_tag returned NULL (2nd call)");
CHECK(d1 && d2 && strcmp(d1, d2) == 0,
"Same path should produce the same d tag (determinism)");
free(d1);
free(d2);
/* ── Opaqueness: different paths → different d tags ─────────────── */
char *da = path_to_d_tag(privkey, "Work");
char *db = path_to_d_tag(privkey, "Work/Projects");
char *dc = path_to_d_tag(privkey, "Personal");
CHECK(da && db && strcmp(da, db) != 0,
"Different paths should produce different d tags (no prefix leakage)");
CHECK(da && dc && strcmp(da, dc) != 0,
"Different paths should produce different d tags (2)");
/* The d tag should NOT contain the plaintext path. */
CHECK(da && strstr(da, "Work") == NULL,
"d tag should not leak the plaintext path");
CHECK(db && strstr(db, "Work") == NULL,
"d tag should not leak the plaintext path (child)");
free(da);
free(db);
free(dc);
/* ── d tag is 64 lowercase hex chars ────────────────────────────── */
char *d = path_to_d_tag(privkey, "General");
CHECK(d != NULL, "path_to_d_tag returned NULL for General");
CHECK(d && strlen(d) == 64, "d tag should be 64 hex chars");
CHECK(d && is_hmac_d_tag(d), "d tag should pass is_hmac_d_tag");
free(d);
/* ── is_hmac_d_tag distinguishes legacy names ───────────────────── */
CHECK(is_hmac_d_tag("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
"64 lowercase hex should be recognized as an HMAC d tag");
CHECK(!is_hmac_d_tag("General"),
"Legacy plaintext 'General' should NOT be recognized as an HMAC d tag");
CHECK(!is_hmac_d_tag("Work/Projects"),
"Legacy plaintext path with slash should NOT be recognized as an HMAC d tag");
CHECK(!is_hmac_d_tag(""),
"Empty string should NOT be recognized as an HMAC d tag");
CHECK(!is_hmac_d_tag(NULL),
"NULL should NOT be recognized as an HMAC d tag");
/* Uppercase hex is not produced by nostr_bytes_to_hex (lowercase), so
* treat it as non-HMAC (legacy). */
CHECK(!is_hmac_d_tag("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
"Uppercase hex should NOT be recognized as an HMAC d tag (we emit lowercase)");
/* ── Different privkeys → different d tags for the same path ────── */
unsigned char privkey2[32];
for (int i = 0; i < 32; i++) privkey2[i] = (unsigned char)(255 - i);
char *e1 = path_to_d_tag(privkey, "General");
char *e2 = path_to_d_tag(privkey2, "General");
CHECK(e1 && e2 && strcmp(e1, e2) != 0,
"Different privkeys should produce different d tags for the same path");
free(e1);
free(e2);
/* ── Empty path is allowed (root) ───────────────────────────────── */
char *root_d = path_to_d_tag(privkey, "");
CHECK(root_d != NULL, "path_to_d_tag should handle empty path");
CHECK(root_d && strlen(root_d) == 64,
"Empty path d tag should still be 64 hex chars");
free(root_d);
printf("bookmarks tree tests: %d passed, %d failed\n", passes, failures);
return failures == 0 ? 0 : 1;
}