From 12fc77f25a831c4172c6a146e7cc7099454e313a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 6 Jun 2026 18:42:56 +0200 Subject: [PATCH] feat(mobile): Card/Invite/Kick en el binding gomobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade al binding plano sobre pkg/client: - Card(): exporta la identidad pública del peer (id + sign_pub + kex_pub) como JSON portable, para intercambio peer-a-peer (paste/QR) sin gateway. - Invite(roomID, peerCard): parsea una Card y sella la clave de room al invitado (delega en client.Invite). - Kick(roomID, endpointID): expulsa y rota la clave (forward secrecy). Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/unibus.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/mobile/unibus.go b/mobile/unibus.go index d2a6a9ce..3e4d79b4 100644 --- a/mobile/unibus.go +++ b/mobile/unibus.go @@ -11,6 +11,9 @@ package mobile import ( + "encoding/base64" + "encoding/json" + "fmt" "time" "github.com/enmanuel/unibus/pkg/client" @@ -92,6 +95,56 @@ func (s *Session) Subscribe(roomID string, l FrameListener) error { return err } +// cardJSON is the portable, copy-pasteable public identity a peer shares so a +// room owner can invite it to an encrypted room. It carries no secret: only the +// endpoint id and the two public keys (signing + key-exchange), base64-encoded +// for transport over text or a QR code. +type cardJSON struct { + ID string `json:"id"` + SignPub string `json:"sign_pub"` // base64 std of the Ed25519 public key + KexPub string `json:"kex_pub"` // base64 std of the X25519 public key +} + +// Card returns this peer's public identity as a portable JSON string. Share it +// (paste, QR) with a room owner so they can Invite you to an encrypted room. It +// contains no private key and is safe to transmit in the clear. +func (s *Session) Card() string { + ep := s.c.Endpoint() + b, _ := json.Marshal(cardJSON{ + ID: ep.ID, + SignPub: base64.StdEncoding.EncodeToString(ep.SignPub), + KexPub: base64.StdEncoding.EncodeToString(ep.KexPub), + }) + return string(b) +} + +// Invite adds the holder of peerCard to roomID. peerCard is the JSON string the +// invitee produced with Card(). For encrypted rooms this seals the current room +// key to the invitee's X25519 public key and signs the request; the caller must +// be the room owner. +func (s *Session) Invite(roomID, peerCard string) error { + var card cardJSON + if err := json.Unmarshal([]byte(peerCard), &card); err != nil { + return fmt.Errorf("mobile: bad peer card: %w", err) + } + signPub, err := base64.StdEncoding.DecodeString(card.SignPub) + if err != nil { + return fmt.Errorf("mobile: bad sign_pub in card: %w", err) + } + kexPub, err := base64.StdEncoding.DecodeString(card.KexPub) + if err != nil { + return fmt.Errorf("mobile: bad kex_pub in card: %w", err) + } + return s.c.Invite(roomID, client.Endpoint{ID: card.ID, SignPub: signPub, KexPub: kexPub}) +} + +// Kick removes endpointID from roomID and, for encrypted rooms, rotates the room +// key to a new epoch so the removed peer cannot decrypt messages published after +// the kick (forward secrecy). The caller must be the room owner. +func (s *Session) Kick(roomID, endpointID string) error { + return s.c.Kick(roomID, endpointID) +} + // Request performs an RPC request/reply against subject and returns the reply // payload as text. timeoutMs bounds the wait in milliseconds. func (s *Session) Request(subject, text string, timeoutMs int) (string, error) {