Commit Graph

2 Commits

Author SHA1 Message Date
Alejandro Gutiérrez
81a8d0714b feat(crypto): client-side direct-message encryption with crypto_box
Direct messages between peers are now end-to-end encrypted. The
broker only ever sees {nonce, ciphertext} — plaintext lives on the
two endpoints.

apps/cli/src/crypto/envelope.ts:
- encryptDirect(message, recipientPubkeyHex, senderSecretKeyHex)
  → {nonce, ciphertext} via crypto_box_easy, 24-byte fresh nonce
- decryptDirect(envelope, senderPubkeyHex, recipientSecretKeyHex)
  → plaintext or null (null on MAC failure / malformed input)
- ed25519 keys (from Step 17) are converted to X25519 on the fly via
  crypto_sign_ed25519_{pk,sk}_to_curve25519 — one signing keypair
  covers both signing + encryption roles.

BrokerClient.send():
- if targetSpec is a 64-hex pubkey → encrypt via crypto_box
- else (broadcast "*" or channel "#foo") → base64-wrapped plaintext
  (shared-key encryption for channels lands in a later step)

InboundPush now carries:
- plaintext: string | null   (decrypted body, null if decryption failed
                              OR it's a non-direct message)
- kind: "direct" | "broadcast" | "channel" | "unknown"
MCP check_messages formatter reads plaintext directly.

side-fixes pulled in during 18a:
- apps/broker/scripts/seed-test-mesh.ts now generates real ed25519
  keypairs (the previous "aaaa…" / "bbbb…" fillers weren't valid
  curve points, so crypto_sign_ed25519_pk_to_curve25519 rejected
  them). Seed output now includes secretKey for each peer.
- apps/broker/src/broker.ts drainForMember wraps the atomic claim in
  a CTE + outer ORDER BY so FIFO ordering is SQL-sourced, not
  JS-sorted (Postgres microsecond timestamps collapse to the same
  Date.getTime() milliseconds otherwise).
- vitest.config.ts fileParallelism: false — test files share
  DB state via cleanupAllTestMeshes afterAll, so running them in
  parallel caused one file's cleanup to race another's inserts.
- integration/health.test.ts "returns 200" now uses waitFullyHealthy
  (a 200-only waiter) instead of waitHealthyOrAny — prevents a race
  with the startup DB ping.

verified live:
- apps/cli/scripts/roundtrip.ts (direct A→B): ciphertext in DB is
  opaque bytes (not base64-plaintext), decrypted correctly on arrival
- apps/cli/scripts/join-roundtrip.ts (full join → encrypted send):
  PASSED
- 48/48 broker tests green

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:48:33 +01:00
Alejandro Gutiérrez
76760c9b8c test(broker): smoke test for hello + direct message flow
Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Adds scripts/{seed-test-mesh,peer-a,peer-b,smoke-test}.ts|.sh that
prove an end-to-end message flow works against a real Postgres:

- seed-test-mesh.ts creates user+mesh+2 members with deterministic
  hex pubkeys ("aa..aa", "bb..bb"), writes seed JSON to stdout
- peer-a.ts sends hello then a direct "send" message to peer B's
  pubkey with fake ciphertext "hello-from-a"
- peer-b.ts sends hello, waits up to 5s for a push, asserts
  senderPubkey matches peer A, exits 0/1
- smoke-test.sh wires the three together

Verified flow: hello registers presence row → send queues into
mesh.message_queue → fanout matches connected peer by pubkey →
drainForMember joins on mesh.member for senderPubkey → push lands
with ciphertext + correct sender attribution.

Also fixes a date-serialization bug that blocked the first run:
applyPendingHookStatus used `sql${col} >= ${jsDate}` which passed
JS Date.toString() to Postgres (failed to parse). Replaced raw
sql`` template with typed gte/desc/isNotNull operators from
drizzle-orm. Same fix applied in sweepPendingStatuses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:53:33 +01:00