diff --git a/bindings/broadcast.go b/bindings/broadcast.go new file mode 100644 index 0000000000000000000000000000000000000000..d2773904333e46902c85166f66ec5a05a4e3ee91 --- /dev/null +++ b/bindings/broadcast.go @@ -0,0 +1,185 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "encoding/json" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/broadcast" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id/ephemeral" +) + +// Channel is a bindings-level struct encapsulating the broadcast.Channel client object. +type Channel struct { + ch broadcast.Channel +} + +// ChannelDef is the bindings representation of an elixxir/crypto broadcast.Channel object. +// +// Example JSON: +// {"Name": "My broadcast channel", +// "Description":"A broadcast channel for me to test things", +// "Salt":"gpUqW7N22sffMXsvPLE7BA==", +// "PubKey":"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1DZ0NJUUN2YkZVckJKRFpqT3Y0Y0MvUHZZdXNvQkFtUTFkb3Znb044aHRuUjA2T3F3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0=" +// } +type ChannelDef struct { + Name string + Description string + Salt []byte + PubKey []byte +} + +// BroadcastMessage is the bindings representation of a broadcast message. +// +// Example JSON: +// {"RoundID":42, +// "EphID":[0,0,0,0,0,0,24,61], +// "Payload":"SGVsbG8sIGJyb2FkY2FzdCBmcmllbmRzIQ==" +// } +type BroadcastMessage struct { + BroadcastReport + Payload []byte +} + +// BroadcastReport is the bindings representation of the info on how a broadcast message was sent +// +// Example JSON: +// {"RoundID":42, +// "EphID":[0,0,0,0,0,0,24,61] +// } +type BroadcastReport struct { + RoundID int + EphID ephemeral.Id +} + +// BroadcastListener is the public function type bindings can use to listen for broadcast messages. +// It accepts the result of calling json.Marshal on a BroadcastMessage object. +type BroadcastListener interface { + Callback([]byte, error) +} + +// NewBroadcastChannel creates a bindings-layer broadcast channel & starts listening for new messages +// +// Params +// - cmixId - internal ID of cmix +// - channelDefinition - JSON marshalled ChannelDef object +func NewBroadcastChannel(cmixId int, channelDefinition []byte) (*Channel, error) { + c, err := cmixTrackerSingleton.get(cmixId) + if err != nil { + return nil, err + } + + def := &ChannelDef{} + err = json.Unmarshal(channelDefinition, def) + if err != nil { + return nil, errors.WithMessage(err, "Failed to unmarshal underlying channel definition") + } + + channelID, err := cryptoBroadcast.NewChannelID(def.Name, def.Description, def.Salt, def.PubKey) + if err != nil { + return nil, errors.WithMessage(err, "Failed to generate channel ID") + } + chanPubLoaded, err := rsa.LoadPublicKeyFromPem(def.PubKey) + if err != nil { + return nil, errors.WithMessage(err, "Failed to load public key") + } + + ch, err := broadcast.NewBroadcastChannel(cryptoBroadcast.Channel{ + ReceptionID: channelID, + Name: def.Name, + Description: def.Description, + Salt: def.Salt, + RsaPubKey: chanPubLoaded, + }, c.api.GetCmix(), c.api.GetRng()) + if err != nil { + return nil, errors.WithMessage(err, "Failed to create broadcast channel client") + } + + return &Channel{ch: ch}, nil +} + +// Listen registers a BroadcastListener for a given method. +// This allows users to handle incoming broadcast messages. +// +// Params: +// - l - BroadcastListener object +// - method - int corresponding to broadcast.Method constant, 0 for symmetric or 1 for asymmetric +func (c *Channel) Listen(l BroadcastListener, method int) error { + broadcastMethod := broadcast.Method(method) + listen := func(payload []byte, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { + l.Callback(json.Marshal(&BroadcastMessage{ + BroadcastReport: BroadcastReport{ + RoundID: int(round.ID), + EphID: receptionID.EphId, + }, + Payload: payload, + })) + } + return c.ch.RegisterListener(listen, broadcastMethod) +} + +// Broadcast sends a given payload over the broadcast channel using symmetric broadcast. +func (c *Channel) Broadcast(payload []byte) ([]byte, error) { + rid, eid, err := c.ch.Broadcast(payload, cmix.GetDefaultCMIXParams()) + if err != nil { + return nil, err + } + return json.Marshal(BroadcastReport{ + RoundID: int(rid), + EphID: eid, + }) +} + +// BroadcastAsymmetric sends a given payload over the broadcast channel using asymmetric broadcast. +// This mode of encryption requires a private key. +func (c *Channel) BroadcastAsymmetric(payload, pk []byte) ([]byte, error) { + pkLoaded, err := rsa.LoadPrivateKeyFromPem(pk) + if err != nil { + return nil, err + } + rid, eid, err := c.ch.BroadcastAsymmetric(pkLoaded, payload, cmix.GetDefaultCMIXParams()) + if err != nil { + return nil, err + } + return json.Marshal(BroadcastReport{ + RoundID: int(rid), + EphID: eid, + }) +} + +// MaxPayloadSize returns the maximum possible payload size which can be broadcast. +func (c *Channel) MaxPayloadSize() int { + return c.ch.MaxPayloadSize() +} + +// MaxAsymmetricPayloadSize returns the maximum possible payload size which can be broadcast. +func (c *Channel) MaxAsymmetricPayloadSize() int { + return c.ch.MaxAsymmetricPayloadSize() +} + +// Get returns the result of calling json.Marshal on a ChannelDef based on the underlying crypto broadcast.Channel. +func (c *Channel) Get() ([]byte, error) { + def := c.ch.Get() + return json.Marshal(&ChannelDef{ + Name: def.Name, + Description: def.Description, + Salt: def.Salt, + PubKey: rsa.CreatePublicKeyPem(def.RsaPubKey), + }) +} + +// Stop stops the channel from listening for more messages. +func (c *Channel) Stop() { + c.ch.Stop() +} diff --git a/bindings/broadcast_test.go b/bindings/broadcast_test.go new file mode 100644 index 0000000000000000000000000000000000000000..48fa0bed595de2664f5b5f011d129ed53c3a97e6 --- /dev/null +++ b/bindings/broadcast_test.go @@ -0,0 +1,68 @@ +package bindings + +import ( + "encoding/json" + "gitlab.com/elixxir/crypto/cmix" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "testing" + "time" +) + +func TestChannelDef_JSON(t *testing.T) { + rng := csprng.NewSystemRNG() + rng.SetSeed([]byte("rng")) + pk, _ := rsa.GenerateKey(rng, 256) + cd := ChannelDef{ + Name: "My broadcast channel", + Description: "A broadcast channel for me to test things", + Salt: cmix.NewSalt(rng, 16), + PubKey: rsa.CreatePublicKeyPem(pk.GetPublic()), + } + + cdJson, err := json.Marshal(cd) + if err != nil { + t.Errorf("Failed to marshal channel def: %+v", err) + } + t.Log(string(cdJson)) +} + +func TestBroadcastMessage_JSON(t *testing.T) { + uid := id.NewIdFromString("zezima", id.User, t) + eid, _, _, err := ephemeral.GetId(uid, 16, time.Now().UnixNano()) + if err != nil { + t.Errorf("Failed to form ephemeral ID: %+v", err) + } + bm := BroadcastMessage{ + BroadcastReport: BroadcastReport{ + RoundID: 42, + EphID: eid, + }, + Payload: []byte("Hello, broadcast friends!"), + } + bmJson, err := json.Marshal(bm) + if err != nil { + t.Errorf("Failed to marshal broadcast message: %+v", err) + } + t.Log(string(bmJson)) +} + +func TestBroadcastReport_JSON(t *testing.T) { + uid := id.NewIdFromString("zezima", id.User, t) + eid, _, _, err := ephemeral.GetId(uid, 16, time.Now().UnixNano()) + if err != nil { + t.Errorf("Failed to form ephemeral ID: %+v", err) + } + br := BroadcastReport{ + RoundID: 42, + EphID: eid, + } + + brJson, err := json.Marshal(br) + if err != nil { + t.Errorf("Failed to marshal broadcast report: %+v", err) + } + t.Log(string(brJson)) +} diff --git a/broadcast/asymmetric.go b/broadcast/asymmetric.go index 8fab1b5a24dad08ade68a369857f2dca06795c1e..2b263cad705cadcfd0f9fb5a819a1c019cb8bb4b 100644 --- a/broadcast/asymmetric.go +++ b/broadcast/asymmetric.go @@ -23,11 +23,6 @@ const ( internalPayloadSizeLength = 2 ) -// MaxAsymmetricPayloadSize returns the maximum size for an asymmetric broadcast payload -func (bc *broadcastClient) maxAsymmetricPayload() int { - return bc.maxParts() * bc.channel.MaxAsymmetricPayloadSize() -} - // BroadcastAsymmetric broadcasts the payload to the channel. Requires a healthy network state to send // Payload must be equal to bc.MaxAsymmetricPayloadSize, and the channel PrivateKey must be passed in func (bc *broadcastClient) BroadcastAsymmetric(pk multicastRSA.PrivateKey, payload []byte, cMixParams cmix.CMIXParams) ( @@ -40,7 +35,7 @@ func (bc *broadcastClient) BroadcastAsymmetric(pk multicastRSA.PrivateKey, paylo // Check payload size if len(payload) > bc.MaxAsymmetricPayloadSize() { return 0, ephemeral.Id{}, - errors.Errorf(errPayloadSize, len(payload), bc.maxAsymmetricPayload()) + errors.Errorf(errPayloadSize, len(payload), bc.MaxAsymmetricPayloadSize()) } payloadLength := uint16(len(payload)) @@ -54,7 +49,8 @@ func (bc *broadcastClient) BroadcastAsymmetric(pk multicastRSA.PrivateKey, paylo return 0, ephemeral.Id{}, errors.WithMessage(err, "Failed to encrypt asymmetric broadcast message") } - // Create service object to send message + // Create service using asymmetric broadcast service tag & channel reception ID + // Allows anybody with this info to listen for messages on this channel service := message.Service{ Identifier: bc.channel.ReceptionID.Bytes(), Tag: asymmetricBroadcastServiceTag, @@ -76,10 +72,3 @@ func (bc *broadcastClient) BroadcastAsymmetric(pk multicastRSA.PrivateKey, paylo return bc.net.Send( bc.channel.ReceptionID, fp, service, sizedPayload, mac, cMixParams) } - -// Helper function for maximum number of encrypted message parts -func (bc *broadcastClient) maxParts() int { - encPartSize := bc.channel.RsaPubKey.Size() - maxSend := bc.net.GetMaxMessageLength() - return maxSend / encPartSize -} diff --git a/broadcast/interface.go b/broadcast/interface.go index badfc8bc7e4009c59753d2fd39bf55b5d5043cad..bd464945549ba67b4800dcef5c1f4f9d339d061d 100644 --- a/broadcast/interface.go +++ b/broadcast/interface.go @@ -25,6 +25,7 @@ import ( type ListenerFunc func(payload []byte, receptionID receptionID.EphemeralIdentity, round rounds.Round) +// Channel is the public-facing interface to interact with broadcast channels type Channel interface { // MaxPayloadSize returns the maximum size for a symmetric broadcast payload MaxPayloadSize() int @@ -45,6 +46,7 @@ type Channel interface { BroadcastAsymmetric(pk multicastRSA.PrivateKey, payload []byte, cMixParams cmix.CMIXParams) ( id.Round, ephemeral.Id, error) + // RegisterListener registers a listener for broadcast messages RegisterListener(listenerCb ListenerFunc, method Method) error // Stop unregisters the listener callback and stops the channel's identity diff --git a/broadcast/symmetric.go b/broadcast/symmetric.go index e9b96f75c845dc0da8d91f87235d2e011db1cf2c..601c2bebec39f7c00c1e389a9f2d0d8ccc5122f3 100644 --- a/broadcast/symmetric.go +++ b/broadcast/symmetric.go @@ -53,7 +53,8 @@ func (bc *broadcastClient) Broadcast(payload []byte, cMixParams cmix.CMIXParams) encryptedPayload, mac, fp := bc.channel.EncryptSymmetric(payload, rng) rng.Close() - // Create service + // Create service using symmetric broadcast service tag & channel reception ID + // Allows anybody with this info to listen for messages on this channel service := message.Service{ Identifier: bc.channel.ReceptionID.Bytes(), Tag: symmetricBroadcastServiceTag,