From 793213b3fb1b1aeb35630f32f13638ab084a3bbb Mon Sep 17 00:00:00 2001 From: yc Date: Sat, 11 Apr 2026 22:05:30 +0200 Subject: [PATCH] refactor invitation --- Unnamed.FCStd | Bin 0 -> 53299 bytes client/helpers/bgPollHelper.go | 10 +- client/helpers/invitationAnswerHelper.go | 110 --------------- client/helpers/invitationCheckHelper.go | 70 ---------- client/helpers/invitationCreateHelper.go | 102 -------------- client/helpers/invitationFinalizeHelper.go | 150 --------------------- client/invitation/files/step1.go | 30 +++++ client/invitation/files/step2.go | 44 ++++++ client/invitation/messages/step1.go | 22 +++ client/invitation/messages/step2.go | 22 +++ client/invitation/messages/step3.go | 46 +++++++ client/invitation/messages/step4.go | 32 +++++ client/invitation/server/step1.go | 61 +++++++++ client/invitation/server/step2.go | 107 +++++++++++++++ client/invitation/server/step3.go | 51 +++++++ client/invitation/server/step4.go | 44 ++++++ 16 files changed, 465 insertions(+), 436 deletions(-) create mode 100644 Unnamed.FCStd delete mode 100644 client/helpers/invitationAnswerHelper.go delete mode 100644 client/helpers/invitationCheckHelper.go delete mode 100644 client/helpers/invitationCreateHelper.go delete mode 100644 client/helpers/invitationFinalizeHelper.go create mode 100644 client/invitation/files/step1.go create mode 100644 client/invitation/files/step2.go create mode 100644 client/invitation/messages/step1.go create mode 100644 client/invitation/messages/step2.go create mode 100644 client/invitation/messages/step3.go create mode 100644 client/invitation/messages/step4.go create mode 100644 client/invitation/server/step1.go create mode 100644 client/invitation/server/step2.go create mode 100644 client/invitation/server/step3.go create mode 100644 client/invitation/server/step4.go diff --git a/Unnamed.FCStd b/Unnamed.FCStd new file mode 100644 index 0000000000000000000000000000000000000000..73b4f674b49be5182fece3676b66cf354c42b676 GIT binary patch literal 53299 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 Z9N;~t5%5olK*HAg`T~4{_&+v~{}0W6)~5gf literal 0 HcmV?d00001 diff --git a/client/helpers/bgPollHelper.go b/client/helpers/bgPollHelper.go index 1ff0b88..2ae64ee 100644 --- a/client/helpers/bgPollHelper.go +++ b/client/helpers/bgPollHelper.go @@ -11,6 +11,8 @@ import ( "forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib/client" + invmsgs "forge.redroom.link/yves/meowlib/client/invitation/messages" + invsrv "forge.redroom.link/yves/meowlib/client/invitation/server" "github.com/google/uuid" "google.golang.org/protobuf/proto" ) @@ -131,10 +133,10 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error } // check if invitation answer (step-2 answer waiting for the initiator) if fromServerMessage.Invitation != nil { - peer, _, _, invErr := InvitationStep3ProcessAnswer(fromServerMessage.Invitation) + peer, _, invErr := invmsgs.Step3InitiatorFinalizesInviteeAndCreatesContactCard(fromServerMessage.Invitation) if invErr == nil && peer != nil { // Auto-send step-3 CC to invitee's servers. - msgs, _, sendErr := InvitationStep3Message(peer.InvitationId) + msgs, sendErr := invsrv.Step3PostCard(peer.InvitationId) if sendErr == nil { for i, bytemsg := range msgs { if i < len(peer.ContactPullServers) { @@ -167,10 +169,10 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error // Handle invitation step 3: initiator's full ContactCard arriving at the invitee. if usermsg.Invitation != nil && usermsg.Invitation.Step == 3 { - finalizedPeer, _, finalErr := InvitationStep4ProcessStep3(usermsg) + finalizedPeer, finalErr := invmsgs.Step4InviteeFinalizesInitiator(usermsg) if finalErr == nil && finalizedPeer != nil { // Auto-send step-4 confirmation to initiator's servers. - step4msgs, _, sendErr := InvitationStep4Message(finalizedPeer.InvitationId) + step4msgs, sendErr := invsrv.Step4PostConfirmation(finalizedPeer.InvitationId) if sendErr == nil { for i, bytemsg := range step4msgs { if i < len(finalizedPeer.ContactPullServers) { diff --git a/client/helpers/invitationAnswerHelper.go b/client/helpers/invitationAnswerHelper.go deleted file mode 100644 index 996ea62..0000000 --- a/client/helpers/invitationAnswerHelper.go +++ /dev/null @@ -1,110 +0,0 @@ -package helpers - -import ( - "errors" - "os" - "strings" - - "forge.redroom.link/yves/meowlib" - "forge.redroom.link/yves/meowlib/client" -) - -// InvitationStep2Answer creates the invitee's peer from an InvitationInitPayload and returns -// the new peer (STEP_2, invitee side — in-memory, no server involved). -func InvitationStep2Answer(payload *meowlib.InvitationInitPayload, nickname string, myNickname string, serverUids []string) (*client.Peer, string, error) { - mynick := myNickname - if myNickname == "" { - mynick = client.GetConfig().GetIdentity().Nickname - } - peer, err := client.GetConfig().GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload) - if err != nil { - return nil, "InvitationStep2Answer: InvitationStep2", err - } - client.GetConfig().GetIdentity().Save() - return peer, "", nil -} - -// InvitationStep2AnswerFile reads an InvitationInitPayload from a .mwiv file and creates the -// invitee's peer. It also writes the invitee's ContactCard response to a file (STEP_2_SEND, file variant). -func InvitationStep2AnswerFile(invitationFile string, nickname string, myNickname string, serverUids []string) (string, error) { - if _, err := os.Stat(invitationFile); os.IsNotExist(err) { - return "InvitationStep2AnswerFile: os.Stat", err - } - if !strings.HasSuffix(invitationFile, ".mwiv") { - return "InvitationStep2AnswerFile: unsupported format", errors.New("only .mwiv files are supported") - } - data, err := os.ReadFile(invitationFile) - if err != nil { - return "InvitationStep2AnswerFile: os.ReadFile", err - } - payload, err := meowlib.NewInvitationInitPayloadFromCompressed(data) - if err != nil { - return "InvitationStep2AnswerFile: NewInvitationInitPayloadFromCompressed", err - } - - mynick := myNickname - if myNickname == "" { - mynick = client.GetConfig().GetIdentity().Nickname - } - c := client.GetConfig() - response, err := c.GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload) - if err != nil { - return "InvitationStep2AnswerFile: InvitationStep2", err - } - - filename := c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv" - if err := response.GetMyContact().WriteCompressed(filename); err != nil { - return "InvitationStep2AnswerFile: WriteCompressed", err - } - c.GetIdentity().Save() - return "", nil -} - -// InvitationStep2AnswerMessage builds and returns the packed server message that posts the -// invitee's ContactCard (encrypted with the initiator's temp key) to the invitation server -// (STEP_2_SEND, through-server variant). -func InvitationStep2AnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) { - peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId) - if peer == nil { - return nil, "InvitationStep2AnswerMessage: peer not found", errors.New("no peer with that invitation id") - } - - answermsg, err := peer.BuildInvitationStep2Message(peer.GetMyContact()) - if err != nil { - return nil, "InvitationStep2AnswerMessage: BuildInvitationStep2Message", err - } - - invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) - if err != nil { - return nil, "InvitationStep2AnswerMessage: LoadServer", err - } - - packedMsg, err := peer.ProcessOutboundUserMessage(answermsg) - if err != nil { - return nil, "InvitationStep2AnswerMessage: ProcessOutboundUserMessage", err - } - - toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout) - if err != nil { - return nil, "InvitationStep2AnswerMessage: BuildToServerMessageInvitationAnswer", err - } - - bytemsg, err := invitationServer.ProcessOutboundMessage(toServerMessage) - if err != nil { - return nil, "InvitationStep2AnswerMessage: ProcessOutboundMessage", err - } - return bytemsg, "", nil -} - -// InvitationStep2AnswerMessageReadResponse reads the server acknowledgement of a Step2 answer. -func InvitationStep2AnswerMessageReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.Invitation, string, error) { - server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) - if err != nil { - return nil, "InvitationStep2AnswerMessageReadResponse: LoadServer", err - } - serverMsg, err := server.ProcessInboundServerResponse(invitationData) - if err != nil { - return nil, "InvitationStep2AnswerMessageReadResponse: ProcessInboundServerResponse", err - } - return serverMsg.Invitation, "", nil -} diff --git a/client/helpers/invitationCheckHelper.go b/client/helpers/invitationCheckHelper.go deleted file mode 100644 index 7e47b0b..0000000 --- a/client/helpers/invitationCheckHelper.go +++ /dev/null @@ -1,70 +0,0 @@ -package helpers - -import ( - "strings" - - "forge.redroom.link/yves/meowlib" - "forge.redroom.link/yves/meowlib/client" -) - -// InvitationStep2GetMessage builds and returns the packed server message that retrieves -// the InvitationInitPayload from the server using the shortcode URL (STEP_2, invitee side). -func InvitationStep2GetMessage(invitationUrl string, serverPublicKey string, invitationPassword string) ([]byte, string, error) { - meowurl := strings.Split(invitationUrl, "?") - shortcode := meowurl[1] - - srv, err := client.CreateServerFromMeowUrl(meowurl[0]) - if err != nil { - return nil, "InvitationStep2GetMessage: CreateServerFromMeowUrl", err - } - - // Reuse the server entry if already known. - dbsrv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srv.Url) - if err != nil { - return nil, "InvitationStep2GetMessage: LoadServer", err - } - if dbsrv == nil { - srv.PublicKey = serverPublicKey - k, err := meowlib.NewKeyPair() - if err != nil { - return nil, "InvitationStep2GetMessage: NewKeyPair", err - } - srv.UserKp = k - if err := client.GetConfig().GetIdentity().MessageServers.StoreServer(srv); err != nil { - return nil, "InvitationStep2GetMessage: StoreServer", err - } - } else { - if dbsrv.PublicKey != serverPublicKey { - dbsrv.PublicKey = serverPublicKey - } - srv = dbsrv - } - - toSrvMsg, err := srv.BuildToServerMessageInvitationRequest(shortcode, invitationPassword) - if err != nil { - return nil, "InvitationStep2GetMessage: BuildToServerMessageInvitationRequest", err - } - bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg) - if err != nil { - return nil, "InvitationStep2GetMessage: ProcessOutboundMessage", err - } - return bytemsg, "", nil -} - -// InvitationStep2ReadResponse decodes the server response to a Step2 get-message and returns -// the InvitationInitPayload sent by the initiator. -func InvitationStep2ReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.InvitationInitPayload, string, error) { - server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) - if err != nil { - return nil, "InvitationStep2ReadResponse: LoadServer", err - } - serverMsg, err := server.ProcessInboundServerResponse(invitationData) - if err != nil { - return nil, "InvitationStep2ReadResponse: ProcessInboundServerResponse", err - } - payload, err := meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload) - if err != nil { - return nil, "InvitationStep2ReadResponse: NewInvitationInitPayloadFromCompressed", err - } - return payload, "", nil -} diff --git a/client/helpers/invitationCreateHelper.go b/client/helpers/invitationCreateHelper.go deleted file mode 100644 index ac4da6e..0000000 --- a/client/helpers/invitationCreateHelper.go +++ /dev/null @@ -1,102 +0,0 @@ -package helpers - -import ( - "os" - "time" - - "forge.redroom.link/yves/meowlib" - "forge.redroom.link/yves/meowlib/client" -) - -// InvitationStep1CreatePeer creates a minimal pending peer and returns the InvitationInitPayload -// to be transmitted to the invitee (STEP_1). -func InvitationStep1CreatePeer(contactName string, myNickname string, invitationMessage string, serverUids []string) (*meowlib.InvitationInitPayload, *client.Peer, string, error) { - mynick := myNickname - if myNickname == "" { - mynick = client.GetConfig().GetIdentity().Nickname - } - payload, peer, err := client.GetConfig().GetIdentity().InvitationStep1(mynick, contactName, serverUids, invitationMessage) - if err != nil { - return nil, nil, "InvitationStep1CreatePeer: InvitationStep1", err - } - client.GetConfig().GetIdentity().Save() - return payload, peer, "", nil -} - -// InvitationStep1File creates a pending peer and writes the InvitationInitPayload to a file -// (format: "qr" for QR-code PNG, anything else for compressed binary .mwiv). -func InvitationStep1File(contactName string, myNickname string, invitationMessage string, serverUids []string, format string) (*client.Peer, string, error) { - payload, peer, errdata, err := InvitationStep1CreatePeer(contactName, myNickname, invitationMessage, serverUids) - if err != nil { - return nil, errdata, 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, "InvitationStep1File: WriteQr", err - } - } else { - filename := c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".mwiv" - if err := payload.WriteCompressed(filename); err != nil { - return nil, "InvitationStep1File: WriteCompressed", err - } - } - return peer, "", nil -} - -// InvitationStep1Message builds and returns the packed server message that posts the -// InvitationInitPayload to the invitation server (STEP_1 through-server variant). -func InvitationStep1Message(invitationId string, invitationServerUid string, timeOut int, urlLen int, password string) ([]byte, string, error) { - peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId) - if peer == nil { - return nil, "InvitationStep1Message: peer not found", nil - } - if peer.InvitationKp == nil { - return nil, "InvitationStep1Message: peer has no InvitationKp", 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, "InvitationStep1Message: LoadServer", err - } - msg, err := invitationServer.BuildToServerMessageInvitationStep1(initPayload, password, timeOut, urlLen) - if err != nil { - return nil, "InvitationStep1Message: BuildToServerMessageInvitationStep1", err - } - bytemsg, err := invitationServer.ProcessOutboundMessage(msg) - if err != nil { - return nil, "InvitationStep1Message: ProcessOutboundMessage", err - } - return bytemsg, "", nil -} - -// InvitationStep1ReadResponse reads the server response to a Step1 message (shortcode URL + expiry). -func InvitationStep1ReadResponse(invitationServerUid string, invitationResponse []byte) (*meowlib.Invitation, string, error) { - server, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) - if err != nil { - return nil, "InvitationStep1ReadResponse: LoadServer", err - } - serverMsg, err := server.ProcessInboundServerResponse(invitationResponse) - if err != nil { - return nil, "InvitationStep1ReadResponse: ProcessInboundServerResponse", err - } - return serverMsg.Invitation, "", nil -} - -// InvitationSetUrlInfo stores the shortcode URL and expiry on the pending peer. -func InvitationSetUrlInfo(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) -} diff --git a/client/helpers/invitationFinalizeHelper.go b/client/helpers/invitationFinalizeHelper.go deleted file mode 100644 index 7ae821e..0000000 --- a/client/helpers/invitationFinalizeHelper.go +++ /dev/null @@ -1,150 +0,0 @@ -package helpers - -import ( - "errors" - - "forge.redroom.link/yves/meowlib" - "forge.redroom.link/yves/meowlib/client" - "google.golang.org/protobuf/proto" -) - -// InvitationStep3ProcessAnswer is called by the initiator's background service when a -// step-2 answer (invitee's ContactCard) arrives via the invitation server poll. -// It decrypts the answer, calls InvitationStep3 to generate the initiator's full keypairs, -// and returns the peer and the initiator's ContactCard ready for STEP_3_SEND. -func InvitationStep3ProcessAnswer(invitation *meowlib.Invitation) (*client.Peer, *meowlib.ContactCard, string, error) { - var invitationAnswer meowlib.PackedUserMessage - if err := proto.Unmarshal(invitation.Payload, &invitationAnswer); err != nil { - return nil, nil, "InvitationStep3ProcessAnswer: Unmarshal PackedUserMessage", err - } - - peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitation.Uuid) - if peer == nil { - return nil, nil, "InvitationStep3ProcessAnswer: peer not found", 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 - } - - // Decrypt invitee's ContactCard using the initiator's temporary InvitationKp. - usermsg, err := peer.ProcessInboundStep2UserMessage(&invitationAnswer, invitation.From) - if err != nil { - return nil, nil, "InvitationStep3ProcessAnswer: ProcessInboundStep2UserMessage", err - } - - var inviteeCC meowlib.ContactCard - if err := proto.Unmarshal(usermsg.Invitation.Payload, &inviteeCC); err != nil { - return nil, nil, "InvitationStep3ProcessAnswer: Unmarshal ContactCard", err - } - - myCC, peer, err := client.GetConfig().GetIdentity().InvitationStep3(&inviteeCC) - if err != nil { - return nil, nil, "InvitationStep3ProcessAnswer: InvitationStep3", err - } - client.GetConfig().GetIdentity().Save() - return peer, myCC, "", nil -} - -// InvitationStep3Message builds and returns the packed server messages that send the -// initiator's full ContactCard to the invitee through the invitee's servers (STEP_3_SEND). -func InvitationStep3Message(invitationId string) ([][]byte, string, error) { - id := client.GetConfig().GetIdentity() - peer := id.Peers.GetFromInvitationId(invitationId) - if peer == nil { - return nil, "InvitationStep3Message: peer not found", errors.New("no peer for invitation id " + invitationId) - } - - step3msg, err := peer.BuildInvitationStep3Message(peer.GetMyContact()) - if err != nil { - return nil, "InvitationStep3Message: BuildInvitationStep3Message", err - } - // Step-3 must NOT use DR or sym layers: the invitee hasn't received those - // keys yet (they are carried inside this very message). Use plain asym only. - serialized, err := peer.SerializeUserMessage(step3msg) - if err != nil { - return nil, "InvitationStep3Message: SerializeUserMessage", err - } - enc, err := peer.AsymEncryptMessage(serialized) - if err != nil { - return nil, "InvitationStep3Message: AsymEncryptMessage", 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, "InvitationStep3Message: no reachable invitee server", errors.New("could not build message for any invitee server") - } - return results, "", nil -} - -// InvitationStep4ProcessStep3 is called by the invitee's message processing when a UserMessage -// with invitation.step==3 is received. It finalizes the initiator's peer entry. -func InvitationStep4ProcessStep3(usermsg *meowlib.UserMessage) (*client.Peer, string, error) { - if usermsg.Invitation == nil || usermsg.Invitation.Step != 3 { - return nil, "InvitationStep4ProcessStep3: unexpected step", errors.New("expected invitation step 3") - } - var initiatorCC meowlib.ContactCard - if err := proto.Unmarshal(usermsg.Invitation.Payload, &initiatorCC); err != nil { - return nil, "InvitationStep4ProcessStep3: Unmarshal ContactCard", 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, "InvitationStep4ProcessStep3: InvitationStep4", err - } - client.GetConfig().GetIdentity().Save() - peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(initiatorCC.InvitationId) - return peer, "", nil -} - -// InvitationStep4Message builds and returns the packed server messages that send the -// invitee's confirmation to the initiator through the initiator's servers (STEP_4). -func InvitationStep4Message(invitationId string) ([][]byte, string, error) { - id := client.GetConfig().GetIdentity() - peer := id.Peers.GetFromInvitationId(invitationId) - if peer == nil { - return nil, "InvitationStep4Message: peer not found", errors.New("no peer for invitation id " + invitationId) - } - - step4msg, err := peer.BuildInvitationStep4Message() - if err != nil { - return nil, "InvitationStep4Message: BuildInvitationStep4Message", err - } - packedMsg, err := peer.ProcessOutboundUserMessage(step4msg) - if err != nil { - return nil, "InvitationStep4Message: ProcessOutboundUserMessage", 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, "InvitationStep4Message: no reachable initiator server", errors.New("could not build message for any initiator server") - } - return results, "", nil -} diff --git a/client/invitation/files/step1.go b/client/invitation/files/step1.go new file mode 100644 index 0000000..ba8ef0f --- /dev/null +++ b/client/invitation/files/step1.go @@ -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 +} diff --git a/client/invitation/files/step2.go b/client/invitation/files/step2.go new file mode 100644 index 0000000..96e713c --- /dev/null +++ b/client/invitation/files/step2.go @@ -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) +} diff --git a/client/invitation/messages/step1.go b/client/invitation/messages/step1.go new file mode 100644 index 0000000..9933081 --- /dev/null +++ b/client/invitation/messages/step1.go @@ -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 +} diff --git a/client/invitation/messages/step2.go b/client/invitation/messages/step2.go new file mode 100644 index 0000000..a4972b7 --- /dev/null +++ b/client/invitation/messages/step2.go @@ -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 +} diff --git a/client/invitation/messages/step3.go b/client/invitation/messages/step3.go new file mode 100644 index 0000000..53d9602 --- /dev/null +++ b/client/invitation/messages/step3.go @@ -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 +} diff --git a/client/invitation/messages/step4.go b/client/invitation/messages/step4.go new file mode 100644 index 0000000..1d8e529 --- /dev/null +++ b/client/invitation/messages/step4.go @@ -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 +} diff --git a/client/invitation/server/step1.go b/client/invitation/server/step1.go new file mode 100644 index 0000000..12a6c9f --- /dev/null +++ b/client/invitation/server/step1.go @@ -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) +} diff --git a/client/invitation/server/step2.go b/client/invitation/server/step2.go new file mode 100644 index 0000000..5fcfb22 --- /dev/null +++ b/client/invitation/server/step2.go @@ -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 +} diff --git a/client/invitation/server/step3.go b/client/invitation/server/step3.go new file mode 100644 index 0000000..8156009 --- /dev/null +++ b/client/invitation/server/step3.go @@ -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 +} diff --git a/client/invitation/server/step4.go b/client/invitation/server/step4.go new file mode 100644 index 0000000..160e295 --- /dev/null +++ b/client/invitation/server/step4.go @@ -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 +}