Add signed invites, hang links, multi-share, and EXTENSIONS.md

- Signed invites: waste: URI gains inviter+sig fields (Ed25519); hello
  carries the invite so receiving peers can verify against known keys
- RequireInvite per-network flag: rejects peers without valid signed invite
- Hash-based hang links: #waste:base64 fragment pre-fills join form without
  server-side leakage of network name
- Multi-share: shares.json (daemon) + waste_shares localStorage (browser);
  IPC add_share/remove_share/list_shares commands
- EXTENSIONS.md: addendum documenting all waste-go protocol deviations from
  YAW/2; all extensions are additive and backward compatible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-26 22:05:56 +02:00
parent 0e8ddbf4f4
commit 31e13fd509
12 changed files with 382 additions and 37 deletions

View File

@@ -27,8 +27,10 @@ type PeerConn struct {
type Mesh struct {
Identity *crypto.Identity
Store *store.Store // may be nil if persistence is disabled
ShareDir string // directory whose contents are shared with peers; "" = no sharing
DownloadDir string // directory where received files are saved
ShareDir string // directory whose contents are shared with peers; "" = no sharing
DownloadDir string // directory where received files are saved
RequireInvite bool // waste-go ext: reject peers that present no valid signed invite
InviteString string // the invite this peer used to join (sent in hello to other peers)
// ScanFiles overrides ScanShareDir when set — allows the manager to inject
// multi-share scanning without the mesh needing to know about shares.json.
ScanFiles func() []proto.FileEntry
@@ -63,6 +65,29 @@ func New(id *crypto.Identity, st *store.Store) *Mesh {
}
}
// trustedPeerIDs returns a set of peer IDs trusted on this network:
// all currently connected peers plus all peers in the persistent store.
func (m *Mesh) trustedPeerIDs() map[string]bool {
trusted := map[string]bool{}
// Own identity is always trusted.
trusted[string(m.Identity.PeerID())] = true
// Connected peers.
m.mu.RLock()
for id := range m.peers {
trusted[string(id)] = true
}
m.mu.RUnlock()
// Previously seen peers from the store.
if m.Store != nil {
if known, err := m.Store.KnownPeers(); err == nil {
for id := range known {
trusted[string(id)] = true
}
}
}
return trusted
}
// ScanShareDir returns the list of files in the local share directory.
// Returns an empty slice if ShareDir is unset or the directory is empty.
func (m *Mesh) ScanShareDir() []proto.FileEntry {

View File

@@ -14,6 +14,7 @@ import (
"github.com/pion/webrtc/v3"
"github.com/waste-go/internal/crypto"
"github.com/waste-go/internal/invite"
"github.com/waste-go/internal/proto"
)
@@ -43,11 +44,12 @@ func WireDataChannel(
localFP, remoteFP := dtlsFingerprints(pc)
bindBytes := proto.HelloBindString(localFP, remoteFP)
hello := proto.HelloMessage{
Type: "hello",
ID: string(id.PeerID()),
Nick: id.Alias,
Caps: []string{"chat", "file"},
Sig: id.Sign(bindBytes),
Type: "hello",
ID: string(id.PeerID()),
Nick: id.Alias,
Caps: []string{"chat", "file"},
Sig: id.Sign(bindBytes),
Invite: m.InviteString,
}
helloJSON, _ := json.Marshal(hello)
if err := dc.SendText(string(helloJSON)); err != nil {
@@ -149,6 +151,37 @@ func handleDCMessage(data []byte, from proto.PeerID, localID *crypto.Identity, m
log.Printf("peer: bad hello from %s: %v", from.Short(), err)
return
}
// Invite enforcement (waste-go extension).
if m.RequireInvite {
if hello.Invite == "" {
log.Printf("peer: rejecting %s — no invite presented (RequireInvite=true)", from.Short())
m.Emit(proto.IpcMessage{
Type: proto.EvtError,
ErrorMessage: fmt.Sprintf("peer %s rejected: no invite", from.Short()),
})
return
}
inv, err := invite.Decode(hello.Invite)
if err != nil || !inv.IsSigned() {
log.Printf("peer: rejecting %s — invite not signed: %v", from.Short(), err)
m.Emit(proto.IpcMessage{
Type: proto.EvtError,
ErrorMessage: fmt.Sprintf("peer %s rejected: invite not signed", from.Short()),
})
return
}
trusted := m.trustedPeerIDs()
if err := invite.Verify(inv, trusted, crypto.Verify); err != nil {
log.Printf("peer: rejecting %s — %v", from.Short(), err)
m.Emit(proto.IpcMessage{
Type: proto.EvtError,
ErrorMessage: fmt.Sprintf("peer %s rejected: %v", from.Short(), err),
})
return
}
}
// Update alias once we have the verified nick.
m.mu.Lock()
if conn, ok := m.peers[from]; ok {