This commit is contained in:
@@ -129,9 +129,20 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: Unmarshal FromServerMessage", err
|
||||
}
|
||||
// check if invitation answer
|
||||
// check if invitation answer (step-2 answer waiting for the initiator)
|
||||
if fromServerMessage.Invitation != nil {
|
||||
invitationGetAnswerReadResponse(fromServerMessage.Invitation)
|
||||
peer, _, _, invErr := InvitationStep3ProcessAnswer(fromServerMessage.Invitation)
|
||||
if invErr == nil && peer != nil {
|
||||
// Auto-send step-3 CC to invitee's servers.
|
||||
msgs, _, sendErr := InvitationStep3Message(peer.InvitationId)
|
||||
if sendErr == nil {
|
||||
for i, bytemsg := range msgs {
|
||||
if i < len(peer.ContactPullServers) {
|
||||
meowlib.HttpPostMessage(peer.ContactPullServers[i], bytemsg, client.GetConfig().HttpTimeOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Chat messages
|
||||
if len(fromServerMessage.Chat) > 0 {
|
||||
@@ -142,12 +153,41 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error
|
||||
if peer == nil {
|
||||
return nil, nil, "ReadMessage: GetFromMyLookupKey", errors.New("no visible peer for that message")
|
||||
}
|
||||
// Unpack the message
|
||||
usermsg, err := peer.ProcessInboundUserMessage(packedUserMessage)
|
||||
// Unpack the message — step-3 messages arrive before the initiator's identity
|
||||
// key is known, so skip signature verification for pending peers.
|
||||
var usermsg *meowlib.UserMessage
|
||||
if peer.InvitationPending() {
|
||||
usermsg, err = peer.ProcessInboundStep3UserMessage(packedUserMessage)
|
||||
} else {
|
||||
usermsg, err = peer.ProcessInboundUserMessage(packedUserMessage)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, "ReadMessage: ProcessInboundUserMessage", err
|
||||
}
|
||||
|
||||
// Handle invitation step 3: initiator's full ContactCard arriving at the invitee.
|
||||
if usermsg.Invitation != nil && usermsg.Invitation.Step == 3 {
|
||||
finalizedPeer, _, finalErr := InvitationStep4ProcessStep3(usermsg)
|
||||
if finalErr == nil && finalizedPeer != nil {
|
||||
// Auto-send step-4 confirmation to initiator's servers.
|
||||
step4msgs, _, sendErr := InvitationStep4Message(finalizedPeer.InvitationId)
|
||||
if sendErr == nil {
|
||||
for i, bytemsg := range step4msgs {
|
||||
if i < len(finalizedPeer.ContactPullServers) {
|
||||
meowlib.HttpPostMessage(finalizedPeer.ContactPullServers[i], bytemsg, client.GetConfig().HttpTimeOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle invitation step 4: invitee's confirmation arriving at the initiator.
|
||||
if usermsg.Invitation != nil && usermsg.Invitation.Step == 4 {
|
||||
// Contact is fully active — nothing more to do on the initiator side.
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for received or processed already filled => it's an ack for one of our sent messages
|
||||
if len(usermsg.Data) == 0 && usermsg.Status != nil && usermsg.Status.Uuid != "" &&
|
||||
(usermsg.Status.Received != 0 || usermsg.Status.Processed != 0) {
|
||||
|
||||
@@ -1,161 +1,110 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"C"
|
||||
"fmt"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// InvitationAnswer
|
||||
func InvitationAnswer(cc *meowlib.ContactCard, nickname string, myNickname string, serverUids []string) (*client.Peer, string, error) {
|
||||
|
||||
// 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
|
||||
// my nickname for that contact
|
||||
|
||||
if myNickname == "" {
|
||||
mynick = client.GetConfig().GetIdentity().Nickname
|
||||
}
|
||||
|
||||
// build my contact card for that friend
|
||||
peer, err := client.GetConfig().GetIdentity().AnswerInvitation(mynick, nickname, serverUids, cc)
|
||||
peer, err := client.GetConfig().GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload)
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswer: AnswerInvitation", err
|
||||
return nil, "InvitationStep2Answer: InvitationStep2", err
|
||||
}
|
||||
|
||||
//peerstr, err := json.Marshal(peer)
|
||||
//fmt.Println("InvitationAnswer: " + string(peerstr))
|
||||
c := client.GetConfig()
|
||||
c.GetIdentity().Save()
|
||||
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
return peer, "", nil
|
||||
}
|
||||
|
||||
// InvitationAnswerFile
|
||||
func InvitationAnswerFile(invitationFile string, nickname string, myNickname string, serverUids []string) (string, error) {
|
||||
format := "qr"
|
||||
var filename string = ""
|
||||
var cc *meowlib.ContactCard
|
||||
c := client.GetConfig()
|
||||
// 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 "InvitationAnswerFile : os.Stat", err
|
||||
return "InvitationStep2AnswerFile: os.Stat", err
|
||||
}
|
||||
if strings.HasSuffix(invitationFile, ".mwiv") {
|
||||
format = "mwiv"
|
||||
data, err := os.ReadFile(invitationFile)
|
||||
if err != nil {
|
||||
return "InvitationAnswerFile : os.ReadFile", err
|
||||
}
|
||||
cc, err = meowlib.NewContactCardFromCompressed(data)
|
||||
if err != nil {
|
||||
return "InvitationAnswerFile : NewContactCardFromCompressed", err
|
||||
}
|
||||
if !strings.HasSuffix(invitationFile, ".mwiv") {
|
||||
return "InvitationStep2AnswerFile: unsupported format", errors.New("only .mwiv files are supported")
|
||||
}
|
||||
identity := client.GetConfig().GetIdentity()
|
||||
if cc != nil {
|
||||
isAnswer, proposed, received, _ := identity.CheckInvitation(cc)
|
||||
if isAnswer {
|
||||
fmt.Fprintln(os.Stdout, "This is already a response "+proposed+" to your invitation.")
|
||||
fmt.Fprintln(os.Stdout, "You cannot answer again.")
|
||||
fmt.Fprintln(os.Stdout, "You should finalize it by importing "+proposed+" contact card to your meow.")
|
||||
fmt.Fprintln(os.Stdout, "Use : 'meow invitation finalize "+invitationFile+"' to do it.")
|
||||
|
||||
} else {
|
||||
mynick := myNickname
|
||||
// my nickname for that contact
|
||||
|
||||
if myNickname == "" {
|
||||
mynick = client.GetConfig().GetIdentity().Nickname
|
||||
}
|
||||
|
||||
response, err := identity.AnswerInvitation(mynick, nickname, serverUids, cc)
|
||||
if err != nil {
|
||||
return "InvitationAnswerFile : AnswerInvitation", err
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, "Invitation sent by "+received)
|
||||
if format == "qr" {
|
||||
filename = c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".png"
|
||||
response.GetMyContact().WriteQr(filename)
|
||||
} else {
|
||||
filename = c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv"
|
||||
response.GetMyContact().WriteCompressed(filename)
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// InvitationAnswerMessage
|
||||
func InvitationAnswerMessage(invitationId string, invitationServerUid string, timeout int) ([]byte, string, error) {
|
||||
|
||||
// find the peer with that invitation id
|
||||
/*var peer *client.Peer
|
||||
for i := len(client.GetConfig().GetIdentity().Peers) - 1; i >= 0; i-- { //! to allow self invitation : testing only, findinc the received peer before myself
|
||||
// for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
|
||||
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
|
||||
peer = client.GetConfig().GetIdentity().Peers[i]
|
||||
break
|
||||
}
|
||||
}*/
|
||||
// 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 {
|
||||
// declare a custom go error for no peer found
|
||||
return nil, "InvitationAnswerMessage: loop for peer", errors.New("no peer with that invitation id")
|
||||
return nil, "InvitationStep2AnswerMessage: peer not found", errors.New("no peer with that invitation id")
|
||||
}
|
||||
answermsg, err := peer.BuildInvitationAnswerMessage(peer.GetMyContact())
|
||||
|
||||
answermsg, err := peer.BuildInvitationStep2Message(peer.GetMyContact())
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswerMessage: BuildInvitationAnswserMessage", err
|
||||
return nil, "InvitationStep2AnswerMessage: BuildInvitationStep2Message", err
|
||||
}
|
||||
// Server: get the invitation server
|
||||
|
||||
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswerMessage: LoadServer", err
|
||||
return nil, "InvitationStep2AnswerMessage: LoadServer", err
|
||||
}
|
||||
|
||||
// this will be the invitation's payload
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(answermsg)
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswerMessage: ProcessOutboundUserMessage", err
|
||||
}
|
||||
// Creating Server message for transporting the user message
|
||||
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswerMessage: BuildToServerMessageInvitationAnswer", err
|
||||
return nil, "InvitationStep2AnswerMessage: ProcessOutboundUserMessage", err
|
||||
}
|
||||
|
||||
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
|
||||
if err != nil {
|
||||
return nil, "InvitationStep2AnswerMessage: BuildToServerMessageInvitationAnswer", err
|
||||
}
|
||||
|
||||
// Server outbound processing
|
||||
bytemsg, err := invitationServer.ProcessOutboundMessage(toServerMessage)
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswerMessage: ProcessOutboundMessage", err
|
||||
return nil, "InvitationStep2AnswerMessage: ProcessOutboundMessage", err
|
||||
}
|
||||
return bytemsg, "", nil
|
||||
}
|
||||
|
||||
// InvitationAnswerMessageReadResponse
|
||||
// Called by the invitation receiver
|
||||
// invitationData: the data received from the server
|
||||
// invitationServerUid: the uid of the server holding the invitation
|
||||
func InvitationAnswerMessageReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.Invitation, string, error) {
|
||||
|
||||
// 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, "InvitationAnswerMessageReadResponse: LoadServer", err
|
||||
return nil, "InvitationStep2AnswerMessageReadResponse: LoadServer", err
|
||||
}
|
||||
// Server inbound processing : get the invitation server
|
||||
serverMsg, err := server.ProcessInboundServerResponse(invitationData)
|
||||
if err != nil {
|
||||
return nil, "InvitationAnswerMessageReadResponse: ProcessInboundServerResponse", err
|
||||
return nil, "InvitationStep2AnswerMessageReadResponse: ProcessInboundServerResponse", err
|
||||
}
|
||||
|
||||
return serverMsg.Invitation, "", nil
|
||||
|
||||
}
|
||||
|
||||
@@ -7,84 +7,31 @@ import (
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
// InvitationCheck
|
||||
// todo
|
||||
/*
|
||||
func InvitationCheck(invitationdata []byte) *C.char {
|
||||
var jsoninv map[string]interface{}
|
||||
err := json.Unmarshal([]byte(C.GoString(invitationdata)), &jsoninv)
|
||||
if err != nil {
|
||||
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||
}
|
||||
var cc *meowlib.ContactCard
|
||||
if _, err := os.Stat(jsoninv["filename"].(string)); os.IsNotExist(err) {
|
||||
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||
}
|
||||
if strings.HasSuffix(jsoninv["filename"].(string), ".mwiv") {
|
||||
data, err := os.ReadFile(jsoninv["filename"].(string))
|
||||
if err != nil {
|
||||
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||
}
|
||||
cc, err = meowlib.NewContactCardFromCompressed(data)
|
||||
if err != nil {
|
||||
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||
}
|
||||
}
|
||||
identity := client.GetConfig().GetIdentity()
|
||||
result := map[string]string{}
|
||||
if cc != nil {
|
||||
isAnswer, proposed, received, invitationMessage := identity.CheckInvitation(cc)
|
||||
if isAnswer { // answer to infitation
|
||||
result["type"] = "answer"
|
||||
result["to"] = proposed
|
||||
result["from"] = received
|
||||
result["invitation_message"] = invitationMessage
|
||||
//fmt.Fprintln(os.Stdout, "Invitation sent to "+proposed+" received with "+received+" as suggested nickname")
|
||||
} else { // finalization message
|
||||
result["type"] = "finalize"
|
||||
result["from"] = received
|
||||
//fmt.Fprintln(os.Stdout, "Invitation sent by "+received)
|
||||
}
|
||||
|
||||
}
|
||||
val, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return C.CString(errorToJson(err, "InvitationCheck: "))
|
||||
}
|
||||
return C.CString(string(val))
|
||||
}*/
|
||||
|
||||
// InvitationGetMessage
|
||||
// Called by the invitation receiver
|
||||
// invitationUrl: the url of server holding the invitation
|
||||
// serverPublicKey: the public key of the server holding the invitation
|
||||
// invitationPassword: the password of the invitation
|
||||
func InvitationGetMessage(invitationUrl string, serverPublicKey string, invitationPassword string) ([]byte, string, error) {
|
||||
|
||||
// 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, "InvitationGetMessage: CreateServerFromMeowUrl", err
|
||||
return nil, "InvitationStep2GetMessage: CreateServerFromMeowUrl", err
|
||||
}
|
||||
// check if already in msg servers
|
||||
|
||||
// Reuse the server entry if already known.
|
||||
dbsrv, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(srv.Url)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessage: LoadServer", err
|
||||
return nil, "InvitationStep2GetMessage: LoadServer", err
|
||||
}
|
||||
if dbsrv == nil {
|
||||
// create a server object with url & pubkey
|
||||
srv.PublicKey = serverPublicKey
|
||||
k, err := meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessage: NewKeyPair", err
|
||||
return nil, "InvitationStep2GetMessage: NewKeyPair", err
|
||||
}
|
||||
srv.UserKp = k
|
||||
// save it
|
||||
err = client.GetConfig().GetIdentity().MessageServers.StoreServer(srv)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessage: StoreServer", err
|
||||
if err := client.GetConfig().GetIdentity().MessageServers.StoreServer(srv); err != nil {
|
||||
return nil, "InvitationStep2GetMessage: StoreServer", err
|
||||
}
|
||||
} else {
|
||||
if dbsrv.PublicKey != serverPublicKey {
|
||||
@@ -92,42 +39,32 @@ func InvitationGetMessage(invitationUrl string, serverPublicKey string, invitati
|
||||
}
|
||||
srv = dbsrv
|
||||
}
|
||||
// buildserver message
|
||||
|
||||
toSrvMsg, err := srv.BuildToServerMessageInvitationRequest(shortcode, invitationPassword)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessage: BuildToServerMessageInvitationRequest", err
|
||||
return nil, "InvitationStep2GetMessage: BuildToServerMessageInvitationRequest", err
|
||||
}
|
||||
// processoutbound
|
||||
bytemsg, err := srv.ProcessOutboundMessage(toSrvMsg)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessage: ProcessOutboundMessage", err
|
||||
return nil, "InvitationStep2GetMessage: ProcessOutboundMessage", err
|
||||
}
|
||||
|
||||
return bytemsg, "", nil
|
||||
}
|
||||
|
||||
// InvitationGetMessageReadResponse
|
||||
// Called by the invitation receiver
|
||||
// invitationData: the data received from the server
|
||||
// invitationServerUid: the uid of the server holding the invitation
|
||||
func InvitationGetMessageReadResponse(invitationData []byte, invitationServerUid string) (*meowlib.ContactCard, string, error) {
|
||||
|
||||
// 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, "InvitationGetMessageReadResponse: LoadServer", err
|
||||
return nil, "InvitationStep2ReadResponse: LoadServer", err
|
||||
}
|
||||
// Server inbound processing : get the invitation server
|
||||
serverMsg, err := server.ProcessInboundServerResponse(invitationData)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessageReadResponse: ProcessInboundServerResponse", err
|
||||
return nil, "InvitationStep2ReadResponse: ProcessInboundServerResponse", err
|
||||
}
|
||||
// fmt.Println("Inbound OK, Invitation Step: ", serverMsg.Invitation.Step, len(serverMsg.Invitation.Payload))
|
||||
// fmt.Println("Invitation Check")
|
||||
// fmt.Println(hex.EncodeToString(serverMsg.Invitation.Payload))
|
||||
// contactCard decode
|
||||
cc, err := meowlib.NewContactCardFromCompressed(serverMsg.Invitation.Payload)
|
||||
payload, err := meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetMessageReadResponse: NewContactCardFromCompressed", err
|
||||
return nil, "InvitationStep2ReadResponse: NewInvitationInitPayloadFromCompressed", err
|
||||
}
|
||||
return cc, "", nil
|
||||
return payload, "", nil
|
||||
}
|
||||
|
||||
@@ -8,136 +8,95 @@ import (
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
)
|
||||
|
||||
// InvitationCreatePeer creates a new peer and returns it
|
||||
// Called by invitation initiator
|
||||
// name: the name of the peer
|
||||
// myNickname: my nickname for that peer
|
||||
// invitationMessage: the message to send to the peer
|
||||
// serverUids: the list of server uids
|
||||
func InvitationCreatePeer(name string, myNickname string, invitationMessage string, serverUids []string) (*client.Peer, string, error) {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// build my contact card for that friend
|
||||
peer, err := client.GetConfig().GetIdentity().InvitePeer(mynick, name, serverUids, invitationMessage)
|
||||
payload, peer, err := client.GetConfig().GetIdentity().InvitationStep1(mynick, contactName, serverUids, invitationMessage)
|
||||
if err != nil {
|
||||
return nil, "InvitationCreate: InvitePeer", err
|
||||
return nil, nil, "InvitationStep1CreatePeer: InvitationStep1", err
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
|
||||
return peer, "", nil
|
||||
return payload, peer, "", nil
|
||||
}
|
||||
|
||||
// InvitationCreateFile creates a new peer and writes the invitation to a file
|
||||
// Called by invitation initiator
|
||||
// name: the name of the peer
|
||||
// myNickname: my nickname for that peer
|
||||
// invitationMessage: the message to send to the peer
|
||||
// serverUids: the list of server uids
|
||||
// format: the format of the file (qr or mwiv)
|
||||
func InvitationCreateFile(name string, myNickname string, invitationMessage string, serverUids []string, format string) (*client.Peer, string, error) {
|
||||
|
||||
peer, errdata, err := InvitationCreatePeer(name, myNickname, invitationMessage, serverUids)
|
||||
// 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()
|
||||
var filename string = ""
|
||||
if format == "qr" {
|
||||
filename = c.StoragePath + string(os.PathSeparator) + peer.MyName + "-" + peer.Name + ".png"
|
||||
err := peer.GetMyContact().WriteQr(filename)
|
||||
if err != nil {
|
||||
return nil, "InvitationCreateFile: WriteQr", err
|
||||
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"
|
||||
err := peer.GetMyContact().WriteCompressed(filename)
|
||||
if err != nil {
|
||||
return nil, "InvitationCreateFile: WriteCompressed", err
|
||||
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
|
||||
}
|
||||
|
||||
// InvitationCreateMessage creates a new invitation message for an invited peer
|
||||
// Called by invitation initiator
|
||||
// invitationId: the invitation id of the peer
|
||||
// invitationServerUid: the uid of the server for sending the invitation
|
||||
// timeOut: the timeout for the invitation
|
||||
// urlLen: the length of the invitation url
|
||||
// password: the password for the invitation
|
||||
func InvitationCreateMessage(invitationId string, invitationServerUid string, timeOut int, urlLen int, password string) ([]byte, string, error) {
|
||||
|
||||
// lookup for peer with "invitation_id"
|
||||
var myContact *meowlib.ContactCard
|
||||
/* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
|
||||
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
|
||||
myContact = client.GetConfig().GetIdentity().Peers[i].GetMyContact()
|
||||
break
|
||||
}
|
||||
}*/
|
||||
// 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)
|
||||
myContact = peer.GetMyContact()
|
||||
// todo handle not found !!
|
||||
// lookup for message server with "invitation_server"
|
||||
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid) //.GetServerByIdx(int(jsoninv["invitation_server"].(float64)))
|
||||
if err != nil {
|
||||
return nil, "InvitationCreateMessage: LoadServer", err
|
||||
if peer == nil {
|
||||
return nil, "InvitationStep1Message: peer not found", nil
|
||||
}
|
||||
// call server.buildinviattion
|
||||
msg, err := invitationServer.BuildToServerMessageInvitationCreation(myContact, password, timeOut, urlLen)
|
||||
if err != nil {
|
||||
return nil, "InvitationCreateMessage: BuildToServerMessageInvitationCreation", err
|
||||
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
|
||||
}
|
||||
// fmt.Println("Invitation Create")
|
||||
// fmt.Println(hex.EncodeToString(msg.Invitation.Payload))
|
||||
bytemsg, err := invitationServer.ProcessOutboundMessage(msg)
|
||||
if err != nil {
|
||||
return nil, "InvitationCreateMessage: ProcessOutboundMessage", err
|
||||
return nil, "InvitationStep1Message: ProcessOutboundMessage", err
|
||||
}
|
||||
return bytemsg, "", nil
|
||||
}
|
||||
|
||||
// InvitationCreateReadResponse reads the response of an invitation creation (url, expiry)
|
||||
// Called by invitation initiator
|
||||
// invitationServerUid: the uid of the server where we sent the invitation
|
||||
// invitationResponse: the response we got from the server
|
||||
func InvitationCreateReadResponse(invitationServerUid string, invitationResponse []byte) (*meowlib.Invitation, string, error) {
|
||||
|
||||
// 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, "InvitationCreateReadResponse: LoadServer", err
|
||||
return nil, "InvitationStep1ReadResponse: LoadServer", err
|
||||
}
|
||||
serverMsg, err := server.ProcessInboundServerResponse(invitationResponse)
|
||||
if err != nil {
|
||||
return nil, "InvitationCreateReadResponse: ProcessInboundServerResponse", err
|
||||
return nil, "InvitationStep1ReadResponse: ProcessInboundServerResponse", err
|
||||
}
|
||||
|
||||
return serverMsg.Invitation, "", nil
|
||||
}
|
||||
|
||||
// InvitationSetUrlInfo sets the url info for an invitation
|
||||
// Called by invitation initiator
|
||||
// invitationId: the invitation id of the peer
|
||||
// url: the url of the invitation we got from the server
|
||||
// InvitationSetUrlInfo stores the shortcode URL and expiry on the pending peer.
|
||||
func InvitationSetUrlInfo(invitationId string, url string, expiry int64) {
|
||||
id := client.GetConfig().GetIdentity()
|
||||
// lookup for peer with "invitation_id"
|
||||
peer := id.Peers.GetFromInvitationId(invitationId)
|
||||
if peer == nil {
|
||||
return
|
||||
}
|
||||
peer.InvitationUrl = url
|
||||
peer.InvitationExpiry = time.Unix(expiry, 0)
|
||||
id.Peers.StorePeer(peer)
|
||||
|
||||
/* for i := 0; i < len(client.GetConfig().GetIdentity().Peers); i++ {
|
||||
if client.GetConfig().GetIdentity().Peers[i].InvitationId == invitationId {
|
||||
client.GetConfig().GetIdentity().Peers[i].InvitationUrl = url
|
||||
client.GetConfig().GetIdentity().Peers[i].InvitationExpiry = time.Unix(expiry, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()*/
|
||||
|
||||
}
|
||||
|
||||
@@ -1,53 +1,150 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"forge.redroom.link/yves/meowlib/client"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Got it by the message background check
|
||||
// => noInvitationGetAnswer
|
||||
|
||||
// invitationGetAnswerReadResponse
|
||||
// Called by the initiator's background service only
|
||||
// invitationAnswerData: the data received from the server
|
||||
// invitationServerUid: the uid of the server holding the invitation
|
||||
func invitationGetAnswerReadResponse(invitation *meowlib.Invitation) (*client.Peer, string, error) {
|
||||
|
||||
// decode the payload
|
||||
// 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
|
||||
err := proto.Unmarshal(invitation.Payload, &invitationAnswer)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetAnswerReadResponse: Unmarshal", err
|
||||
if err := proto.Unmarshal(invitation.Payload, &invitationAnswer); err != nil {
|
||||
return nil, nil, "InvitationStep3ProcessAnswer: Unmarshal PackedUserMessage", err
|
||||
}
|
||||
// retreive user public key to check usermessage signature
|
||||
// contactPublikKey := serverMsg.Invitation.From
|
||||
|
||||
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitation.Uuid)
|
||||
peer.ContactPublicKey = invitation.From
|
||||
if peer != nil {
|
||||
|
||||
// process the packed user message
|
||||
usermsg, err := peer.ProcessInboundUserMessage(&invitationAnswer)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetAnswerReadResponse: ProcessInboundUserMessage", err
|
||||
}
|
||||
decodedInvitation := usermsg.Invitation
|
||||
var cc meowlib.ContactCard
|
||||
err = proto.Unmarshal(decodedInvitation.Payload, &cc)
|
||||
if err != nil {
|
||||
return nil, "InvitationGetAnswerReadResponse: Unmarshal", err
|
||||
}
|
||||
|
||||
// finalize the invitation
|
||||
// id := client.GetConfig().GetIdentity()
|
||||
peer.ContactLookupKey = cc.ContactPublicKey
|
||||
peer.ContactEncryption = cc.EncryptionPublicKey
|
||||
for _, server := range cc.PullServers {
|
||||
peer.ContactPullServers = append(peer.ContactPullServers, server.GetUid())
|
||||
}
|
||||
client.GetConfig().GetIdentity().Save()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -80,89 +80,44 @@ func (id *Identity) WipeFolder() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates an invitation for a peer, returns the newly created peer including infos to provide a ContactCard
|
||||
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerUids []string, InvitationMessage string) (*Peer, error) {
|
||||
// InvitationStep1 creates a minimal pending peer with only a temporary keypair and returns
|
||||
// the InvitationInitPayload to be transmitted to the invitee (via file, QR code, or server).
|
||||
// Full keypairs are only generated in InvitationStep3, after the invitee's answer is received.
|
||||
func (id *Identity) InvitationStep1(MyName string, ContactName string, MessageServerUids []string, InvitationMessage string) (*meowlib.InvitationInitPayload, *Peer, error) {
|
||||
var peer Peer
|
||||
var err error
|
||||
peer.Uid = uuid.New().String()
|
||||
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.Name = ContactName
|
||||
peer.InvitationId = uuid.New().String() // todo as param to identify then update url
|
||||
symKeyBytes := make([]byte, 32)
|
||||
if _, err = rand.Read(symKeyBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.MySymKey = base64.StdEncoding.EncodeToString(symKeyBytes)
|
||||
/* if id.MessageServers.Servers == nil {
|
||||
return nil, errors.New("no message servers defined in your identity")
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
if i > len(id.MessageServers.Servers)-1 {
|
||||
return nil, errors.New("requested server out of range of defined message servers")
|
||||
}
|
||||
}
|
||||
for _, i := range MessageServerIdxs {
|
||||
srv := id.MessageServers.Servers[i].GetServerCard()
|
||||
peer.MyContact.PullServers = append(peer.MyContact.PullServers, srv)
|
||||
}*/
|
||||
/* pullServers, err := id.MessageServers.LoadServerCardsFromUids(MessageServerUids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}*/
|
||||
peer.MyPullServers = MessageServerUids
|
||||
peer.MyName = MyName
|
||||
peer.InvitationId = uuid.New().String()
|
||||
peer.InvitationMessage = InvitationMessage
|
||||
peer.MyPullServers = MessageServerUids
|
||||
|
||||
// Generate DR keypair and root key for the initiator side
|
||||
drKp, err := doubleratchet.DefaultCrypto{}.GenerateDH()
|
||||
// Temporary keypair: public key is sent to invitee for step-2 encryption and as
|
||||
// the server-side lookup key where the invitee will post their answer.
|
||||
peer.InvitationKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.DrKpPrivate = base64.StdEncoding.EncodeToString(drKp.PrivateKey())
|
||||
peer.DrKpPublic = base64.StdEncoding.EncodeToString(drKp.PublicKey())
|
||||
drRootKey := make([]byte, 32)
|
||||
if _, err = rand.Read(drRootKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer.DrRootKey = base64.StdEncoding.EncodeToString(drRootKey)
|
||||
peer.DrInitiator = true
|
||||
|
||||
id.Peers.StorePeer(&peer)
|
||||
|
||||
return &peer, nil
|
||||
}
|
||||
|
||||
// Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames
|
||||
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) {
|
||||
// invitation Id found, this is an answer to an invitation
|
||||
/*for _, p := range id.Peers {
|
||||
if p.InvitationId == ReceivedContact.InvitationId {
|
||||
return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage
|
||||
}
|
||||
payload := &meowlib.InvitationInitPayload{
|
||||
Uuid: peer.InvitationId,
|
||||
Name: MyName,
|
||||
PublicKey: peer.InvitationKp.Public,
|
||||
InvitationMessage: InvitationMessage,
|
||||
}
|
||||
|
||||
// it's an invitation
|
||||
return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage*/
|
||||
return id.Peers.CheckInvitation(ReceivedContact)
|
||||
return payload, &peer, nil
|
||||
}
|
||||
|
||||
// Answers an invitation, returns the newly created peer including infos to provide a ContactCard
|
||||
func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageServerIdxs []string, ReceivedContact *meowlib.ContactCard) (*Peer, error) {
|
||||
// InvitationStep2 creates the invitee's peer entry from the received InvitationInitPayload
|
||||
// and returns the peer. The invitee generates their full keypairs here.
|
||||
// The initiator's temporary public key (payload.PublicKey) is used both as the encryption
|
||||
// target for the step-2 answer and as the server-side lookup address.
|
||||
func (id *Identity) InvitationStep2(MyName string, ContactName string, MessageServerUids []string, payload *meowlib.InvitationInitPayload) (*Peer, error) {
|
||||
var peer Peer
|
||||
var err error
|
||||
var newsrv *Server
|
||||
//var myContactCard meowlib.ContactCard
|
||||
peer.Uid = uuid.New().String()
|
||||
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
@@ -179,66 +134,116 @@ func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageS
|
||||
if ContactName != "" {
|
||||
peer.Name = ContactName
|
||||
} else {
|
||||
peer.Name = ReceivedContact.Name
|
||||
peer.Name = payload.Name
|
||||
}
|
||||
peer.ContactEncryption = ReceivedContact.EncryptionPublicKey
|
||||
peer.ContactLookupKey = ReceivedContact.LookupPublicKey
|
||||
peer.ContactPublicKey = ReceivedContact.ContactPublicKey
|
||||
peer.MySymKey = ReceivedContact.SymetricKey
|
||||
peer.InvitationId = ReceivedContact.InvitationId
|
||||
peer.InvitationMessage = ReceivedContact.InvitationMessage
|
||||
for srv := range ReceivedContact.PullServers {
|
||||
peer.ContactPullServers = append(peer.ContactPullServers, ReceivedContact.PullServers[srv].GetUid())
|
||||
newsrv, err = CreateServerFromUid(ReceivedContact.PullServers[srv].GetUid())
|
||||
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||
}
|
||||
/* for _, i := range MessageServerIdxs {
|
||||
srv := id.MessageServers.Servers[i].GetServerCard()
|
||||
peer.MyContact.PullServers = append(peer.MyContact.PullServers, srv)
|
||||
}*/
|
||||
/* srvCards, err := GetConfig().GetIdentity().MessageServers.LoadServerCardsFromUids(MessageServerIdxs)
|
||||
if err != nil {
|
||||
peer.MyContact.PullServers = srvCards
|
||||
}*/
|
||||
peer.MyPullServers = MessageServerIdxs
|
||||
// The initiator's temp key is used for both encrypting the answer and as destination.
|
||||
peer.ContactEncryption = payload.PublicKey
|
||||
peer.ContactLookupKey = payload.PublicKey
|
||||
peer.InvitationId = payload.Uuid
|
||||
peer.InvitationMessage = payload.InvitationMessage
|
||||
peer.MyPullServers = MessageServerUids
|
||||
peer.MyName = MyName
|
||||
peer.InvitationId = ReceivedContact.InvitationId
|
||||
// Adopt DR material from the initiator's ContactCard
|
||||
peer.DrRootKey = ReceivedContact.DrRootKey
|
||||
peer.ContactDrPublicKey = ReceivedContact.DrPublicKey
|
||||
peer.DrInitiator = false
|
||||
id.Peers.StorePeer(&peer)
|
||||
|
||||
return &peer, nil
|
||||
}
|
||||
|
||||
// Finalizes an invitation, returns nil if successful
|
||||
func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
|
||||
// InvitationStep3 is called by the initiator after receiving and decrypting the invitee's
|
||||
// ContactCard (step-2 answer). It generates the initiator's full keypairs and DR material,
|
||||
// updates the pending peer with the invitee's contact info, and returns the initiator's
|
||||
// full ContactCard to be sent to the invitee (STEP_3_SEND).
|
||||
func (id *Identity) InvitationStep3(inviteeContact *meowlib.ContactCard) (*meowlib.ContactCard, *Peer, error) {
|
||||
var err error
|
||||
var newsrv *Server
|
||||
/*for i, p := range id.Peers {
|
||||
if p.InvitationId == ReceivedContact.InvitationId {
|
||||
//id.Peers[i].Name = ReceivedContact.Name
|
||||
id.Peers[i].ContactEncryption = ReceivedContact.EncryptionPublicKey
|
||||
id.Peers[i].ContactLookupKey = ReceivedContact.LookupPublicKey
|
||||
id.Peers[i].ContactPublicKey = ReceivedContact.ContactPublicKey
|
||||
srvs := []string{}
|
||||
for srv := range ReceivedContact.PullServers {
|
||||
srvs = append(srvs, ReceivedContact.PullServers[srv].GetUid())
|
||||
}
|
||||
id.Peers[i].ContactPullServers = srvs
|
||||
return nil
|
||||
peer := id.Peers.GetFromInvitationId(inviteeContact.InvitationId)
|
||||
if peer == nil {
|
||||
return nil, nil, errors.New("no pending peer found for invitation id " + inviteeContact.InvitationId)
|
||||
}
|
||||
|
||||
// Generate full keypairs now that the invitee's identity is known.
|
||||
peer.MyIdentity, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.MyEncryptionKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.MyLookupKp, err = meowlib.NewKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
symKeyBytes := make([]byte, 32)
|
||||
if _, err = rand.Read(symKeyBytes); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.MySymKey = base64.StdEncoding.EncodeToString(symKeyBytes)
|
||||
|
||||
// Generate DR keypair and root key.
|
||||
drKp, err := doubleratchet.DefaultCrypto{}.GenerateDH()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.DrKpPrivate = base64.StdEncoding.EncodeToString(drKp.PrivateKey())
|
||||
peer.DrKpPublic = base64.StdEncoding.EncodeToString(drKp.PublicKey())
|
||||
drRootKey := make([]byte, 32)
|
||||
if _, err = rand.Read(drRootKey); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
peer.DrRootKey = base64.StdEncoding.EncodeToString(drRootKey)
|
||||
peer.DrInitiator = true
|
||||
|
||||
// Store invitee contact info.
|
||||
peer.ContactPublicKey = inviteeContact.ContactPublicKey
|
||||
peer.ContactEncryption = inviteeContact.EncryptionPublicKey
|
||||
peer.ContactLookupKey = inviteeContact.LookupPublicKey
|
||||
for _, srv := range inviteeContact.PullServers {
|
||||
peer.ContactPullServers = append(peer.ContactPullServers, srv.GetUid())
|
||||
newsrv, err := CreateServerFromUid(srv.GetUid())
|
||||
if err == nil {
|
||||
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||
}
|
||||
}
|
||||
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)*/
|
||||
for srv := range ReceivedContact.PullServers {
|
||||
newsrv, err = CreateServerFromUid(ReceivedContact.PullServers[srv].GetUid())
|
||||
// Drop the temporary invitation keypair — no longer needed.
|
||||
peer.InvitationKp = nil
|
||||
|
||||
id.Peers.StorePeer(peer)
|
||||
|
||||
return peer.GetMyContact(), peer, nil
|
||||
}
|
||||
|
||||
// InvitationStep4 is called by the invitee upon receiving the initiator's full ContactCard
|
||||
// (carried as a regular UserMessage with invitation.step=3). It finalizes the peer entry.
|
||||
func (id *Identity) InvitationStep4(initiatorContact *meowlib.ContactCard) error {
|
||||
var err error
|
||||
var newsrv *Server
|
||||
for _, srv := range initiatorContact.PullServers {
|
||||
newsrv, err = CreateServerFromUid(srv.GetUid())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id.MessageServers.StoreServerIfNotExists(newsrv)
|
||||
}
|
||||
return id.Peers.FinalizeInvitation(ReceivedContact)
|
||||
peer := id.Peers.GetFromInvitationId(initiatorContact.InvitationId)
|
||||
if peer == nil {
|
||||
return errors.New("no pending peer found for invitation id " + initiatorContact.InvitationId)
|
||||
}
|
||||
peer.ContactPublicKey = initiatorContact.ContactPublicKey
|
||||
peer.ContactEncryption = initiatorContact.EncryptionPublicKey
|
||||
peer.ContactLookupKey = initiatorContact.LookupPublicKey
|
||||
peer.MySymKey = initiatorContact.SymetricKey
|
||||
peer.DrRootKey = initiatorContact.DrRootKey
|
||||
peer.ContactDrPublicKey = initiatorContact.DrPublicKey
|
||||
peer.DrInitiator = false
|
||||
for _, srv := range initiatorContact.PullServers {
|
||||
peer.ContactPullServers = append(peer.ContactPullServers, srv.GetUid())
|
||||
}
|
||||
id.Peers.StorePeer(peer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckInvitation checks if the received ContactCard is an answer to one of our pending
|
||||
// invitations. Returns true when it is, with the proposed and received nicknames.
|
||||
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string, invitationMessage string) {
|
||||
return id.Peers.CheckInvitation(ReceivedContact)
|
||||
}
|
||||
|
||||
// LoadIdentity loads an identity from an encrypted file
|
||||
@@ -386,9 +391,18 @@ func (id *Identity) GetRequestJobs() []RequestsJob {
|
||||
return nil
|
||||
}
|
||||
for _, peer := range peers {
|
||||
// check if peer inviation is accepted
|
||||
for _, server := range peer.MyPullServers {
|
||||
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
||||
if srvs[server] == nil {
|
||||
continue
|
||||
}
|
||||
if peer.MyLookupKp != nil {
|
||||
// Active peer — use the permanent lookup key.
|
||||
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
||||
} else if peer.InvitationKp != nil {
|
||||
// Step-1 pending peer — poll using the temp invitation keypair so the
|
||||
// server-stored step-2 answer can be retrieved.
|
||||
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.InvitationKp)
|
||||
}
|
||||
}
|
||||
}
|
||||
// add hidden peers
|
||||
|
||||
@@ -142,14 +142,12 @@ func TestGetRequestJobs(t *testing.T) {
|
||||
// Call GetRequestJobs
|
||||
jobs := id.GetRequestJobs()
|
||||
|
||||
// Check that the returned list is as expected
|
||||
assert.Equal(t, 6, len(jobs), "Expected 6 jobs")
|
||||
// All 10 test peers use server1 and server2, so exactly 2 jobs are expected.
|
||||
assert.Equal(t, 2, len(jobs), "Expected 2 jobs (server1 and server2)")
|
||||
|
||||
// Check that each job has the correct server and lookup keys
|
||||
for _, job := range jobs {
|
||||
//fmt.Println(job.Server.GetUid(), job.LookupKeys)
|
||||
assert.Contains(t, []string{"server1", "server2", "server3", "server4", "server5", "server6"}, job.Server.GetUid(), "Unexpected server UID")
|
||||
assert.Len(t, job.LookupKeys, 1, "Expected 1 lookup key per job")
|
||||
assert.Contains(t, []string{"server1", "server2"}, job.Server.GetUid(), "Unexpected server UID")
|
||||
assert.Len(t, job.LookupKeys, 10, "Expected 10 lookup keys (one per test peer) per job")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
|
||||
44
client/inv_test_init.id
Normal file
44
client/inv_test_init.id
Normal file
@@ -0,0 +1,44 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
Comment: https://gopenpgp.org
|
||||
Version: GopenPGP 2.8.3
|
||||
|
||||
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
|
||||
=C05v
|
||||
-----END PGP MESSAGE-----
|
||||
205
client/invitation_test.go
Normal file
205
client/invitation_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"forge.redroom.link/yves/meowlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// setupInvitationTest creates two independent identities with separate storage paths.
|
||||
func setupInvitationTest(t *testing.T) (initiator *Identity, invitee *Identity, cleanup func()) {
|
||||
t.Helper()
|
||||
cfg := GetConfig()
|
||||
cfg.IdentityFile = "inv_test_init.id"
|
||||
cfg.SetMemPass("testpass")
|
||||
|
||||
initiator, err := CreateIdentity("initiator")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invitee, err = CreateIdentity("invitee")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Give each identity a pull server UID.
|
||||
srvInit, _ := CreateServerFromUrl("http://init.server/meow/")
|
||||
initiator.MessageServers.StoreServer(srvInit)
|
||||
srvInvitee, _ := CreateServerFromUrl("http://invitee.server/meow/")
|
||||
invitee.MessageServers.StoreServer(srvInvitee)
|
||||
|
||||
cleanup = func() {
|
||||
os.Remove("inv_test_init.id")
|
||||
os.RemoveAll(cfg.StoragePath + "/" + initiator.Uuid)
|
||||
os.RemoveAll(cfg.StoragePath + "/" + invitee.Uuid)
|
||||
}
|
||||
return initiator, invitee, cleanup
|
||||
}
|
||||
|
||||
// TestInvitationStep1 verifies that InvitationStep1 creates a minimal peer with only
|
||||
// InvitationKp set (no full keypairs yet) and returns a valid InvitationInitPayload.
|
||||
func TestInvitationStep1(t *testing.T) {
|
||||
initiator, _, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
payload, peer, err := initiator.InvitationStep1("Alice", "Bob", []string{"http://init.server/meow/"}, "Hello Bob!")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, payload)
|
||||
assert.NotNil(t, peer)
|
||||
|
||||
assert.NotEmpty(t, payload.Uuid)
|
||||
assert.Equal(t, "Alice", payload.Name)
|
||||
assert.NotEmpty(t, payload.PublicKey)
|
||||
assert.Equal(t, "Hello Bob!", payload.InvitationMessage)
|
||||
|
||||
// Full keypairs must NOT be set yet.
|
||||
assert.Nil(t, peer.MyIdentity)
|
||||
assert.Nil(t, peer.MyEncryptionKp)
|
||||
assert.Nil(t, peer.MyLookupKp)
|
||||
// Temp keypair must be set.
|
||||
assert.NotNil(t, peer.InvitationKp)
|
||||
assert.Equal(t, payload.PublicKey, peer.InvitationKp.Public)
|
||||
}
|
||||
|
||||
// TestInvitationStep1PayloadRoundTrip verifies Compress/Decompress of InvitationInitPayload.
|
||||
func TestInvitationStep1PayloadRoundTrip(t *testing.T) {
|
||||
initiator, _, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
payload, _, err := initiator.InvitationStep1("Alice", "Bob", nil, "test msg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
compressed, err := payload.Compress()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, compressed)
|
||||
|
||||
restored, err := meowlib.NewInvitationInitPayloadFromCompressed(compressed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, payload.Uuid, restored.Uuid)
|
||||
assert.Equal(t, payload.Name, restored.Name)
|
||||
assert.Equal(t, payload.PublicKey, restored.PublicKey)
|
||||
assert.Equal(t, payload.InvitationMessage, restored.InvitationMessage)
|
||||
}
|
||||
|
||||
// TestInvitationStep2 verifies that InvitationStep2 creates a peer with full keypairs and
|
||||
// sets the initiator's temp key as both ContactEncryption and ContactLookupKey.
|
||||
func TestInvitationStep2(t *testing.T) {
|
||||
initiator, invitee, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
payload, _, err := initiator.InvitationStep1("Alice", "Bob", nil, "Hi")
|
||||
assert.NoError(t, err)
|
||||
|
||||
peer, err := invitee.InvitationStep2("Bob", "Alice", []string{"http://invitee.server/meow/"}, payload)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, peer)
|
||||
|
||||
// Full keypairs must be set on invitee's peer.
|
||||
assert.NotNil(t, peer.MyIdentity)
|
||||
assert.NotNil(t, peer.MyEncryptionKp)
|
||||
assert.NotNil(t, peer.MyLookupKp)
|
||||
|
||||
// Contact fields must point to initiator's temp key.
|
||||
assert.Equal(t, payload.PublicKey, peer.ContactEncryption)
|
||||
assert.Equal(t, payload.PublicKey, peer.ContactLookupKey)
|
||||
assert.Equal(t, payload.Uuid, peer.InvitationId)
|
||||
}
|
||||
|
||||
// TestInvitationFullFlow exercises the complete 4-step invitation handshake end-to-end,
|
||||
// verifying that both peers end up with each other's full contact information.
|
||||
func TestInvitationFullFlow(t *testing.T) {
|
||||
initiator, invitee, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
// STEP_1: initiator creates init payload.
|
||||
payload, initPeer, err := initiator.InvitationStep1("Alice", "Bob", []string{"http://init.server/meow/"}, "Hello!")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, initPeer.InvitationKp)
|
||||
assert.Nil(t, initPeer.MyIdentity)
|
||||
|
||||
// STEP_2: invitee creates their peer from the payload.
|
||||
srvCard := &meowlib.ServerCard{Name: "InviteeServer", Url: "http://invitee.server/meow/"}
|
||||
inviteePeer, err := invitee.InvitationStep2("Bob", "Alice", []string{"http://invitee.server/meow/"}, payload)
|
||||
assert.NoError(t, err)
|
||||
inviteeCC := inviteePeer.GetMyContact()
|
||||
inviteeCC.PullServers = append(inviteeCC.PullServers, srvCard)
|
||||
|
||||
// STEP_3: initiator receives invitee's CC, generates full keypairs.
|
||||
myCC, _, err := initiator.InvitationStep3(inviteeCC)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, myCC)
|
||||
assert.NotEmpty(t, myCC.ContactPublicKey)
|
||||
assert.NotEmpty(t, myCC.EncryptionPublicKey)
|
||||
assert.NotEmpty(t, myCC.LookupPublicKey)
|
||||
assert.NotEmpty(t, myCC.DrRootKey)
|
||||
assert.NotEmpty(t, myCC.DrPublicKey)
|
||||
|
||||
// After step 3, initiator's peer must have full keypairs and invitee's contact info.
|
||||
updatedInitPeer := initiator.Peers.GetFromInvitationId(payload.Uuid)
|
||||
assert.NotNil(t, updatedInitPeer.MyIdentity)
|
||||
assert.NotNil(t, updatedInitPeer.MyEncryptionKp)
|
||||
assert.NotNil(t, updatedInitPeer.MyLookupKp)
|
||||
assert.Equal(t, inviteePeer.MyIdentity.Public, updatedInitPeer.ContactPublicKey)
|
||||
assert.Equal(t, inviteePeer.MyEncryptionKp.Public, updatedInitPeer.ContactEncryption)
|
||||
assert.Nil(t, updatedInitPeer.InvitationKp) // temp key must be cleared
|
||||
|
||||
// STEP_4: invitee finalizes from initiator's full CC.
|
||||
srvCardInit := &meowlib.ServerCard{Name: "InitServer", Url: "http://init.server/meow/"}
|
||||
myCC.PullServers = append(myCC.PullServers, srvCardInit)
|
||||
err = invitee.InvitationStep4(myCC)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Both peers must now be fully finalized (ContactPublicKey set → not pending).
|
||||
finalInitPeer := initiator.Peers.GetFromInvitationId(payload.Uuid)
|
||||
assert.False(t, finalInitPeer.InvitationPending())
|
||||
|
||||
finalInviteePeer := invitee.Peers.GetFromInvitationId(payload.Uuid)
|
||||
assert.False(t, finalInviteePeer.InvitationPending())
|
||||
assert.Equal(t, updatedInitPeer.MyIdentity.Public, finalInviteePeer.ContactPublicKey)
|
||||
assert.Equal(t, updatedInitPeer.MyEncryptionKp.Public, finalInviteePeer.ContactEncryption)
|
||||
assert.Equal(t, updatedInitPeer.MyLookupKp.Public, finalInviteePeer.ContactLookupKey)
|
||||
assert.NotEmpty(t, finalInviteePeer.DrRootKey)
|
||||
}
|
||||
|
||||
// TestInvitationStep3NotFound verifies that InvitationStep3 returns an error when no
|
||||
// pending peer exists for the given invitation ID.
|
||||
func TestInvitationStep3NotFound(t *testing.T) {
|
||||
initiator, _, cleanup := setupInvitationTest(t)
|
||||
defer cleanup()
|
||||
|
||||
cc := &meowlib.ContactCard{InvitationId: "nonexistent-uuid", ContactPublicKey: "pub"}
|
||||
_, _, err := initiator.InvitationStep3(cc)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestGetRequestJobsPendingPeer verifies that pending (step-1 only) peers contribute
|
||||
// their InvitationKp to GetRequestJobs instead of MyLookupKp.
|
||||
func TestGetRequestJobsPendingPeer(t *testing.T) {
|
||||
cfg := GetConfig()
|
||||
cfg.SetMemPass("testpass")
|
||||
id, err := CreateIdentity("testjobs")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(cfg.StoragePath + "/" + id.Uuid)
|
||||
|
||||
cfg.SetIdentity(id)
|
||||
id.MessageServers = ServerStorage{DbFile: "testjobs.db"}
|
||||
defer os.RemoveAll("testjobs.db")
|
||||
|
||||
srv, _ := CreateServerFromUrl("http://srv1.test/meow/")
|
||||
id.MessageServers.StoreServer(srv)
|
||||
|
||||
// Create a step-1 pending peer.
|
||||
_, _, err = id.InvitationStep1("Me", "Friend", []string{"http://srv1.test/meow/"}, "Hi")
|
||||
assert.NoError(t, err)
|
||||
|
||||
jobs := id.GetRequestJobs()
|
||||
// At least one job should have a lookup key (the InvitationKp).
|
||||
total := 0
|
||||
for _, j := range jobs {
|
||||
total += len(j.LookupKeys)
|
||||
}
|
||||
assert.Greater(t, total, 0)
|
||||
}
|
||||
@@ -56,8 +56,10 @@ type Peer struct {
|
||||
DbIds []string `json:"db_ids,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
PersonnaeDbId string `json:"personnae_db_id,omitempty"`
|
||||
// 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"`
|
||||
@@ -171,9 +173,28 @@ func (p *Peer) BuildSingleFileMessage(filename string, message []byte) ([]meowli
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// Builds an invitation answer user message.
|
||||
// it takes as input a contactcard generated by Identity.AnswerInvitation
|
||||
func (p *Peer) BuildInvitationAnswerMessage(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
// BuildInvitationStep2Message builds the invitee's answer UserMessage (STEP_2_SEND).
|
||||
// The ContactCard is encrypted with the initiator's temp public key via ProcessOutboundUserMessage.
|
||||
func (p *Peer) BuildInvitationStep2Message(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 2
|
||||
out, err := proto.Marshal(myContactCard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invitation.Uuid = p.InvitationId
|
||||
invitation.Payload = out
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.Invitation = &invitation
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Type = "1"
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildInvitationStep3Message builds the initiator's full ContactCard UserMessage (STEP_3_SEND).
|
||||
// Sent through the invitee's servers after the initiator has finalized their keypairs.
|
||||
func (p *Peer) BuildInvitationStep3Message(myContactCard *meowlib.ContactCard) (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 3
|
||||
@@ -190,6 +211,42 @@ func (p *Peer) BuildInvitationAnswerMessage(myContactCard *meowlib.ContactCard)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildInvitationStep4Message builds the invitee's confirmation UserMessage (STEP_4).
|
||||
// Sent through the initiator's servers to signal the invitation is complete.
|
||||
func (p *Peer) BuildInvitationStep4Message() (*meowlib.UserMessage, error) {
|
||||
var msg meowlib.UserMessage
|
||||
var invitation meowlib.Invitation
|
||||
invitation.Step = 4
|
||||
invitation.Uuid = p.InvitationId
|
||||
msg.Destination = p.ContactLookupKey
|
||||
msg.Invitation = &invitation
|
||||
msg.From = p.MyIdentity.Public
|
||||
msg.Type = "1"
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// ProcessInboundStep2UserMessage decrypts the invitee's step-2 answer using the
|
||||
// initiator's temporary InvitationKp private key. inviteePublicKey is the sender's
|
||||
// identity public key (carried in Invitation.From by the server).
|
||||
func (p *Peer) ProcessInboundStep2UserMessage(packed *meowlib.PackedUserMessage, inviteePublicKey string) (*meowlib.UserMessage, error) {
|
||||
dec, err := meowlib.AsymDecryptAndCheck(p.InvitationKp.Private, inviteePublicKey, packed.Payload, packed.Signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.DeserializeUserMessage(dec)
|
||||
}
|
||||
|
||||
// ProcessInboundStep3UserMessage decrypts the initiator's step-3 full ContactCard using
|
||||
// the invitee's MyEncryptionKp. Signature verification is skipped because the
|
||||
// initiator's identity key is not yet known — it is extracted from the decrypted payload.
|
||||
func (p *Peer) ProcessInboundStep3UserMessage(packed *meowlib.PackedUserMessage) (*meowlib.UserMessage, error) {
|
||||
dec, err := meowlib.AsymDecrypt(p.MyEncryptionKp.Private, packed.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.DeserializeUserMessage(dec)
|
||||
}
|
||||
|
||||
//
|
||||
// Messages encryption and packaging
|
||||
//
|
||||
|
||||
@@ -228,7 +228,7 @@ func TestBuildSingleFileMessage_EmptyFile(t *testing.T) {
|
||||
// BuildInvitationAnswerMessage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestBuildInvitationAnswerMessage(t *testing.T) {
|
||||
func TestBuildInvitationStep2Message(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
@@ -239,13 +239,13 @@ func TestBuildInvitationAnswerMessage(t *testing.T) {
|
||||
ContactPublicKey: "alice-pub",
|
||||
}
|
||||
|
||||
msg, err := p.BuildInvitationAnswerMessage(contactCard)
|
||||
msg, err := p.BuildInvitationStep2Message(contactCard)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "dest-lookup", msg.Destination)
|
||||
assert.Equal(t, "my-pub", msg.From)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.NotNil(t, msg.Invitation)
|
||||
assert.Equal(t, int32(3), msg.Invitation.Step)
|
||||
assert.Equal(t, int32(2), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-123", msg.Invitation.Uuid)
|
||||
|
||||
// Payload is the proto-serialized contact card
|
||||
@@ -256,6 +256,42 @@ func TestBuildInvitationAnswerMessage(t *testing.T) {
|
||||
assert.Equal(t, "alice-pub", decoded.ContactPublicKey)
|
||||
}
|
||||
|
||||
func TestBuildInvitationStep3Message(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
InvitationId: "inv-uuid-456",
|
||||
}
|
||||
contactCard := &meowlib.ContactCard{
|
||||
Name: "Initiator",
|
||||
ContactPublicKey: "init-pub",
|
||||
}
|
||||
|
||||
msg, err := p.BuildInvitationStep3Message(contactCard)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(3), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-456", msg.Invitation.Uuid)
|
||||
|
||||
var decoded meowlib.ContactCard
|
||||
err = proto.Unmarshal(msg.Invitation.Payload, &decoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Initiator", decoded.Name)
|
||||
}
|
||||
|
||||
func TestBuildInvitationStep4Message(t *testing.T) {
|
||||
p := &Peer{
|
||||
ContactLookupKey: "dest-lookup",
|
||||
MyIdentity: &meowlib.KeyPair{Public: "my-pub"},
|
||||
InvitationId: "inv-uuid-789",
|
||||
}
|
||||
|
||||
msg, err := p.BuildInvitationStep4Message()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int32(4), msg.Invitation.Step)
|
||||
assert.Equal(t, "inv-uuid-789", msg.Invitation.Uuid)
|
||||
assert.Nil(t, msg.Invitation.Payload)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialize / Deserialize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -199,12 +199,12 @@ func (ints *Server) BuildVideoRoomRequestMessage(users []string, expiry uint64)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// BuildToServerMessageInvitation creates an invitation message to server and returns it as a meowlib.ToServerMessage
|
||||
// it takes as input a contactcard generated by Identity.InvitePeer
|
||||
func (ints *Server) BuildToServerMessageInvitationCreation(invitation *meowlib.ContactCard, password string, timeout int, shortCodeLen int) (*meowlib.ToServerMessage, error) {
|
||||
// BuildToServerMessageInvitationStep1 sends the InvitationInitPayload to the server (STEP_1).
|
||||
// The server stores it and returns a shortcode URL.
|
||||
func (ints *Server) BuildToServerMessageInvitationStep1(initPayload *meowlib.InvitationInitPayload, password string, timeout int, shortCodeLen int) (*meowlib.ToServerMessage, error) {
|
||||
var msg meowlib.ToServerMessage
|
||||
var inv meowlib.Invitation
|
||||
payload, err := invitation.Compress()
|
||||
payload, err := initPayload.Compress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -454,16 +454,18 @@ func TestServer_BuildVideoRoomRequestMessage_SingleUser(t *testing.T) {
|
||||
// BuildToServerMessageInvitationCreation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationCreation(t *testing.T) {
|
||||
func TestServer_BuildToServerMessageInvitationStep1(t *testing.T) {
|
||||
srv, err := CreateServerFromUrl("https://example.com/meow")
|
||||
assert.NoError(t, err)
|
||||
|
||||
cc := &meowlib.ContactCard{
|
||||
Name: "Alice",
|
||||
ContactPublicKey: "alice-pub",
|
||||
initPayload := &meowlib.InvitationInitPayload{
|
||||
Uuid: "test-uuid",
|
||||
Name: "Alice",
|
||||
PublicKey: "alice-temp-pub",
|
||||
InvitationMessage: "Hello!",
|
||||
}
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationCreation(cc, "secret", 300, 8)
|
||||
msg, err := srv.BuildToServerMessageInvitationStep1(initPayload, "secret", 300, 8)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1", msg.Type)
|
||||
assert.Equal(t, srv.UserKp.Public, msg.From)
|
||||
@@ -474,18 +476,19 @@ func TestServer_BuildToServerMessageInvitationCreation(t *testing.T) {
|
||||
assert.Equal(t, int32(8), msg.Invitation.ShortcodeLen)
|
||||
assert.NotEmpty(t, msg.Invitation.Payload)
|
||||
|
||||
// Payload is a compressed ContactCard — decompress and verify
|
||||
restored, err := meowlib.NewContactCardFromCompressed(msg.Invitation.Payload)
|
||||
// Payload is a compressed InvitationInitPayload — decompress and verify
|
||||
restored, err := meowlib.NewInvitationInitPayloadFromCompressed(msg.Invitation.Payload)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", restored.Name)
|
||||
assert.Equal(t, "alice-pub", restored.ContactPublicKey)
|
||||
assert.Equal(t, "alice-temp-pub", restored.PublicKey)
|
||||
assert.Equal(t, "test-uuid", restored.Uuid)
|
||||
}
|
||||
|
||||
func TestServer_BuildToServerMessageInvitationCreation_NoPassword(t *testing.T) {
|
||||
func TestServer_BuildToServerMessageInvitationStep1_NoPassword(t *testing.T) {
|
||||
srv, _ := CreateServerFromUrl("https://example.com/meow")
|
||||
cc := &meowlib.ContactCard{Name: "Bob"}
|
||||
initPayload := &meowlib.InvitationInitPayload{Name: "Bob", Uuid: "bob-uuid", PublicKey: "bob-pub"}
|
||||
|
||||
msg, err := srv.BuildToServerMessageInvitationCreation(cc, "", 60, 6)
|
||||
msg, err := srv.BuildToServerMessageInvitationStep1(initPayload, "", 60, 6)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, msg.Invitation.Password)
|
||||
assert.Equal(t, int32(1), msg.Invitation.Step)
|
||||
|
||||
44
client/test.id
Normal file
44
client/test.id
Normal file
@@ -0,0 +1,44 @@
|
||||
-----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