diff --git a/Unnamed.FCStd b/Unnamed.FCStd new file mode 100644 index 0000000..73b4f67 Binary files /dev/null and b/Unnamed.FCStd differ 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 +}