refactor invitation

This commit is contained in:
yc
2026-04-11 22:05:30 +02:00
parent 1906431061
commit 793213b3fb
16 changed files with 465 additions and 436 deletions

View File

@@ -0,0 +1,30 @@
package files
import (
"os"
"forge.redroom.link/yves/meowlib/client"
"forge.redroom.link/yves/meowlib/client/invitation/messages"
)
// Step1Write creates a pending peer and writes the InvitationInitPayload to a file.
// format: "qr" writes a QR-code PNG; anything else writes a compressed binary .mwiv file.
func Step1Write(contactName string, myNickname string, invitationMessage string, serverUids []string, format string) (*client.Peer, error) {
payload, peer, err := messages.Step1InitiatorCreatesInviteeAndTempKey(contactName, myNickname, invitationMessage, serverUids)
if err != nil {
return nil, err
}
c := client.GetConfig()
if format == "qr" {
filename := c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".png"
if err := payload.WriteQr(filename); err != nil {
return nil, err
}
} else {
filename := c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".mwiv"
if err := payload.WriteCompressed(filename); err != nil {
return nil, err
}
}
return peer, nil
}

View File

@@ -0,0 +1,44 @@
package files
import (
"errors"
"os"
"strings"
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
"forge.redroom.link/yves/meowlib/client/invitation/messages"
)
// Step2ReadAndAnswer reads an InvitationInitPayload from a .mwiv file, creates the
// invitee's peer entry, and writes the invitee's ContactCard response to a .mwiv file.
func Step2ReadAndAnswer(invitationFile string, nickname string, myNickname string, serverUids []string) error {
if _, err := os.Stat(invitationFile); os.IsNotExist(err) {
return err
}
if !strings.HasSuffix(invitationFile, ".mwiv") {
return errors.New("only .mwiv files are supported")
}
data, err := os.ReadFile(invitationFile)
if err != nil {
return err
}
payload, err := meowlib.NewInvitationInitPayloadFromCompressed(data)
if err != nil {
return err
}
mynick := myNickname
if mynick == "" {
mynick = client.GetConfig().GetIdentity().Nickname
}
response, err := messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload, nickname, mynick, serverUids)
if err != nil {
return err
}
c := client.GetConfig()
filename := c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv"
return response.GetMyContact().WriteCompressed(filename)
}

View File

@@ -0,0 +1,22 @@
package messages
import (
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
)
// Step1InitiatorCreatesInviteeAndTempKey creates a minimal pending peer and a temporary
// keypair, and returns the InvitationInitPayload to be transmitted to the invitee
// via any transport (file, QR, server…).
func Step1InitiatorCreatesInviteeAndTempKey(contactName string, myNickname string, invitationMessage string, serverUids []string) (*meowlib.InvitationInitPayload, *client.Peer, error) {
mynick := myNickname
if mynick == "" {
mynick = client.GetConfig().GetIdentity().Nickname
}
payload, peer, err := client.GetConfig().GetIdentity().InvitationStep1(mynick, contactName, serverUids, invitationMessage)
if err != nil {
return nil, nil, err
}
client.GetConfig().GetIdentity().Save()
return payload, peer, nil
}

View File

@@ -0,0 +1,22 @@
package messages
import (
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
)
// Step2InviteeCreatesInitiatorAndEncryptedContactCard creates the invitee's peer entry
// from an InvitationInitPayload and generates the encrypted ContactCard to be sent back
// to the initiator via any transport.
func Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload *meowlib.InvitationInitPayload, nickname string, myNickname string, serverUids []string) (*client.Peer, error) {
mynick := myNickname
if mynick == "" {
mynick = client.GetConfig().GetIdentity().Nickname
}
peer, err := client.GetConfig().GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload)
if err != nil {
return nil, err
}
client.GetConfig().GetIdentity().Save()
return peer, nil
}

View File

