This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
Comment: https://gopenpgp.org
|
||||
Version: GopenPGP 2.8.3
|
||||
|
||||
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
|
||||
=C05v
|
||||
-----END PGP MESSAGE-----
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
"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 invitee's ContactCard response to a .mwiv file.
|
||||
// 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
|
||||
@@ -33,12 +35,29 @@ func Step2ReadAndAnswer(invitationFile string, nickname string, myNickname strin
|
||||
mynick = client.GetConfig().GetIdentity().Nickname
|
||||
}
|
||||
|
||||
response, err := messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload, nickname, mynick, serverUids)
|
||||
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 response.GetMyContact().WriteCompressed(filename)
|
||||
return os.WriteFile(filename, out, 0600)
|
||||
}
|
||||
|
||||
142
client/invitation/messages/flow_test.go
Normal file
142
client/invitation/messages/flow_test.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -6,17 +6,28 @@ import (
|
||||
)
|
||||
|
||||
// Step2InviteeCreatesInitiatorAndEncryptedContactCard creates the invitee's peer entry
|
||||
// from an InvitationInitPayload and generates the encrypted ContactCard to be sent back
|
||||
// to the initiator via any transport.
|
||||
func Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload *meowlib.InvitationInitPayload, nickname string, myNickname string, serverUids []string) (*client.Peer, error) {
|
||||
// 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, err
|
||||
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 peer, nil
|
||||
return packed, peer, nil
|
||||
}
|
||||
|
||||
@@ -62,29 +62,20 @@ func Step2ReadResponse(invitationData []byte, invitationServerUid string) (*meow
|
||||
return meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload)
|
||||
}
|
||||
|
||||
// Step2PostAnswer builds and returns the packed server message that posts the
|
||||
// invitee's ContactCard (encrypted with the initiator's temp key) to the invitation server.
|
||||
func Step2PostAnswer(invitationId string, invitationServerUid string, timeout int) ([]byte, error) {
|
||||
// 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")
|
||||
}
|
||||
|
||||
answermsg, err := peer.BuildInvitationStep2Message(peer.GetMyContact())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packedMsg, err := peer.ProcessOutboundUserMessage(answermsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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