fixes step 2
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
yc
2026-04-12 13:38:15 +02:00
parent 793213b3fb
commit 327bd390c4
7 changed files with 186 additions and 111 deletions

View File

@@ -1,44 +0,0 @@
-----BEGIN PGP MESSAGE-----
Comment: https://gopenpgp.org
Version: GopenPGP 2.8.3
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
=C05v
-----END PGP MESSAGE-----

View File

@@ -8,10 +8,12 @@ import (
"forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib"
"forge.redroom.link/yves/meowlib/client" "forge.redroom.link/yves/meowlib/client"
"forge.redroom.link/yves/meowlib/client/invitation/messages" "forge.redroom.link/yves/meowlib/client/invitation/messages"
"google.golang.org/protobuf/proto"
) )
// Step2ReadAndAnswer reads an InvitationInitPayload from a .mwiv file, creates the // 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 { func Step2ReadAndAnswer(invitationFile string, nickname string, myNickname string, serverUids []string) error {
if _, err := os.Stat(invitationFile); os.IsNotExist(err) { if _, err := os.Stat(invitationFile); os.IsNotExist(err) {
return err return err
@@ -33,12 +35,29 @@ func Step2ReadAndAnswer(invitationFile string, nickname string, myNickname strin
mynick = client.GetConfig().GetIdentity().Nickname 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 { if err != nil {
return err return err
} }
c := client.GetConfig() c := client.GetConfig()
filename := c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv" filename := c.StoragePath + string(os.PathSeparator) + mynick + "-" + nickname + ".mwiv"
return response.GetMyContact().WriteCompressed(filename) return os.WriteFile(filename, out, 0600)
} }

View 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)
}

View File

@@ -6,17 +6,28 @@ import (
) )
// Step2InviteeCreatesInitiatorAndEncryptedContactCard creates the invitee's peer entry // Step2InviteeCreatesInitiatorAndEncryptedContactCard creates the invitee's peer entry
// from an InvitationInitPayload and generates the encrypted ContactCard to be sent back // from an InvitationInitPayload, then builds the invitee's ContactCard and returns it
// to the initiator via any transport. // as a PackedUserMessage asymmetrically encrypted with the initiator's temporary public
func Step2InviteeCreatesInitiatorAndEncryptedContactCard(payload *meowlib.InvitationInitPayload, nickname string, myNickname string, serverUids []string) (*client.Peer, error) { // 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 mynick := myNickname
if mynick == "" { if mynick == "" {
mynick = client.GetConfig().GetIdentity().Nickname mynick = client.GetConfig().GetIdentity().Nickname
} }
peer, err := client.GetConfig().GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload) peer, err := client.GetConfig().GetIdentity().InvitationStep2(mynick, nickname, serverUids, payload)
if err != nil { 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() client.GetConfig().GetIdentity().Save()
return peer, nil return packed, peer, nil
} }

View File

@@ -62,29 +62,20 @@ func Step2ReadResponse(invitationData []byte, invitationServerUid string) (*meow
return meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload) return meowlib.NewInvitationInitPayloadFromCompressed(serverMsg.Invitation.Payload)
} }
// Step2PostAnswer builds and returns the packed server message that posts the // Step2PostAnswer wraps the invitee's already-built PackedUserMessage into a server
// invitee's ContactCard (encrypted with the initiator's temp key) to the invitation server. // message and posts it to the invitation server. The packed message is produced by
func Step2PostAnswer(invitationId string, invitationServerUid string, timeout int) ([]byte, error) { // messages.Step2InviteeCreatesInitiatorAndEncryptedContactCard.
func Step2PostAnswer(invitationId string, packedMsg *meowlib.PackedUserMessage, invitationServerUid string, timeout int) ([]byte, error) {
peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId) peer := client.GetConfig().GetIdentity().Peers.GetFromInvitationId(invitationId)
if peer == nil { if peer == nil {
return nil, errors.New("no peer with that invitation id") 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) invitationServer, err := client.GetConfig().GetIdentity().MessageServers.LoadServer(invitationServerUid)
if err != nil { if err != nil {
return nil, err return nil, err
} }
packedMsg, err := peer.ProcessOutboundUserMessage(answermsg)
if err != nil {
return nil, err
}
toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout) toServerMessage, err := invitationServer.BuildToServerMessageInvitationAnswer(packedMsg, peer.MyIdentity.Public, invitationId, timeout)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -7,8 +7,8 @@ import (
"time" "time"
"forge.redroom.link/yves/meowlib" "forge.redroom.link/yves/meowlib"
doubleratchet "github.com/status-im/doubleratchet"
"github.com/google/uuid" "github.com/google/uuid"
doubleratchet "github.com/status-im/doubleratchet"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
@@ -59,7 +59,7 @@ type Peer struct {
// Invitation temporary keypair (step 1 only — discarded after step 3) // Invitation temporary keypair (step 1 only — discarded after step 3)
InvitationKp *meowlib.KeyPair `json:"invitation_kp,omitempty"` InvitationKp *meowlib.KeyPair `json:"invitation_kp,omitempty"`
// Double Ratchet state // Double Ratchet state
DrKpPublic string `json:"dr_kp_public,omitempty"` DrKpPublic string `json:"dr_kp_public,omitempty"`
DrKpPrivate string `json:"dr_kp_private,omitempty"` DrKpPrivate string `json:"dr_kp_private,omitempty"`
DrRootKey string `json:"dr_root_key,omitempty"` DrRootKey string `json:"dr_root_key,omitempty"`
DrInitiator bool `json:"dr_initiator,omitempty"` DrInitiator bool `json:"dr_initiator,omitempty"`

View File

@@ -1,44 +0,0 @@
-----BEGIN PGP MESSAGE-----
Comment: https://gopenpgp.org
Version: GopenPGP 2.8.3
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
=BI6k
-----END PGP MESSAGE-----