@@ -0,0 +1,46 @@
package messages
import (
"errors"
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
"google.golang.org/protobuf/proto"
)
// Step3InitiatorFinalizesInviteeAndCreatesContactCard is called by the initiator when a
// step-2 answer (invitee's encrypted ContactCard) arrives. It decrypts the card, upgrades
// the invitee's peer entry with the real keys, and returns the initiator's own ContactCard
// ready to be sent to the invitee via any transport.
func Step3InitiatorFinalizesInviteeAndCreatesContactCard(invitation *meowlib.Invitation) (*client.Peer, *meowlib.ContactCard, error) {
var invitationAnswer meowlib.PackedUserMessage
if err := proto.Unmarshal(invitation.Payload, &invitationAnswer); err != nil {
return nil, nil, err
}
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitation.Uuid)
if peer == nil {
return nil, nil, errors.New("no peer for invitation uuid " + invitation.Uuid)
}
// Guard against duplicate delivery (e.g., same answer from multiple servers).
if peer.InvitationKp == nil {
return nil, nil, nil
}
usermsg, err := peer.ProcessInboundStep2UserMessage(&invitationAnswer, invitation.From)
if err != nil {
return nil, nil, err
}
var inviteeCC meowlib.ContactCard
if err := proto.Unmarshal(usermsg.Invitation.Payload, &inviteeCC); err != nil {
return nil, nil, err
}
myCC, peer, err := client.GetConfig().GetIdentity().InvitationStep3(&inviteeCC)
if err != nil {
return nil, nil, err
}
client.GetConfig().GetIdentity().Save()
return peer, myCC, nil
}

View File

@@ -0,0 +1,32 @@
package messages
import (
"errors"
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
"google.golang.org/protobuf/proto"
)
// Step4InviteeFinalizesInitiator is called by the invitee's message processor when a
// UserMessage with invitation.step == 3 arrives. It unmarshals the initiator's ContactCard
// and completes the invitee's peer entry with the initiator's real keys.
func Step4InviteeFinalizesInitiator(usermsg *meowlib.UserMessage) (*client.Peer, error) {
if usermsg.Invitation == nil || usermsg.Invitation.Step != 3 {
return nil, errors.New("expected invitation step 3")
}
var initiatorCC meowlib.ContactCard
if err := proto.Unmarshal(usermsg.Invitation.Payload, &initiatorCC); err != nil {
return nil, err
}
// Patch the invitation ID from the outer message in case it was not set in the CC.
if initiatorCC.InvitationId == "" {
initiatorCC.InvitationId = usermsg.Invitation.Uuid
}
if err := client.GetConfig().GetIdentity().InvitationStep4(&initiatorCC); err != nil {
return nil, err
}
client.GetConfig().GetIdentity().Save()
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(initiatorCC.InvitationId)
return peer, nil
}

View File

@@ -0,0 +1,61 @@
package server
import (
"time"
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
)
// Step1Post builds and returns the packed server message that posts the
// InvitationInitPayload to the invitation server.
func Step1Post(invitationId string, invitationServerUid string, timeOut int, urlLen int, password string) ([]byte, error) {
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
if peer == nil {
return nil, nil
}
if peer.InvitationKp == nil {
return nil, nil
}
initPayload := &meowlib.InvitationInitPayload{
Uuid: peer.InvitationId,
Name: peer.MyName,
PublicKey: peer.InvitationKp.Public,
InvitationMessage: peer.InvitationMessage,
}
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
if err != nil {
return nil, err
}
msg, err := invitationServer.BuildToServerMessageInvitationStep1(initPayload, password, timeOut, urlLen)
if err != nil {
return nil, err
}
return invitationServer.ProcessOutboundMessage(msg)
}
// Step1ReadResponse reads the server response to a Step1 post and returns the
// shortcode URL and expiry wrapped in an Invitation.
func Step1ReadResponse(invitationServerUid string, invitationResponse []byte) (*meowlib.Invitation, error) {
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
if err != nil {
return nil, err
}
serverMsg, err := srv.ProcessInboundServerResponse(invitationResponse)
if err != nil {
return nil, err
}
return serverMsg.Invitation, nil
}
// SetUrlInfo stores the shortcode URL and expiry on the pending peer.
func SetUrlInfo(invitationId string, url string, expiry int64) {
id := client.GetConfig().GetIdentity()
peer := id.Peers.GetFromInvitationId(invitationId)
if peer == nil {
return
}
peer.InvitationUrl = url
peer.InvitationExpiry = time.Unix(expiry, 0)
id.Peers.StorePeer(peer)
}

View File

