Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
|
|
|
package crypto
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestPeerIDIsHex(t *testing.T) {
|
|
|
|
|
id := newTestIdentity(t)
|
|
|
|
|
peerID := string(id.PeerID())
|
|
|
|
|
|
|
|
|
|
if len(peerID) != 64 {
|
|
|
|
|
t.Fatalf("PeerID length = %d, want 64", len(peerID))
|
|
|
|
|
}
|
|
|
|
|
if strings.ToLower(peerID) != peerID {
|
|
|
|
|
t.Fatal("PeerID is not lowercase")
|
|
|
|
|
}
|
|
|
|
|
if _, err := hex.DecodeString(peerID); err != nil {
|
|
|
|
|
t.Fatalf("PeerID is not valid hex: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSignVerifyHex(t *testing.T) {
|
|
|
|
|
id := newTestIdentity(t)
|
|
|
|
|
msg := []byte("test message")
|
|
|
|
|
|
|
|
|
|
sig := id.Sign(msg)
|
|
|
|
|
if err := Verify(string(id.PeerID()), msg, sig); err != nil {
|
|
|
|
|
t.Fatalf("Verify failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSignalingBoxRoundTrip(t *testing.T) {
|
|
|
|
|
alice := newTestIdentity(t)
|
|
|
|
|
bob := newTestIdentity(t)
|
|
|
|
|
|
|
|
|
|
plaintext := []byte(`{"kind":"offer","sdp":"v=0..."}`)
|
|
|
|
|
|
|
|
|
|
sealed := SignalingBox(plaintext, bob.CurvePublicKey(), alice.CurvePrivateKey())
|
|
|
|
|
got, err := SignalingOpen(sealed, alice.CurvePublicKey(), bob.CurvePrivateKey())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("SignalingOpen failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if string(got) != string(plaintext) {
|
|
|
|
|
t.Fatalf("got %q, want %q", got, plaintext)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSignalingBoxTamperedFails(t *testing.T) {
|
|
|
|
|
alice := newTestIdentity(t)
|
|
|
|
|
bob := newTestIdentity(t)
|
|
|
|
|
|
|
|
|
|
sealed := SignalingBox([]byte("hello"), bob.CurvePublicKey(), alice.CurvePrivateKey())
|
|
|
|
|
|
|
|
|
|
// wrong sender public key
|
|
|
|
|
carol := newTestIdentity(t)
|
|
|
|
|
if _, err := SignalingOpen(sealed, carol.CurvePublicKey(), bob.CurvePrivateKey()); err == nil {
|
|
|
|
|
t.Fatal("expected error with wrong sender key, got nil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 21:44:48 +02:00
|
|
|
func TestExportImportRoundtrip(t *testing.T) {
|
|
|
|
|
id := newTestIdentity(t)
|
|
|
|
|
passphrase := "correct horse battery staple"
|
|
|
|
|
|
|
|
|
|
blob, err := ExportIdentity(id, passphrase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ExportIdentity: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exported blob must contain the public peer ID in plaintext.
|
|
|
|
|
if !strings.Contains(string(blob), string(id.PeerID())) {
|
|
|
|
|
t.Fatal("exported blob does not contain peer ID")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imported, err := ImportIdentity(blob, passphrase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ImportIdentity: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if imported.PeerID() != id.PeerID() {
|
|
|
|
|
t.Fatalf("peer ID mismatch: got %s, want %s", imported.PeerID(), id.PeerID())
|
|
|
|
|
}
|
|
|
|
|
if imported.Alias != id.Alias {
|
|
|
|
|
t.Fatalf("alias mismatch: got %q, want %q", imported.Alias, id.Alias)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestImportWrongPassphraseFails(t *testing.T) {
|
|
|
|
|
id := newTestIdentity(t)
|
|
|
|
|
blob, err := ExportIdentity(id, "correct")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ExportIdentity: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := ImportIdentity(blob, "wrong"); err == nil {
|
|
|
|
|
t.Fatal("expected error with wrong passphrase, got nil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Migrate to YAW/2 protocol: WebRTC transport, anchor signaling, nacl/box crypto
Replace the original raw-TCP peer connections and Blowfish/RSA-era design with
the YAW/2 protocol stack:
Transport
- internal/mesh/peer.go: raw TCP + manual ECDH replaced with pion/webrtc
DataChannels (ICE, DTLS, SCTP). Peers negotiate via sealed offer/answer/
candidate exchange rather than direct dialling.
- internal/nat: deleted — ICE subsumes everything this package was going to do.
Signaling
- cmd/anchor: new WebSocket signaling server (replaces cmd/relay). Verifies
Ed25519 challenge/join signatures, routes opaque nacl/box blobs, broadcasts
peer-join/peer-leave events. Never sees plaintext signaling content.
- internal/anchor: new anchor client. Manages PeerConnection lifecycle,
decides offerer by peer-id comparison, seals/opens signaling payloads with
nacl/box, implements mesh.Anchor.
Crypto (internal/crypto)
- PeerID encoding: base64url → lowercase hex (64 chars, YAW/2 §2).
- Sign/Verify: hex signatures throughout.
- Added CurvePublicKey/CurvePrivateKey: X25519 keys derived from Ed25519
identity via Montgomery conversion (filippo.io/edwards25519), matching
libsodium's crypto_sign_ed25519_*_to_curve25519.
- Added SignalingBox/SignalingOpen: nacl/box (XSalsa20-Poly1305) seal/open
for signaling payloads.
Wire types (internal/proto)
- ChatMessage gains Mid (random 16-byte hex) for mesh deduplication (§8).
- FileChunk removed from PeerMessage — chunks go on a separate binary
DataChannel labelled "f:<xid>"; FileDone added.
- New types: SignalingPayload, AnchorMessage, HelloMessage, HelloBindString.
- PeerID.Short() now returns first 16 hex chars grouped in 4s.
- IPC: CmdConnect removed; CmdJoinNetwork/CmdLeaveNetwork added;
EvtSessionReady added (fires when DataChannel opens and hello is received).
IPC (internal/ipc)
- join_network/leave_network replace connect (direct TCP dial).
- Network joins are context-scoped: leave_network or client disconnect
cancels the anchor connection cleanly.
README updated to reflect new project layout, getting-started commands,
IPC protocol, and roadmap state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:14 +02:00
|
|
|
// newTestIdentity creates a fresh in-memory identity for testing.
|
|
|
|
|
func newTestIdentity(t *testing.T) *Identity {
|
|
|
|
|
t.Helper()
|
|
|
|
|
id, err := LoadOrCreate(t.TempDir(), "test")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("LoadOrCreate: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return id
|
|
|
|
|
}
|