From f4fb42d72e19dd9e9cbe614cac34ef32b76895f8 Mon Sep 17 00:00:00 2001 From: ycc Date: Wed, 4 Mar 2026 22:30:22 +0100 Subject: [PATCH] double ratchet first implementation --- client/drsession.go | 174 +++++++++++++++++++++ client/helpers/bgPollHelper.go | 9 +- client/helpers/invitationFinalizeHelper.go | 2 +- client/helpers/messageHelper.go | 7 + client/identity.go | 20 +++ client/peer.go | 62 +++++++- client/peer_test.go | 75 ++++++++- go.mod | 1 + go.sum | 2 + messages.pb.go | 34 +++- pb/messages.proto | 3 + 11 files changed, 375 insertions(+), 14 deletions(-) create mode 100644 client/drsession.go diff --git a/client/drsession.go b/client/drsession.go new file mode 100644 index 0000000..7ae9102 --- /dev/null +++ b/client/drsession.go @@ -0,0 +1,174 @@ +package client + +import ( + "encoding/hex" + "encoding/json" + "encoding/base64" + "fmt" + + doubleratchet "github.com/status-im/doubleratchet" +) + +// drLocalPair implements doubleratchet.DHPair using raw byte slices. +type drLocalPair struct { + priv doubleratchet.Key + pub doubleratchet.Key +} + +func (p drLocalPair) PrivateKey() doubleratchet.Key { return p.priv } +func (p drLocalPair) PublicKey() doubleratchet.Key { return p.pub } + +// serializedDRState is an intermediate JSON-friendly representation of doubleratchet.State. +type serializedDRState struct { + DHrPublic []byte `json:"dhr_pub"` + DHsPrivate []byte `json:"dhs_priv"` + DHsPublic []byte `json:"dhs_pub"` + RootChCK []byte `json:"root_ch_ck"` + SendChCK []byte `json:"send_ch_ck"` + SendChN uint32 `json:"send_ch_n"` + RecvChCK []byte `json:"recv_ch_ck"` + RecvChN uint32 `json:"recv_ch_n"` + PN uint32 `json:"pn"` + MkSkipped map[string]map[uint][]byte `json:"mk_skipped"` + MaxSkip uint `json:"max_skip"` + MaxKeep uint `json:"max_keep"` + MaxMessageKeysPerSession int `json:"max_mks_per_session"` + Step uint `json:"step"` + KeysCount uint `json:"keys_count"` +} + +// drSessionStorage implements doubleratchet.SessionStorage, persisting state into peer.DrStateJson. +type drSessionStorage struct{ peer *Peer } + +func (s *drSessionStorage) Save(id []byte, state *doubleratchet.State) error { + all, err := state.MkSkipped.All() + if err != nil { + return fmt.Errorf("drSessionStorage.Save: MkSkipped.All: %w", err) + } + mkSkipped := make(map[string]map[uint][]byte, len(all)) + for k, msgs := range all { + inner := make(map[uint][]byte, len(msgs)) + for num, mk := range msgs { + inner[num] = []byte(mk) + } + mkSkipped[k] = inner + } + + ss := serializedDRState{ + DHrPublic: []byte(state.DHr), + DHsPrivate: []byte(state.DHs.PrivateKey()), + DHsPublic: []byte(state.DHs.PublicKey()), + RootChCK: []byte(state.RootCh.CK), + SendChCK: []byte(state.SendCh.CK), + SendChN: state.SendCh.N, + RecvChCK: []byte(state.RecvCh.CK), + RecvChN: state.RecvCh.N, + PN: state.PN, + MkSkipped: mkSkipped, + MaxSkip: state.MaxSkip, + MaxKeep: state.MaxKeep, + MaxMessageKeysPerSession: state.MaxMessageKeysPerSession, + Step: state.Step, + KeysCount: state.KeysCount, + } + + b, err := json.Marshal(ss) + if err != nil { + return fmt.Errorf("drSessionStorage.Save: json.Marshal: %w", err) + } + s.peer.DrStateJson = string(b) + return nil +} + +func (s *drSessionStorage) Load(id []byte) (*doubleratchet.State, error) { + if s.peer.DrStateJson == "" { + return nil, nil + } + + var ss serializedDRState + if err := json.Unmarshal([]byte(s.peer.DrStateJson), &ss); err != nil { + return nil, fmt.Errorf("drSessionStorage.Load: json.Unmarshal: %w", err) + } + + c := doubleratchet.DefaultCrypto{} + mkStorage := &doubleratchet.KeysStorageInMemory{} + seq := uint(0) + for k, msgs := range ss.MkSkipped { + pubKey, err := hex.DecodeString(k) + if err != nil { + return nil, fmt.Errorf("drSessionStorage.Load: decode skipped key hex: %w", err) + } + for num, mk := range msgs { + if err := mkStorage.Put(id, doubleratchet.Key(pubKey), num, doubleratchet.Key(mk), seq); err != nil { + return nil, fmt.Errorf("drSessionStorage.Load: Put: %w", err) + } + seq++ + } + } + + state := &doubleratchet.State{ + Crypto: c, + DHr: doubleratchet.Key(ss.DHrPublic), + DHs: drLocalPair{priv: doubleratchet.Key(ss.DHsPrivate), pub: doubleratchet.Key(ss.DHsPublic)}, + PN: ss.PN, + MkSkipped: mkStorage, + MaxSkip: ss.MaxSkip, + MaxKeep: ss.MaxKeep, + MaxMessageKeysPerSession: ss.MaxMessageKeysPerSession, + Step: ss.Step, + KeysCount: ss.KeysCount, + } + state.RootCh.CK = doubleratchet.Key(ss.RootChCK) + state.RootCh.Crypto = c + state.SendCh.CK = doubleratchet.Key(ss.SendChCK) + state.SendCh.N = ss.SendChN + state.SendCh.Crypto = c + state.RecvCh.CK = doubleratchet.Key(ss.RecvChCK) + state.RecvCh.N = ss.RecvChN + state.RecvCh.Crypto = c + + return state, nil +} + +// GetDRSession returns an active DR session for the peer, creating one if needed. +func (p *Peer) GetDRSession() (doubleratchet.Session, error) { + store := &drSessionStorage{peer: p} + + // If we already have a saved state, load it + if p.DrStateJson != "" { + return doubleratchet.Load([]byte(p.Uid), store) + } + + // Initiator: has own DH keypair + root key, no state yet + if p.DrInitiator && p.DrKpPrivate != "" { + privBytes, err := base64.StdEncoding.DecodeString(p.DrKpPrivate) + if err != nil { + return nil, fmt.Errorf("GetDRSession: decode DrKpPrivate: %w", err) + } + pubBytes, err := base64.StdEncoding.DecodeString(p.DrKpPublic) + if err != nil { + return nil, fmt.Errorf("GetDRSession: decode DrKpPublic: %w", err) + } + rootKeyBytes, err := base64.StdEncoding.DecodeString(p.DrRootKey) + if err != nil { + return nil, fmt.Errorf("GetDRSession: decode DrRootKey: %w", err) + } + kp := drLocalPair{priv: doubleratchet.Key(privBytes), pub: doubleratchet.Key(pubBytes)} + return doubleratchet.New([]byte(p.Uid), doubleratchet.Key(rootKeyBytes), kp, store) + } + + // Responder: has remote DH public key + root key + if !p.DrInitiator && p.ContactDrPublicKey != "" { + remotePubBytes, err := base64.StdEncoding.DecodeString(p.ContactDrPublicKey) + if err != nil { + return nil, fmt.Errorf("GetDRSession: decode ContactDrPublicKey: %w", err) + } + rootKeyBytes, err := base64.StdEncoding.DecodeString(p.DrRootKey) + if err != nil { + return nil, fmt.Errorf("GetDRSession: decode DrRootKey: %w", err) + } + return doubleratchet.NewWithRemoteKey([]byte(p.Uid), doubleratchet.Key(rootKeyBytes), doubleratchet.Key(remotePubBytes), store) + } + + return nil, fmt.Errorf("GetDRSession: peer %s has no DR keys configured", p.Uid) +} diff --git a/client/helpers/bgPollHelper.go b/client/helpers/bgPollHelper.go index 92f20ad..77f467a 100644 --- a/client/helpers/bgPollHelper.go +++ b/client/helpers/bgPollHelper.go @@ -143,7 +143,7 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error return nil, nil, "ReadMessage: GetFromMyLookupKey", errors.New("no visible peer for that message") } // Unpack the message - usermsg, err := peer.ProcessInboundUserMessage(packedUserMessage.Payload, packedUserMessage.Signature) + usermsg, err := peer.ProcessInboundUserMessage(packedUserMessage) if err != nil { return nil, nil, "ReadMessage: ProcessInboundUserMessage", err } @@ -188,6 +188,13 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error } filenames = []string{} + // Persist peer to save updated DR state (DrStateJson) + if peer.DrRootKey != "" { + if storeErr := identity.Peers.StorePeer(peer); storeErr != nil { + logger.Warn().Err(storeErr).Str("peer", peer.Uid).Msg("ConsumeInboxFile: StorePeer (DR state)") + } + } + // Send delivery ack if the peer requested it if peer.SendDeliveryAck && usermsg.Status.Uuid != "" { storagePath := filepath.Join(client.GetConfig().StoragePath, identity.Uuid) diff --git a/client/helpers/invitationFinalizeHelper.go b/client/helpers/invitationFinalizeHelper.go index d9b1120..f0bfb48 100644 --- a/client/helpers/invitationFinalizeHelper.go +++ b/client/helpers/invitationFinalizeHelper.go @@ -28,7 +28,7 @@ func invitationGetAnswerReadResponse(invitation *meowlib.Invitation) (*client.Pe if peer != nil { // process the packed user message - usermsg, err := peer.ProcessInboundUserMessage(invitationAnswer.Payload, invitationAnswer.Signature) + usermsg, err := peer.ProcessInboundUserMessage(&invitationAnswer) if err != nil { return nil, "InvitationGetAnswerReadResponse: ProcessInboundUserMessage", err } diff --git a/client/helpers/messageHelper.go b/client/helpers/messageHelper.go index 1372c46..6de4e22 100644 --- a/client/helpers/messageHelper.go +++ b/client/helpers/messageHelper.go @@ -81,6 +81,13 @@ func CreateAndStoreUserMessage(message string, peer_uid string, replyToUid strin return nil, "", 0, "messageBuildPostprocess : ProcessOutboundUserMessage", err } + // Persist peer to save updated DR state (DrStateJson) + if peer.DrRootKey != "" { + if storeErr := client.GetConfig().GetIdentity().Peers.StorePeer(peer); storeErr != nil { + logger.Warn().Err(storeErr).Str("peer", peer.Uid).Msg("messageBuildPostprocess: StorePeer (DR state)") + } + } + return packedMsg, dbFile, dbId, "", nil } diff --git a/client/identity.go b/client/identity.go index 34f42cd..6f0dd71 100644 --- a/client/identity.go +++ b/client/identity.go @@ -15,6 +15,7 @@ import ( "forge.redroom.link/yves/meowlib" "github.com/ProtonMail/gopenpgp/v2/helper" "github.com/google/uuid" + doubleratchet "github.com/status-im/doubleratchet" ) const maxHiddenCount = 30 @@ -122,6 +123,21 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerU peer.MyPullServers = MessageServerUids peer.MyName = MyName peer.InvitationMessage = InvitationMessage + + // Generate DR keypair and root key for the initiator side + drKp, err := doubleratchet.DefaultCrypto{}.GenerateDH() + if err != nil { + return 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 @@ -187,6 +203,10 @@ func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageS peer.MyPullServers = MessageServerIdxs 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 diff --git a/client/peer.go b/client/peer.go index c2d993e..dd1a203 100644 --- a/client/peer.go +++ b/client/peer.go @@ -1,11 +1,13 @@ package client import ( + "encoding/json" "io" "os" "time" "forge.redroom.link/yves/meowlib" + doubleratchet "github.com/status-im/doubleratchet" "github.com/google/uuid" "google.golang.org/protobuf/proto" ) @@ -54,7 +56,14 @@ type Peer struct { DbIds []string `json:"db_ids,omitempty"` Type string `json:"type,omitempty"` PersonnaeDbId string `json:"personnae_db_id,omitempty"` - dbPassword string + // Double Ratchet state + 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"` + ContactDrPublicKey string `json:"contact_dr_public_key,omitempty"` + DrStateJson string `json:"dr_state_json,omitempty"` + dbPassword string } // @@ -74,6 +83,8 @@ func (p *Peer) GetMyContact() *meowlib.ContactCard { c.InvitationMessage = p.InvitationMessage c.Name = p.MyName c.SymetricKey = p.MySymKey + c.DrRootKey = p.DrRootKey + c.DrPublicKey = p.DrKpPublic return &c } @@ -283,24 +294,61 @@ func (p *Peer) ProcessOutboundUserMessage(usermessage *meowlib.UserMessage) (*me if err != nil { return nil, err } - // Symmetric encryption (outer layer, if symkey is configured) + // Symmetric encryption (middle layer, if symkey is configured) symEncrypted, err := p.SymEncryptPayload(enc.Data) if err != nil { return nil, err } - // Packing it + // Double Ratchet encryption (outermost layer, if DR is configured) + if p.DrRootKey != "" { + session, err := p.GetDRSession() + if err != nil { + return nil, err + } + drMsg, err := session.RatchetEncrypt(symEncrypted, nil) + if err != nil { + return nil, err + } + headerBytes, err := json.Marshal(drMsg.Header) + if err != nil { + return nil, err + } + packed := p.PackUserMessage(drMsg.Ciphertext, enc.Signature) + packed.DrHeader = headerBytes + return packed, nil + } + // No DR layer packedMsg := p.PackUserMessage(symEncrypted, enc.Signature) return packedMsg, nil } // ProcessInboundUserMessage is a helper function that decrypts and deserializes a user message -func (p *Peer) ProcessInboundUserMessage(message []byte, signature []byte) (*meowlib.UserMessage, error) { - // Symmetric decryption (outer layer, if symkey is configured) - symDecrypted, err := p.SymDecryptPayload(message) +func (p *Peer) ProcessInboundUserMessage(packed *meowlib.PackedUserMessage) (*meowlib.UserMessage, error) { + payload := packed.Payload + // Double Ratchet decryption (outermost layer), only when DR is configured and header present + if p.DrRootKey != "" && len(packed.DrHeader) > 0 { + session, err := p.GetDRSession() + if err != nil { + return nil, err + } + var header doubleratchet.MessageHeader + if err := json.Unmarshal(packed.DrHeader, &header); err != nil { + return nil, err + } + payload, err = session.RatchetDecrypt( + doubleratchet.Message{Header: header, Ciphertext: packed.Payload}, + nil, + ) + if err != nil { + return nil, err + } + } + // Symmetric decryption (middle layer, if symkey is configured) + symDecrypted, err := p.SymDecryptPayload(payload) if err != nil { return nil, err } - dec, err := p.AsymDecryptMessage(symDecrypted, signature) + dec, err := p.AsymDecryptMessage(symDecrypted, packed.Signature) if err != nil { return nil, err } diff --git a/client/peer_test.go b/client/peer_test.go index f8358cd..3383553 100644 --- a/client/peer_test.go +++ b/client/peer_test.go @@ -1,11 +1,14 @@ package client import ( + "crypto/rand" + "encoding/base64" "os" "strconv" "testing" "forge.redroom.link/yves/meowlib" + doubleratchet "github.com/status-im/doubleratchet" "github.com/google/uuid" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/proto" @@ -421,7 +424,7 @@ func TestProcessOutboundInbound_RoundTrip(t *testing.T) { assert.NotEmpty(t, packed.Signature) assert.Equal(t, bob.MyLookupKp.Public, packed.Destination) - received, err := bob.ProcessInboundUserMessage(packed.Payload, packed.Signature) + received, err := bob.ProcessInboundUserMessage(packed) assert.NoError(t, err) assert.Equal(t, []byte("end to end test"), received.Data) assert.Equal(t, alice.MyIdentity.Public, received.From) @@ -436,7 +439,7 @@ func TestProcessOutboundInbound_EmptyMessage(t *testing.T) { packed, err := alice.ProcessOutboundUserMessage(userMsg) assert.NoError(t, err) - received, err := bob.ProcessInboundUserMessage(packed.Payload, packed.Signature) + received, err := bob.ProcessInboundUserMessage(packed) assert.NoError(t, err) assert.Empty(t, received.Data) } @@ -452,6 +455,74 @@ func TestProcessOutboundUserMessage_InvalidKey(t *testing.T) { assert.Error(t, err) } +// --------------------------------------------------------------------------- +// DR-encrypted round-trip +// --------------------------------------------------------------------------- + +func makeDRPeerPair(t *testing.T) (alice *Peer, bob *Peer) { + t.Helper() + alice, bob = makePeerPair(t) + + // Generate DR keypair for alice (initiator) + drKp, err := doubleratchet.DefaultCrypto{}.GenerateDH() + if err != nil { + t.Fatal(err) + } + drRootKeyBytes := make([]byte, 32) + if _, err = rand.Read(drRootKeyBytes); err != nil { + t.Fatal(err) + } + drRootKey := base64.StdEncoding.EncodeToString(drRootKeyBytes) + + alice.DrKpPrivate = base64.StdEncoding.EncodeToString(drKp.PrivateKey()) + alice.DrKpPublic = base64.StdEncoding.EncodeToString(drKp.PublicKey()) + alice.DrRootKey = drRootKey + alice.DrInitiator = true + + bob.DrRootKey = drRootKey + bob.ContactDrPublicKey = alice.DrKpPublic + bob.DrInitiator = false + + return alice, bob +} + +func TestProcessOutboundInbound_DR_RoundTrip(t *testing.T) { + alice, bob := makeDRPeerPair(t) + + userMsg, err := alice.BuildSimpleUserMessage([]byte("dr round trip test")) + assert.NoError(t, err) + + packed, err := alice.ProcessOutboundUserMessage(userMsg) + assert.NoError(t, err) + assert.NotEmpty(t, packed.DrHeader, "DR header should be set") + + received, err := bob.ProcessInboundUserMessage(packed) + assert.NoError(t, err) + assert.Equal(t, []byte("dr round trip test"), received.Data) + + // Verify DR state was updated + assert.NotEmpty(t, alice.DrStateJson, "alice DR state should be persisted") + assert.NotEmpty(t, bob.DrStateJson, "bob DR state should be persisted") +} + +func TestProcessOutboundInbound_DR_MultipleMessages(t *testing.T) { + alice, bob := makeDRPeerPair(t) + + for i := 0; i < 5; i++ { + msg := []byte("message " + strconv.Itoa(i)) + userMsg, err := alice.BuildSimpleUserMessage(msg) + assert.NoError(t, err) + + packed, err := alice.ProcessOutboundUserMessage(userMsg) + assert.NoError(t, err) + assert.NotEmpty(t, packed.DrHeader) + + received, err := bob.ProcessInboundUserMessage(packed) + assert.NoError(t, err) + assert.Equal(t, msg, received.Data) + } +} + // --------------------------------------------------------------------------- // GetConversationRequest // --------------------------------------------------------------------------- diff --git a/go.mod b/go.mod index 9e54b9e..85f8101 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.30.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/status-im/doubleratchet v3.0.0+incompatible // indirect github.com/twitchtv/twirp v8.1.3+incompatible // indirect github.com/yuin/gopher-lua v1.1.1 // indirect golang.org/x/crypto v0.41.0 // indirect diff --git a/go.sum b/go.sum index 10627ed..1c5ca80 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,8 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/status-im/doubleratchet v3.0.0+incompatible h1:aJ1ejcSERpSzmWZBgtfYtiU2nF0Q8ZkGyuEPYETXkCY= +github.com/status-im/doubleratchet v3.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/messages.pb.go b/messages.pb.go index 667c425..deca035 100644 --- a/messages.pb.go +++ b/messages.pb.go @@ -907,6 +907,8 @@ type ContactCard struct { Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty"` InvitationId string `protobuf:"bytes,8,opt,name=invitation_id,json=invitationId,proto3" json:"invitation_id,omitempty"` InvitationMessage string `protobuf:"bytes,9,opt,name=invitation_message,json=invitationMessage,proto3" json:"invitation_message,omitempty"` + DrRootKey string `protobuf:"bytes,10,opt,name=dr_root_key,json=drRootKey,proto3" json:"dr_root_key,omitempty"` // DR pre-shared root key (base64, 32 bytes) + DrPublicKey string `protobuf:"bytes,11,opt,name=dr_public_key,json=drPublicKey,proto3" json:"dr_public_key,omitempty"` // DR DH public key of the initiator (base64) unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1004,6 +1006,20 @@ func (x *ContactCard) GetInvitationMessage() string { return "" } +func (x *ContactCard) GetDrRootKey() string { + if x != nil { + return x.DrRootKey + } + return "" +} + +func (x *ContactCard) GetDrPublicKey() string { + if x != nil { + return x.DrPublicKey + } + return "" +} + // structure for sending a message to be forwarded to another user in protobuf format type PackedUserMessage struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1012,6 +1028,7 @@ type PackedUserMessage struct { Signature []byte `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` // the payload signature with the client identity private key ServerTimestamp []int64 `protobuf:"varint,4,rep,packed,name=server_timestamp,json=serverTimestamp,proto3" json:"server_timestamp,omitempty"` // server time stamp, might be several in matriochka mode ServerDeliveryUuid string `protobuf:"bytes,5,opt,name=server_delivery_uuid,json=serverDeliveryUuid,proto3" json:"server_delivery_uuid,omitempty"` // message uuid, for server delivery tracking, omitted if not delivery tracking desired + DrHeader []byte `protobuf:"bytes,6,opt,name=dr_header,json=drHeader,proto3" json:"dr_header,omitempty"` // serialized doubleratchet MessageHeader; empty = no DR layer unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1081,6 +1098,13 @@ func (x *PackedUserMessage) GetServerDeliveryUuid() string { return "" } +func (x *PackedUserMessage) GetDrHeader() []byte { + if x != nil { + return x.DrHeader + } + return nil +} + type ConversationStatus struct { state protoimpl.MessageState `protogen:"open.v1"` Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` // uuid of message, or uuid of related message if uuid_action is not empty @@ -1888,7 +1912,7 @@ const file_messages_proto_rawDesc = "" + "\x03url\x18\x04 \x01(\tR\x03url\x12\x14\n" + "\x05login\x18\x05 \x01(\tR\x05login\x12\x1a\n" + "\bpassword\x18\x06 \x01(\tR\bpassword\x12\x1c\n" + - "\tsignature\x18\a \x01(\tR\tsignature\"\xf8\x02\n" + + "\tsignature\x18\a \x01(\tR\tsignature\"\xbc\x03\n" + "\vContactCard\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12,\n" + "\x12contact_public_key\x18\x02 \x01(\tR\x10contactPublicKey\x122\n" + @@ -1898,13 +1922,17 @@ const file_messages_proto_rawDesc = "" + "\fpull_servers\x18\x06 \x03(\v2\x13.meowlib.ServerCardR\vpullServers\x12\x18\n" + "\aversion\x18\a \x01(\rR\aversion\x12#\n" + "\rinvitation_id\x18\b \x01(\tR\finvitationId\x12-\n" + - "\x12invitation_message\x18\t \x01(\tR\x11invitationMessage\"\xca\x01\n" + + "\x12invitation_message\x18\t \x01(\tR\x11invitationMessage\x12\x1e\n" + + "\vdr_root_key\x18\n" + + " \x01(\tR\tdrRootKey\x12\"\n" + + "\rdr_public_key\x18\v \x01(\tR\vdrPublicKey\"\xe7\x01\n" + "\x11PackedUserMessage\x12 \n" + "\vdestination\x18\x01 \x01(\tR\vdestination\x12\x18\n" + "\apayload\x18\x02 \x01(\fR\apayload\x12\x1c\n" + "\tsignature\x18\x03 \x01(\fR\tsignature\x12)\n" + "\x10server_timestamp\x18\x04 \x03(\x03R\x0fserverTimestamp\x120\n" + - "\x14server_delivery_uuid\x18\x05 \x01(\tR\x12serverDeliveryUuid\"\xd7\x02\n" + + "\x14server_delivery_uuid\x18\x05 \x01(\tR\x12serverDeliveryUuid\x12\x1b\n" + + "\tdr_header\x18\x06 \x01(\fR\bdrHeader\"\xd7\x02\n" + "\x12ConversationStatus\x12\x12\n" + "\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1f\n" + "\vuuid_action\x18\x02 \x01(\x05R\n" + diff --git a/pb/messages.proto b/pb/messages.proto index f19cb86..2fa7ec9 100644 --- a/pb/messages.proto +++ b/pb/messages.proto @@ -138,6 +138,8 @@ message ContactCard { uint32 version = 7; string invitation_id = 8; string invitation_message = 9; + string dr_root_key = 10; // DR pre-shared root key (base64, 32 bytes) + string dr_public_key = 11; // DR DH public key of the initiator (base64) } // structure for sending a message to be forwarded to another user in protobuf format @@ -147,6 +149,7 @@ message PackedUserMessage { bytes signature = 3; // the payload signature with the client identity private key repeated int64 server_timestamp = 4; // server time stamp, might be several in matriochka mode string server_delivery_uuid = 5; // message uuid, for server delivery tracking, omitted if not delivery tracking desired + bytes dr_header = 6; // serialized doubleratchet MessageHeader; empty = no DR layer } message ConversationStatus {