double ratchet first implementation
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
174
client/drsession.go
Normal file
174
client/drsession.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
return nil, nil, "ReadMessage: GetFromMyLookupKey", errors.New("no visible peer for that message")
|
||||||
}
|
}
|
||||||
// Unpack the message
|
// Unpack the message
|
||||||
usermsg, err := peer.ProcessInboundUserMessage(packedUserMessage.Payload, packedUserMessage.Signature)
|
usermsg, err := peer.ProcessInboundUserMessage(packedUserMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "ReadMessage: ProcessInboundUserMessage", err
|
return nil, nil, "ReadMessage: ProcessInboundUserMessage", err
|
||||||
}
|
}
|
||||||
@@ -188,6 +188,13 @@ func ConsumeInboxFile(messageFilename string) ([]string, []string, string, error
|
|||||||
}
|
}
|
||||||
filenames = []string{}
|
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
|
// Send delivery ack if the peer requested it
|
||||||
if peer.SendDeliveryAck && usermsg.Status.Uuid != "" {
|
if peer.SendDeliveryAck && usermsg.Status.Uuid != "" {
|
||||||
storagePath := filepath.Join(client.GetConfig().StoragePath, identity.Uuid)
|
storagePath := filepath.Join(client.GetConfig().StoragePath, identity.Uuid)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func invitationGetAnswerReadResponse(invitation *meowlib.Invitation) (*client.Pe
|
|||||||
if peer != nil {
|
if peer != nil {
|
||||||
|
|
||||||
// process the packed user message
|
// process the packed user message
|
||||||
usermsg, err := peer.ProcessInboundUserMessage(invitationAnswer.Payload, invitationAnswer.Signature)
|
usermsg, err := peer.ProcessInboundUserMessage(&invitationAnswer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "InvitationGetAnswerReadResponse: ProcessInboundUserMessage", err
|
return nil, "InvitationGetAnswerReadResponse: ProcessInboundUserMessage", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,13 @@ func CreateAndStoreUserMessage(message string, peer_uid string, replyToUid strin
|
|||||||
return nil, "", 0, "messageBuildPostprocess : ProcessOutboundUserMessage", err
|
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
|
return packedMsg, dbFile, dbId, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"forge.redroom.link/yves/meowlib"
|
"forge.redroom.link/yves/meowlib"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/helper"
|
"github.com/ProtonMail/gopenpgp/v2/helper"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
doubleratchet "github.com/status-im/doubleratchet"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxHiddenCount = 30
|
const maxHiddenCount = 30
|
||||||
@@ -122,6 +123,21 @@ func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerU
|
|||||||
peer.MyPullServers = MessageServerUids
|
peer.MyPullServers = MessageServerUids
|
||||||
peer.MyName = MyName
|
peer.MyName = MyName
|
||||||
peer.InvitationMessage = InvitationMessage
|
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)
|
id.Peers.StorePeer(&peer)
|
||||||
|
|
||||||
return &peer, nil
|
return &peer, nil
|
||||||
@@ -187,6 +203,10 @@ func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageS
|
|||||||
peer.MyPullServers = MessageServerIdxs
|
peer.MyPullServers = MessageServerIdxs
|
||||||
peer.MyName = MyName
|
peer.MyName = MyName
|
||||||
peer.InvitationId = ReceivedContact.InvitationId
|
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)
|
id.Peers.StorePeer(&peer)
|
||||||
|
|
||||||
return &peer, nil
|
return &peer, nil
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"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"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -54,6 +56,13 @@ type Peer struct {
|
|||||||
DbIds []string `json:"db_ids,omitempty"`
|
DbIds []string `json:"db_ids,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
PersonnaeDbId string `json:"personnae_db_id,omitempty"`
|
PersonnaeDbId string `json:"personnae_db_id,omitempty"`
|
||||||
|
// 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
|
dbPassword string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +83,8 @@ func (p *Peer) GetMyContact() *meowlib.ContactCard {
|
|||||||
c.InvitationMessage = p.InvitationMessage
|
c.InvitationMessage = p.InvitationMessage
|
||||||
c.Name = p.MyName
|
c.Name = p.MyName
|
||||||
c.SymetricKey = p.MySymKey
|
c.SymetricKey = p.MySymKey
|
||||||
|
c.DrRootKey = p.DrRootKey
|
||||||
|
c.DrPublicKey = p.DrKpPublic
|
||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,24 +294,61 @@ func (p *Peer) ProcessOutboundUserMessage(usermessage *meowlib.UserMessage) (*me
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
symEncrypted, err := p.SymEncryptPayload(enc.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
packedMsg := p.PackUserMessage(symEncrypted, enc.Signature)
|
||||||
return packedMsg, nil
|
return packedMsg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessInboundUserMessage is a helper function that decrypts and deserializes a user message
|
// ProcessInboundUserMessage is a helper function that decrypts and deserializes a user message
|
||||||
func (p *Peer) ProcessInboundUserMessage(message []byte, signature []byte) (*meowlib.UserMessage, error) {
|
func (p *Peer) ProcessInboundUserMessage(packed *meowlib.PackedUserMessage) (*meowlib.UserMessage, error) {
|
||||||
// Symmetric decryption (outer layer, if symkey is configured)
|
payload := packed.Payload
|
||||||
symDecrypted, err := p.SymDecryptPayload(message)
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dec, err := p.AsymDecryptMessage(symDecrypted, signature)
|
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, packed.Signature)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"forge.redroom.link/yves/meowlib"
|
"forge.redroom.link/yves/meowlib"
|
||||||
|
doubleratchet "github.com/status-im/doubleratchet"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -421,7 +424,7 @@ func TestProcessOutboundInbound_RoundTrip(t *testing.T) {
|
|||||||
assert.NotEmpty(t, packed.Signature)
|
assert.NotEmpty(t, packed.Signature)
|
||||||
assert.Equal(t, bob.MyLookupKp.Public, packed.Destination)
|
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.NoError(t, err)
|
||||||
assert.Equal(t, []byte("end to end test"), received.Data)
|
assert.Equal(t, []byte("end to end test"), received.Data)
|
||||||
assert.Equal(t, alice.MyIdentity.Public, received.From)
|
assert.Equal(t, alice.MyIdentity.Public, received.From)
|
||||||
@@ -436,7 +439,7 @@ func TestProcessOutboundInbound_EmptyMessage(t *testing.T) {
|
|||||||
packed, err := alice.ProcessOutboundUserMessage(userMsg)
|
packed, err := alice.ProcessOutboundUserMessage(userMsg)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
received, err := bob.ProcessInboundUserMessage(packed.Payload, packed.Signature)
|
received, err := bob.ProcessInboundUserMessage(packed)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, received.Data)
|
assert.Empty(t, received.Data)
|
||||||
}
|
}
|
||||||
@@ -452,6 +455,74 @@ func TestProcessOutboundUserMessage_InvalidKey(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
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
|
// GetConversationRequest
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -40,6 +40,7 @@ require (
|
|||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
github.com/onsi/gomega v1.30.0 // indirect
|
github.com/onsi/gomega v1.30.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.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/twitchtv/twirp v8.1.3+incompatible // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
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/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/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
|||||||
@@ -907,6 +907,8 @@ type ContactCard struct {
|
|||||||
Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty"`
|
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"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1006,20 @@ func (x *ContactCard) GetInvitationMessage() string {
|
|||||||
return ""
|
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
|
// structure for sending a message to be forwarded to another user in protobuf format
|
||||||
type PackedUserMessage struct {
|
type PackedUserMessage struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
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
|
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
|
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
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -1081,6 +1098,13 @@ func (x *PackedUserMessage) GetServerDeliveryUuid() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *PackedUserMessage) GetDrHeader() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.DrHeader
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ConversationStatus struct {
|
type ConversationStatus struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
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
|
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" +
|
"\x03url\x18\x04 \x01(\tR\x03url\x12\x14\n" +
|
||||||
"\x05login\x18\x05 \x01(\tR\x05login\x12\x1a\n" +
|
"\x05login\x18\x05 \x01(\tR\x05login\x12\x1a\n" +
|
||||||
"\bpassword\x18\x06 \x01(\tR\bpassword\x12\x1c\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" +
|
"\vContactCard\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12,\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12,\n" +
|
||||||
"\x12contact_public_key\x18\x02 \x01(\tR\x10contactPublicKey\x122\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" +
|
"\fpull_servers\x18\x06 \x03(\v2\x13.meowlib.ServerCardR\vpullServers\x12\x18\n" +
|
||||||
"\aversion\x18\a \x01(\rR\aversion\x12#\n" +
|
"\aversion\x18\a \x01(\rR\aversion\x12#\n" +
|
||||||
"\rinvitation_id\x18\b \x01(\tR\finvitationId\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" +
|
"\x11PackedUserMessage\x12 \n" +
|
||||||
"\vdestination\x18\x01 \x01(\tR\vdestination\x12\x18\n" +
|
"\vdestination\x18\x01 \x01(\tR\vdestination\x12\x18\n" +
|
||||||
"\apayload\x18\x02 \x01(\fR\apayload\x12\x1c\n" +
|
"\apayload\x18\x02 \x01(\fR\apayload\x12\x1c\n" +
|
||||||
"\tsignature\x18\x03 \x01(\fR\tsignature\x12)\n" +
|
"\tsignature\x18\x03 \x01(\fR\tsignature\x12)\n" +
|
||||||
"\x10server_timestamp\x18\x04 \x03(\x03R\x0fserverTimestamp\x120\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" +
|
"\x12ConversationStatus\x12\x12\n" +
|
||||||
"\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1f\n" +
|
"\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1f\n" +
|
||||||
"\vuuid_action\x18\x02 \x01(\x05R\n" +
|
"\vuuid_action\x18\x02 \x01(\x05R\n" +
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ message ContactCard {
|
|||||||
uint32 version = 7;
|
uint32 version = 7;
|
||||||
string invitation_id = 8;
|
string invitation_id = 8;
|
||||||
string invitation_message = 9;
|
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
|
// 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
|
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
|
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
|
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 {
|
message ConversationStatus {
|
||||||
|
|||||||
Reference in New Issue
Block a user