@@ -0,0 +1,107 @@
package server
import (
"errors"
"strings"
"forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client"
)
// Step2Fetch builds and returns the packed server message that retrieves the
// InvitationInitPayload from the server using the shortcode URL.
func Step2Fetch(invitationUrl string, serverPublicKey string, invitationPassword string) ([]byte, error) {
meowurl := strings.Split(invitationUrl, "?")
shortcode := meowurl[1]
srv, err := client.CreateServerFromMeowUrl(meowurl[0])
if err != nil {
return nil, err
}
// Reuse the server entry if already known.
dbsrv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srv.Url)
if err != nil {
return nil, err
}
if dbsrv == nil {
srv.PublicKey = serverPublicKey
k, err := meowlib.NewKeyPair()
if err != nil {
return nil, err
}
srv.UserKp = k
if err := client.GetConfig().GetIdentity().MessageServers.StoreServer(srv); err != nil {
return nil, err
}
} else {
if dbsrv.PublicKey != serverPublicKey {
dbsrv.PublicKey = serverPublicKey
}
srv = dbsrv
}
toSrvMsg, err := srv.BuildToServerMessageInvitationRequest(shortcode, invitationPassword)
if err != nil {
return nil, err
}
return srv.ProcessOutboundMessage(toSrvMsg)
}
// Step2ReadResponse decodes the server response to a Step2Fetch and returns
// the InvitationInitPayload sent by the initiator.
func Step2ReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.InvitationInitPayload, error) {
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
if err != nil {
return nil, err
}
serverMsg, err := srv.ProcessInboundServerResponse(invitationData)
if err != nil {
return nil, err
}
return meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload)
}
// Step2PostAnswer builds and returns the packed server message that posts the
// invitee's ContactCard (encrypted with the initiator's temp key) to the invitation server.
func Step2PostAnswer(invitationId string, invitationServerUid string, timeout int) ([]byte, error) {
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
if peer == nil {
return nil, errors.New("no peer with that invitation id")
}
answermsg, err := peer.BuildInvitationStep2Message(peer.GetMyContact())
if err != nil {
return nil, err
}
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
if err != nil {
return nil, err
}
packedMsg, err := peer.ProcessOutboundUserMessage(answermsg)
if err != nil {
return nil, err
}
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
if err != nil {
return nil, err
}
return invitationServer.ProcessOutboundMessage(toServerMessage)
}
// Step2PostAnswerReadResponse reads the server acknowledgement of a Step2PostAnswer.
func Step2PostAnswerReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.Invitation, error) {
srv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
if err != nil {
return nil, err
}
serverMsg, err := srv.ProcessInboundServerResponse(invitationData)
if err != nil {
return nil, err
}
return serverMsg.Invitation, nil
}

View File

@@ -0,0 +1,51 @@
package server
import (
"errors"
"forge.redroom.link/yves/meowlib/client"
)
// Step3PostCard builds and returns the packed server messages that send the
// initiator's full ContactCard to the invitee through the invitee's servers.
// Step 3 must NOT use DR or sym layers: the invitee hasn't received those keys yet
// (they are carried inside this very message). Plain asym encryption is used.
func Step3PostCard(invitationId string) ([][]byte, error) {
id := client.GetConfig().GetIdentity()
peer := id.Peers.GetFromInvitationId(invitationId)
if peer == nil {
return nil, errors.New("no peer for invitation id " + invitationId)
}
step3msg, err := peer.BuildInvitationStep3Message(peer.GetMyContact())
if err != nil {
return nil, err
}
serialized, err := peer.SerializeUserMessage(step3msg)
if err != nil {
return nil, err
}
enc, err := peer.AsymEncryptMessage(serialized)
if err != nil {
return nil, err
}
packedMsg := peer.PackUserMessage(enc.Data, enc.Signature)
var results [][]byte
for _, srvUid := range peer.ContactPullServers {
srv, err := id.MessageServers.LoadServer(srvUid)
if err != nil {
continue
}
toSrvMsg := srv.BuildToServerMessageFromUserMessage(packedMsg)
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
if err != nil {
continue
}
results = append(results, bytemsg)
}
if len(results) == 0 {
return nil, errors.New("could not build message for any invitee server")
}
return results, nil
}

View File

@@ -0,0 +1,44 @@
package server
import (
"errors"
"forge.redroom.link/yves/meowlib/client"
)
// Step4PostConfirmation builds and returns the packed server messages that send the
// invitee's confirmation to the initiator through the initiator's servers.
func Step4PostConfirmation(invitationId string) ([][]byte, error) {
id := client.GetConfig().GetIdentity()
peer := id.Peers.GetFromInvitationId(invitationId)
if peer == nil {
return nil, errors.New("no peer for invitation id " + invitationId)
}
step4msg, err := peer.BuildInvitationStep4Message()
if err != nil {
return nil, err
}
packedMsg, err := peer.ProcessOutboundUserMessage(step4msg)
if err != nil {
return nil, err
}
var results [][]byte
for _, srvUid := range peer.ContactPullServers {
srv, err := id.MessageServers.LoadServer(srvUid)
if err != nil {
continue
}
toSrvMsg := srv.BuildToServerMessageFromUserMessage(packedMsg)
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
if err != nil {
continue
}
results = append(results, bytemsg)
}
if len(results) == 0 {
return nil, errors.New("could not build message for any initiator server")
}
return results, nil
}