/* Реальные A–S–B/C соединения: S хранит только ciphertext. Запросы переживают * офлайн автора/restart; B получает ключ после решения, C получает отказ. */ #include #include #include #include #include "../lib/platform_compat.h" #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../src/utun_instance.h" #include "../src/ntp_time.h" #include "../src/chat/chat_core.h" #include "../src/chat/chat_attachment.h" #include "../src/chat/member_sync.h" #include "../src/chat/chat_setting.h" #include "../src/routing_layer/topo_node_sqlite.h" #include "../src/routing_layer/topo_group.h" #include "../src/routing_layer/etcp_router.h" #include "../src/transport_layer/secure_channel.h" #include "../src/transport_layer/etcp.h" #include "../src/media_async/media_async.h" #include "../src/media_async/attachment_send.h" #include "test_utils.h" #include "test_clock.h" #include "test_net.h" #define GROUP 4242424242424244ULL #define CHANNEL "4242424242424244" #define FILE_BYTES (3 * 32768 + 17) enum { A, B, S, M, NODES }; /* M — второй запрашивающий клиент; S — storage+supernode. */ struct media_test { struct UASYNC* ua; struct UTUN_INSTANCE* nodes[NODES]; struct SC_MYKEYS keys[NODES]; uint64_t ids[NODES]; uint16_t ports[NODES]; uint8_t ed[NODES][32], ch_x[32], ch_ed[32], ch_priv[32], ch_sig[64], message[32]; char dir[512], file[768]; unsigned phase, ticks, heartbeats, hold; int failed; void* timer; }; static int value(struct UTUN_INSTANCE* inst, const char* sql) { if (!inst) return -1; sqlite3_stmt* st = NULL; int result = -1; if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) result = sqlite3_column_int(st, 0); sqlite3_finalize(st); return result; } static int live(struct UTUN_INSTANCE* inst, uint64_t peer) { return inst && etcp_router_get_path(inst, GROUP, peer, ETCP_RT_ID_ATTACHMENT_KEYS).next_hop_node_id != 0; } static void failure(struct media_test* t, const char* reason) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted attachment test failed phase=%u reason=%s", t->phase, reason); t->failed = 1; uasync_stop(t->ua); } static void hex(const uint8_t* p, size_t len, char* out) { for (size_t i = 0; i < len; i++) snprintf(out + i * 2, 3, "%02x", p[i]); } static int start_node(struct media_test* t, int index) { char path[768], pub[65], priv[65], server[65], db[768]; hex(t->keys[index].public_key, 32, pub); hex(t->keys[index].private_key, 32, priv); snprintf(db, sizeof(db), "%s/db%d", t->dir, index); utun_mkdir(db, 0700); snprintf(path, sizeof(path), "%s/%d.conf", t->dir, index); FILE* f = fopen(path, "w"); if (!f) return -1; fprintf(f, "[global]\nmy_private_key=%s\nmy_public_key=%s\ndb_path=%s\n" "[server:s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n" "[chatserver]\nstorage_autoload=0\n", priv, pub, db, t->ports[index]); if (index != S) { hex(t->keys[S].public_key, 32, server); fprintf(f, "[client:to_s]\npeer_public_key=%s\nlink=s1:127.0.0.1:0\n", server); } fclose(f); t->nodes[index] = utun_instance_create(t->ua, path); if (t->nodes[index]) { t->ports[index] = ss_get_port(&test_etcp_bound_socket(t->nodes[index], "s1")->local_addr); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "encrypted attachment fixture: node=%d port=%u retained for restart", index, t->ports[index]); } if (t->nodes[index] && index != S) test_etcp_set_peer(t->nodes[index], "to_s", t->nodes[S], "s1"); return t->nodes[index] ? utun_instance_init(t->nodes[index]) : -1; } static int setup(struct media_test* t) { for (int i = 0; i < NODES; i++) { struct UTUN_INSTANCE* inst = t->nodes[i]; if (topo_node_sqlite_channel_put(inst->topo_sqlite_db, CHANNEL, "encrypted-attachment-test", t->ids[A], t->ch_x, NULL, t->ch_ed, i == A ? t->ch_priv : NULL, t->ch_sig)) return -1; chat_core_ensure_channel_ready(inst, CHANNEL); for (int j = 0; j < NODES; j++) { uint8_t msg[512], join[64], adm_sig[64], certificate[64]; uint64_t ts = ntp_time_get_seconds(inst); int n = member_sync_build_join_msg(t->ch_x, t->ch_ed, t->ids[j], t->keys[j].public_key, ts, msg, sizeof(msg)); if (n < 0 || sc_ed25519_sign(t->nodes[j]->my_ed25519_privkey, msg, (size_t)n, join) != SC_OK) return -1; const char* tags = j == S ? "{\"supernode\":\"yes\",\"storage\":\"yes\",\"ver\":\"1\"}" : "{\"ver\":\"1\"}"; size_t off = strlen(tags); memcpy(msg, tags, off); memcpy(msg + off, &t->ids[j], 8); if (sc_ed25519_sign(t->ch_priv, msg, off + 8, adm_sig) != SC_OK) return -1; char info[64]; snprintf(info, sizeof(info), "{\"name\":\"node%d\"}", j); if (member_sync_sign_pubkey(t->ch_priv, t->keys[j].public_key, certificate)) return -1; if (member_sync_put(inst, CHANNEL, t->ids[j], t->keys[j].public_key, t->ed[j], join, ts, NULL, 0, info, tags, adm_sig, 0, 0, certificate) < 0) return -1; } if (i != S) member_sync_start(inst, t->ids[S], CHANNEL, NULL, NULL); } return 0; } /* Ключи и метаданные до подтверждения отсутствуют даже у скачавшего storage. */ static int snapshot(struct media_test* t) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(t->nodes[A]->topo_sqlite_db, "SELECT message_id FROM attachment_keys WHERE message_id IS NOT NULL", -1, &st, NULL)) return -1; int ok = sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 32; if (ok) memcpy(t->message, sqlite3_column_blob(st, 0), 32); sqlite3_finalize(st); return ok ? 0 : -1; } static int readable(struct media_test* t, int index) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(t->nodes[index]->topo_sqlite_db, "SELECT plain_path FROM attachment_keys WHERE ready=1 AND length(key)=32", -1, &st, NULL)) return 0; char path[1024] = ""; if (sqlite3_step(st) == SQLITE_ROW) snprintf(path, sizeof(path), "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st); FILE* f = path[0] ? fopen(path, "rb") : NULL; if (!f) return 0; int ok = 1; for (unsigned i = 0; i < FILE_BYTES; i++) if (fgetc(f) != (int)((i * 73 + 19) & 255)) { ok = 0; break; } if (fgetc(f) != EOF) ok = 0; fclose(f); return ok; } static void download(struct media_test* t, int index) { int id = value(t->nodes[index], "SELECT id FROM msg_" CHANNEL " ORDER BY id LIMIT 1"); if (id <= 0) { failure(t, "history not available before download"); return; } chat_core_attachment_download(t->nodes[index], CHANNEL, id); } /* Вход ПОСЛЕ router позволяет отдельно проверить требования флагов и точной идентичности. */ static void inject(struct media_test* t, int source, int target, unsigned flags, int corrupt) { struct UTUN_INSTANCE* inst = t->nodes[target]; struct ETCP_CONN* conn = instance_find_conn(inst, t->ids[S]); sqlite3_stmt* st = NULL; if (!conn || sqlite3_prepare_v2(t->nodes[A]->topo_sqlite_db, "SELECT crypto_id,cipher_hash,key FROM attachment_keys", -1, &st, NULL) || sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); failure(t, "control fixture"); return; } struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + 117); if (!e) { sqlite3_finalize(st); failure(t, "control allocation"); return; } memset(e->dgram, 0, ROUTER_SVC_PAYLOAD_OFF + 117); e->len = ROUTER_SVC_PAYLOAD_OFF + 117; e->dgram[0] = ETCP_RT_ID_ATTACHMENT_KEYS; memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &t->ids[source], 8); memcpy(e->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, 8); uint64_t group = GROUP; memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8); e->dgram[ROUTER_SVC_FLAGS_OFF] = flags; uint8_t* b = e->dgram + ROUTER_SVC_PAYLOAD_OFF; memcpy(b, "AK01", 4); b[4] = 3; memcpy(b + 5, t->message, 32); memcpy(b + 37, sqlite3_column_blob(st, 0), 16); memcpy(b + 53, sqlite3_column_blob(st, 1), 32); memcpy(b + 85, sqlite3_column_blob(st, 2), 32); sqlite3_finalize(st); if (corrupt) b[53] ^= 1; inst->router_bindings.callbacks[ETCP_RT_ID_ATTACHMENT_KEYS](conn, e); } static void tick(void* arg) { struct media_test* t = arg; t->ticks++; t->heartbeats++; struct UTUN_INSTANCE* a = t->nodes[A]; struct UTUN_INSTANCE* b = t->nodes[B]; struct UTUN_INSTANCE* s = t->nodes[S]; struct UTUN_INSTANCE* c = t->nodes[M]; switch (t->phase) { case 0: if (live(a, t->ids[S]) && live(b, t->ids[S]) && live(c, t->ids[S])) { if (setup(t)) { failure(t, "signed group members"); return; } t->phase++; } /* UTUN routes precede CHAT routes; setup once physical links are UP. */ else if (t->ticks >= 20 && instance_find_conn(a, t->ids[S]) && instance_find_conn(b, t->ids[S]) && instance_find_conn(c, t->ids[S])) { if (setup(t)) { failure(t, "signed group members"); return; } t->phase++; } break; case 1: if (live(a, t->ids[B]) && live(a, t->ids[M])) { if (chat_setting_set(s, "storage_autoload", "1")) { failure(t, "storage autoload setting"); return; } struct attachment_send_req* req = u_calloc(1, sizeof(*req)); if (!req) { failure(t, "send allocation"); return; } req->inst = a; req->encrypted = 1; req->info.kind = ATTACHMENT_FILE; snprintf(req->target, sizeof(req->target), "%s", CHANNEL); snprintf(req->source, sizeof(req->source), "%s", t->file); snprintf(req->info.name, sizeof(req->info.name), "private-name.bin"); attachment_send_trampoline(req); t->phase++; } break; case 2: if (value(s, "SELECT count(*) FROM msg_" CHANNEL) == 1 && value(b, "SELECT count(*) FROM msg_" CHANNEL) == 1 && value(c, "SELECT count(*) FROM msg_" CHANNEL) == 1 && !snapshot(t)) { if (value(b, "SELECT count(*) FROM msg_" CHANNEL " WHERE instr(CAST(data AS TEXT),'private-name.bin')>0") != 0) { failure(t, "plaintext filename leaked into group history"); return; } t->phase++; } break; case 3: if (value(s, "SELECT downloaded FROM attachment_keys") == 1) { if (value(s, "SELECT count(*) FROM attachment_key_requests") || value(s, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL")) { failure(t, "storage requested or received a key"); return; } utun_instance_destroy(a); t->nodes[A] = NULL; download(t, B); download(t, M); t->phase++; } break; case 4: if (value(b, "SELECT count(*) FROM attachment_key_outbox") == 1 && value(c, "SELECT count(*) FROM attachment_key_outbox") == 1) { if (readable(t, B) || readable(t, M)) { failure(t, "plaintext before author consent"); return; } if (start_node(t, A)) { failure(t, "author restart"); return; } t->phase++; } break; case 5: if (value(a, "SELECT count(*) FROM attachment_key_requests WHERE incoming=1 AND state=0") == 2) { inject(t, S, B, ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED, 0); inject(t, A, B, ROUTER_FLAG_SIGNED, 0); inject(t, A, B, ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED, 1); if (value(b, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL") != 0) { failure(t, "forged control accepted"); return; } utun_instance_destroy(a); t->nodes[A] = NULL; if (start_node(t, A)) { failure(t, "pending request restart"); return; } t->phase++; } break; case 6: if (live(a, t->ids[B]) && live(a, t->ids[M])) { if (value(a, "SELECT count(*) FROM attachment_key_requests WHERE incoming=1 AND state=0") != 2 || chat_attachment_decide(a, CHANNEL, t->message, t->ids[B], 1) || chat_attachment_decide(a, CHANNEL, t->message, t->ids[M], 0)) { failure(t, "author decisions after restart"); return; } t->phase++; } break; case 7: if (readable(t, B) && value(c, "SELECT state FROM attachment_key_requests WHERE incoming=0") == 3 && value(a, "SELECT count(*) FROM attachment_key_outbox") == 0) { if (readable(t, M) || value(c, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL") || value(s, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL")) { failure(t, "key disclosed to denied/storage participant"); return; } for (int i = 0; i < NODES; i++) { if (value(t->nodes[i], "SELECT count(*) FROM media_files WHERE location NOT LIKE '%.uenc'") != 0) { failure(t, "plaintext indexed for network distribution"); return; } } utun_instance_destroy(b); t->nodes[B] = NULL; if (start_node(t, B)) { failure(t, "recipient restart"); return; } t->phase++; } break; case 8: if (live(b, t->ids[A]) && readable(t, B)) { inject(t, A, B, ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED, 0); if (!readable(t, B) || value(b, "SELECT count(*) FROM attachment_key_requests") != 1 || value(a, "SELECT count(*) FROM attachment_key_requests") != 2) { failure(t, "duplicate grant changed persisted access"); return; } t->phase++; } break; case 9: if (++t->hold >= 20) { t->phase++; uasync_stop(t->ua); return; } break; } if (t->failed) return; if (t->ticks > 12000) { failure(t, "timeout"); return; } t->timer = uasync_set_timeout(t->ua, 100, t, tick, "encrypted_attachment_test"); } int main(void) { struct media_test t = {0}; debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); test_clock_init(1000); test_clock_sync_wall_time(); if (getenv("UTUN_TEST_DEBUG")) { debug_set_category_level_by_name("dm", "debug"); debug_set_category_level_by_name("media", "debug"); debug_set_category_level_by_name("bgp", "info"); debug_set_category_level_by_name("etcp_route", "info"); debug_set_category_level_by_name("connection", "debug"); debug_set_category_level_by_name("etcp", "info"); } utun_instance_set_tun_init_enabled(0); snprintf(t.dir, sizeof(t.dir), "/tmp/utun_encrypted_attachment_XXXXXX"); if (test_mkdtemp(t.dir)) return 1; for (int i = 0; i < NODES; i++) { if (sc_generate_keypair(&t.keys[i]) || sc_derive_ed25519_pubkey(t.keys[i].private_key, t.ed[i])) return 1; t.ids[i] = sc_derive_node_id_from_pubkey(t.keys[i].public_key); } struct SC_MYKEYS channel; if (sc_generate_keypair(&channel)) return 1; memcpy(t.ch_x, channel.public_key, 32); if (sc_derive_ed25519_pubkey(channel.private_key, t.ch_ed)) return 1; uint8_t channel_seed[SHA512_DIGEST_LENGTH]; SHA512(channel.private_key, 32, channel_seed); memcpy(t.ch_priv, channel_seed, 32); uint8_t signed_channel[256]; size_t off = sizeof(CHANNEL); memcpy(signed_channel, CHANNEL, off); const char name[] = "encrypted-attachment-test"; memcpy(signed_channel + off, name, sizeof(name)); off += sizeof(name); memcpy(signed_channel + off, &t.ids[A], 8); off += 8; memcpy(signed_channel + off, t.ch_x, 32); off += 32; memcpy(signed_channel + off, t.ch_ed, 32); off += 32; if (sc_ed25519_sign(t.ch_priv, signed_channel, off, t.ch_sig)) return 1; snprintf(t.file, sizeof(t.file), "%s/source.bin", t.dir); FILE* f = fopen(t.file, "wb"); if (!f) return 1; for (unsigned i = 0; i < FILE_BYTES; i++) fputc((int)((i * 73 + 19) & 255), f); fclose(f); t.ua = uasync_create(); if (!t.ua || start_node(&t, S) || start_node(&t, A) || start_node(&t, M) || start_node(&t, B)) return 1; t.timer = uasync_set_timeout(t.ua, 100, &t, tick, "dm_media_test"); test_clock_mainloop(t.ua); for (int i = 0; i < NODES; i++) if (t.nodes[i]) utun_instance_destroy(t.nodes[i]); uasync_destroy(t.ua, 0); test_clock_finish(); printf("test_encrypted_attachment: phase=%u heartbeats=%u %s artifacts=%s\n", t.phase, t.heartbeats, !t.failed && t.phase == 10 ? "PASS" : "FAIL", t.dir); return t.failed || t.phase != 10; }