From 327bd390c4b8c03e1748ec355eb69fd8442f47f4 Mon Sep 17 00:00:00 2001 From: yc Date: Sun, 12 Apr 2026 13:38:15 +0200 Subject: [PATCH] fixes step 2 --- client/inv_test_init.id | 44 -------- client/invitation/files/step2.go | 25 ++++- client/invitation/messages/flow_test.go | 142 ++++++++++++++++++++++++ client/invitation/messages/step2.go | 21 +++- client/invitation/server/step2.go | 17 +-- client/peer.go | 4 +- client/test.id | 44 -------- 7 files changed, 186 insertions(+), 111 deletions(-) delete mode 100644 client/inv_test_init.id create mode 100644 client/invitation/messages/flow_test.go delete mode 100644 client/test.id diff --git a/client/inv_test_init.id b/client/inv_test_init.id deleted file mode 100644 index 842af96..0000000 --- a/client/inv_test_init.id +++ /dev/null @@ -1,44 +0,0 @@ ------BEGIN PGP MESSAGE----- -Comment: https://gopenpgp.org -Version: GopenPGP 2.8.3 - -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 -=C05v ------END PGP MESSAGE----- \ No newline at end of file diff --git a/client/invitation/files/step2.go b/client/invitation/files/step2.go index 96e713c..f83fdeb 100644 --- a/client/invitation/files/step2.go +++ b/client/invitation/files/step2.go @@ -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) } diff --git a/client/invitation/messages/flow_test.go b/client/invitation/messages/flow_test.go new file mode 100644 index 0000000..191554e --- /dev/null +++ b/client/invitation/messages/flow_test.go @@ -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) +} diff --git a/client/invitation/messages/step2.go b/client/invitation/messages/step2.go index a4972b7..2a37e86 100644 --- a/client/invitation/messages/step2.go +++ b/client/invitation/messages/step2.go @@ -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 } diff --git a/client/invitation/server/step2.go b/client/invitation/server/step2.go index 5fcfb22..60f2077 100644 --- a/client/invitation/server/step2.go +++ b/client/invitation/server/step2.go @@ -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 diff --git a/client/peer.go b/client/peer.go index 72c1677..fa27610 100644 --- a/client/peer.go +++ b/client/peer.go @@ -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"` diff --git a/client/test.id b/client/test.id deleted file mode 100644 index 83fc178..0000000 --- a/client/test.id +++ /dev/null @@ -1,44 +0,0 @@ ------BEGIN PGP MESSAGE----- -Comment: https://gopenpgp.org -Version: GopenPGP 2.8.3 - -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 -=BI6k ------END PGP MESSAGE----- \ No newline at end of file