145 lines
6.1 KiB
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;
|
|
}
|