Compare commits
2 Commits
1906431061
...
327bd390c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 327bd390c4 | |||
| 793213b3fb |
Binary file not shown.
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
Comment: https://gopenpgp.org
|
||||
Version: GopenPGP 2.8.3
|
||||
|
||||
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
|
||||
=C05v
|
||||
-----END PGP MESSAGE-----
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Step2ReadAndAnswer reads an InvitationInitPayload from a .mwiv file, creates the
|
||||
// invitee's peer entry, and writes the encrypted ContactCard (PackedUserMessage) to a
|
||||
// .mwiv file for the initiator to pick up and process in step 3.
|
||||
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
|
||||
}
|
||||
|
||||
packed, peer, err := messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload, nickname, mynick, serverUids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wrap the PackedUserMessage in an Invitation so the initiator (step3) has the
|
||||
// invitee's public key available for signature verification without an extra file.
|
||||
packedBytes, err := proto.Marshal(packed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invitation := &meowlib.Invitation{
|
||||
Uuid: peer.InvitationId,
|
||||
Step: 2,
|
||||
From: peer.MyIdentity.Public,
|
||||
Payload: packedBytes,
|
||||
}
|
||||
out, err := proto.Marshal(invitation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := client.GetConfig()
|
||||
filename := c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv"
|
||||
return os.WriteFile(filename, out, 0600)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package messages_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"forge.redroom.link/yves/meowlib/client/invitation/messages"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// setupIdentity creates a fresh identity and sets it as the active config identity.
|
||||
// Returns the identity and a cleanup function.
|
||||
func setupIdentity(t *testing.T, nickname string) (*client.Identity, func()) {
|
||||
t.Helper()
|
||||
cfg := client.GetConfig()
|
||||
cfg.SetMemPass("testpass") //nolint:errcheck
|
||||
|
||||
id, err := client.CreateIdentity(nickname)
|
||||
require.NoError(t, err)
|
||||
cfg.SetIdentity(id)
|
||||
|
||||
cleanup := func() {
|
||||
os.RemoveAll(cfg.StoragePath + "/" + id.Uuid)
|
||||
}
|
||||
return id, cleanup
|
||||
}
|
||||
|
||||
// TestStep2ProducesPackedUserMessage verifies that Step2 returns a PackedUserMessage
|
||||
// (not just a peer) and that the message is encrypted with the initiator's temp key
|
||||
// so Step3 can decrypt it.
|
||||
func TestStep2ProducesPackedUserMessage(t *testing.T) {
|
||||
cfg := client.GetConfig()
|
||||
cfg.SetMemPass("testpass") //nolint:errcheck
|
||||
|
||||
// --- STEP 1: initiator creates temp keypair and payload ---
|
||||
initiator, cleanInit := setupIdentity(t, "alice")
|
||||
defer cleanInit()
|
||||
|
||||
payload, initPeer, err := messages.Step1InitiatorCreatesInviteeAndTempKey("Bob", "Alice", "Hello!", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, payload)
|
||||
require.NotNil(t, initPeer)
|
||||
// Initiator has only the temp keypair at this stage.
|
||||
assert.Nil(t, initPeer.MyIdentity)
|
||||
assert.NotNil(t, initPeer.InvitationKp)
|
||||
|
||||
// --- STEP 2: invitee receives payload, creates peer, returns packed message ---
|
||||
_, cleanInvitee := setupIdentity(t, "bob")
|
||||
defer cleanInvitee()
|
||||
|
||||
packed, inviteePeer, err := messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard(
|
||||
payload, "Alice", "Bob", nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, packed, "step2 must return a PackedUserMessage, not just a peer")
|
||||
require.NotNil(t, inviteePeer)
|
||||
|
||||
// The packed message destination is the initiator's temp key (used as lookup key).
|
||||
assert.Equal(t, payload.PublicKey, packed.Destination)
|
||||
|
||||
// The invitee peer has full keypairs now.
|
||||
assert.NotNil(t, inviteePeer.MyIdentity)
|
||||
assert.NotNil(t, inviteePeer.MyEncryptionKp)
|
||||
assert.NotNil(t, inviteePeer.MyLookupKp)
|
||||
|
||||
// --- STEP 3: initiator decrypts invitee's packed message and finalises ---
|
||||
cfg.SetIdentity(initiator)
|
||||
|
||||
// Simulate how the server delivers the step-2 answer: marshal the PackedUserMessage
|
||||
// into an Invitation.Payload.
|
||||
packedBytes, err := proto.Marshal(packed)
|
||||
require.NoError(t, err)
|
||||
|
||||
invitation := &meowlib.Invitation{
|
||||
Uuid: payload.Uuid,
|
||||
Step: 2,
|
||||
From: inviteePeer.MyIdentity.Public,
|
||||
Payload: packedBytes,
|
||||
}
|
||||
|
||||
peer, myCC, err := messages.Step3InitiatorFinalizesInviteeAndCreatesContactCard(invitation)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, peer)
|
||||
require.NotNil(t, myCC, "step3 must produce the initiator's ContactCard to send to invitee")
|
||||
|
||||
// Initiator's peer must now hold invitee's real keys.
|
||||
assert.Equal(t, inviteePeer.MyIdentity.Public, peer.ContactPublicKey)
|
||||
assert.Equal(t, inviteePeer.MyEncryptionKp.Public, peer.ContactEncryption)
|
||||
assert.Equal(t, inviteePeer.MyLookupKp.Public, peer.ContactLookupKey)
|
||||
assert.Nil(t, peer.InvitationKp, "temp keypair must be cleared after step3")
|
||||
assert.NotEmpty(t, myCC.DrRootKey)
|
||||
assert.NotEmpty(t, myCC.DrPublicKey)
|
||||
}
|
||||
|
||||
// TestStep2Step3RoundTripPayload verifies that the PackedUserMessage produced by step2
|
||||
// actually carries the invitee's ContactCard when decrypted by the initiator.
|
||||
func TestStep2Step3RoundTripPayload(t *testing.T) {
|
||||
cfg := client.GetConfig()
|
||||
cfg.SetMemPass("testpass") //nolint:errcheck
|
||||
|
||||
initiator, cleanInit := setupIdentity(t, "alice2")
|
||||
defer cleanInit()
|
||||
|
||||
payload, _, err := messages.Step1InitiatorCreatesInviteeAndTempKey("Bob", "Alice", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, cleanInvitee := setupIdentity(t, "bob2")
|
||||
defer cleanInvitee()
|
||||
|
||||
packed, inviteePeer, err := messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload, "Alice", "Bob", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Confirm the message serialises cleanly (transport simulation).
|
||||
packedBytes, err := proto.Marshal(packed)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, packedBytes)
|
||||
|
||||
// Switch back to initiator and run step3.
|
||||
cfg.SetIdentity(initiator)
|
||||
|
||||
var roundTripped meowlib.PackedUserMessage
|
||||
require.NoError(t, proto.Unmarshal(packedBytes, &roundTripped))
|
||||
|
||||
invitation := &meowlib.Invitation{
|
||||
Uuid: payload.Uuid,
|
||||
Step: 2,
|
||||
From: inviteePeer.MyIdentity.Public,
|
||||
Payload: packedBytes,
|
||||
}
|
||||
|
||||
_, myCC, err := messages.Step3InitiatorFinalizesInviteeAndCreatesContactCard(invitation)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, myCC)
|
||||
|
||||
// The initiator's CC must reference the invitee's invitation ID so the invitee
|
||||
// can match it when step4 arrives.
|
||||
assert.Equal(t, payload.Uuid, myCC.InvitationId)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
// Step2InviteeCreatesInitiatorAndEncryptedContactCard creates the invitee's peer entry
|
||||
// from an InvitationInitPayload, then builds the invitee's ContactCard and returns it
|
||||
// as a PackedUserMessage asymmetrically encrypted with the initiator's temporary public
|
||||
// key. The packed message is ready to be transmitted to the initiator via any transport
|
||||
// (file, QR, server…); Step3InitiatorFinalizesInviteeAndCreatesContactCard on the
|
||||
// initiator side will decrypt and process it.
|
||||
func Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload *meowlib.InvitationInitPayload, nickname string, myNickname string, serverUids []string) (*meowlib.PackedUserMessage, *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, nil, err
|
||||
}
|
||||
usermsg, err := peer.BuildInvitationStep2Message(peer.GetMyContact())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
packed, err := peer.ProcessOutboundUserMessage(usermsg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
return packed, peer, nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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 wraps the invitee's already-built PackedUserMessage into a server
|
||||
// message and posts it to the invitation server. The packed message is produced by
|
||||
// messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard.
|
||||
func Step2PostAnswer(invitationId string, packedMsg *meowlib.PackedUserMessage, 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")
|
||||
}
|
||||
|
||||
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+2
-2
@@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
doubleratchet "github.com/status-im/doubleratchet"
|
||||
"github.com/google/uuid"
|
||||
doubleratchet "github.com/status-im/doubleratchet"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ type Peer struct {
|
||||
// Invitation temporary keypair (step 1 only — discarded after step 3)
|
||||
InvitationKp *meowlib.KeyPair `json:"invitation_kp,omitempty"`
|
||||
// Double Ratchet state
|
||||
DrKpPublic string `json:"dr_kp_public,omitempty"`
|
||||
DrKpPublic string `json:"dr_kp_public,omitempty"`
|
||||
DrKpPrivate string `json:"dr_kp_private,omitempty"`
|
||||
DrRootKey string `json:"dr_root_key,omitempty"`
|
||||
DrInitiator bool `json:"dr_initiator,omitempty"`
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
Comment: https://gopenpgp.org
|
||||
Version: GopenPGP 2.8.3
|
||||
|
||||
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
|
||||
=BI6k
|
||||
-----END PGP MESSAGE-----
|
||||
Reference in New Issue
Block a user