diff --git a/bindings/channels.go b/bindings/channels.go index 2838fc10ec4109f6b813fd2b4f3a01dd62fdd455..3c0dfdfd53372c59cfdcd5688baa2aa5c3d43f20 100644 --- a/bindings/channels.go +++ b/bindings/channels.go @@ -10,6 +10,7 @@ package bindings import ( "encoding/json" "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/channels" "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/xxdk" @@ -97,104 +98,186 @@ func (cm *ChannelsManager) GetID() int { return cm.id } -// NewChannelsManager constructs a ChannelsManager. -// FIXME: This is a work in progress and should not be used an event model is -// implemented in the style of the bindings layer's AuthCallbacks. Remove this -// note when that has been done. -// -// Parameters: -// - e2eID - The tracked e2e object ID. This can be retrieved using -// [E2e.GetID]. -// - udID - The tracked UD object ID. This can be retrieved using -// [UserDiscovery.GetID]. -func NewChannelsManager(e2eID, udID int) (*ChannelsManager, error) { +// GenerateChannelIdentity creates and returns a marshal for a private +// channel Identity. The public component can be read as json via +// [GetPublicChannelIdentityFromPrivate] +func GenerateChannelIdentity(cmixID int) ([]byte, error) { // Get user from singleton - user, err := e2eTrackerSingleton.get(e2eID) + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + rng := user.api.GetRng().GetStream() + defer rng.Close() + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + return nil, err + } + return pi.Marshal(), nil +} + +// GetPublicChannelIdentity returns a json marshal of the public +// identity related to the given marshaled public identity +func GetPublicChannelIdentity(marshaledPublic []byte) ([]byte, error) { + i, err := cryptoChannel.UnmarshalIdentity(marshaledPublic) if err != nil { return nil, err } + return json.Marshal(&i) +} - udMan, err := udTrackerSingleton.get(udID) +// GetPublicChannelIdentityFromPrivate returns a json marshal of the public +// identity related to the given private identity +func GetPublicChannelIdentityFromPrivate(marshaledPrivate []byte) ([]byte, error) { + pi, err := cryptoChannel.UnmarshalPrivateIdentity(marshaledPrivate) if err != nil { return nil, err } + return json.Marshal(&pi.Identity) +} - nameService, err := udMan.api.StartChannelNameService() +// NewChannelsManagerGoEventModel creates a new ChannelsManager from a new +// identity. This is not compatible with GoMobile Bindings because it +// receives the go event model. This is for creating a manager for an identity +// for the first time. +// for generating a new one channel identity, please use +// GenerateChannelIdentity +// To reload this channel manager, use LoadChannelsManagerGoEventModel, passing +// in the storageTag retrieved by +// +// Parameters: +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - privateIdentity - a private identity generated by GenerateChannelIdentity +// - goEvent the event model which is not compatible with GoMobile bindings +func NewChannelsManagerGoEventModel(cmixID int, privateIdentity []byte, + goEvent channels.EventModel) (*ChannelsManager, error) { + pi, err := cryptoChannel.UnmarshalPrivateIdentity(privateIdentity) + if err != nil { + return nil, err + } + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) if err != nil { return nil, err } // Construct new channels manager - // TODO: Implement a bindings layer event model, pass that in as a parameter - // or the function and pass that into here. - m := channels.NewManager(user.api.GetStorage().GetKV(), user.api.GetCmix(), - user.api.GetRng(), nameService, nil) + m, err := channels.NewManager(pi, user.api.GetStorage().GetKV(), user.api.GetCmix(), + user.api.GetRng(), goEvent) + if err != nil { + return nil, err + } // Add channel to singleton and return return channelManagerTrackerSingleton.make(m), nil } -// NewChannelsManagerGoEventModel constructs a ChannelsManager. This is not +// LoadChannelsManagerGoEventModel loads an existing ChannelsManager. This is not // compatible with GoMobile Bindings because it receives the go event model. +// This is for creating a manager for an identity for the first time. +// The channel manager should have first been created with +// NewChannelsManagerGoEventModel and then the storage tag can be retrieved +// with ChannelsManager.GetStorageTag // // Parameters: -// - e2eID - The tracked e2e object ID. This can be retrieved using -// [E2e.GetID]. -// - udID - The tracked UD object ID. This can be retrieved using -// [UserDiscovery.GetID]. -func NewChannelsManagerGoEventModel(e2eID, udID int, +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - storageTag - retrieved with ChannelsManager.GetStorageTag +// - goEvent the event model which is not compatible with GoMobile bindings +func LoadChannelsManagerGoEventModel(cmixID int, storageTag string, goEvent channels.EventModel) (*ChannelsManager, error) { + // Get user from singleton - user, err := e2eTrackerSingleton.get(e2eID) + user, err := cmixTrackerSingleton.get(cmixID) if err != nil { return nil, err } - udMan, err := udTrackerSingleton.get(udID) + // Construct new channels manager + m, err := channels.LoadManager(storageTag, user.api.GetStorage().GetKV(), + user.api.GetCmix(), + user.api.GetRng(), goEvent) if err != nil { return nil, err } - nameService, err := udMan.api.StartChannelNameService() + // Add channel to singleton and return + return channelManagerTrackerSingleton.make(m), nil +} + +// NewChannelsManager creates a new ChannelsManager from a new +// identity. This is for creating a manager for an identity +// for the first time. +// for generating a new one channel identity, please use +// GenerateChannelIdentity +// To reload this channel manager, use LoadChannelsManagerGoEventModel, passing +// in the storageTag retrieved by +// +// Parameters: +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - privateIdentity - a private identity generated by GenerateChannelIdentity +// - event - the event model, compatible over the bindings +func NewChannelsManager(cmixID int, privateIdentity []byte, + event EventModel) (*ChannelsManager, error) { + pi, err := cryptoChannel.UnmarshalPrivateIdentity(privateIdentity) + if err != nil { + return nil, err + } + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) if err != nil { return nil, err } + // wrap the event model to make it compatible + goEvent := NewEventModel(event) + // Construct new channels manager - m := channels.NewManager(user.api.GetStorage().GetKV(), user.api.GetCmix(), - user.api.GetRng(), nameService, goEvent) + m, err := channels.NewManager(pi, user.api.GetStorage().GetKV(), user.api.GetCmix(), + user.api.GetRng(), goEvent) + if err != nil { + return nil, err + } // Add channel to singleton and return return channelManagerTrackerSingleton.make(m), nil } -// NewChannelsManagerGoEventModelDummyNameService constructs a -// ChannelsManager. This is not compatible with GoMobile Bindings because -// it receives the go event model. This uses the dummy name service -// and is for debugging only. +// LoadChannelsManager loads an existing ChannelsManager. +// This is for creating a manager for an identity for the first time. +// The channel manager should have first been created with +// NewChannelsManagerGoEventModel and then the storage tag can be retrieved +// with ChannelsManager.GetStorageTag +// // Parameters: // - cmixID - The tracked cmix object ID. This can be retrieved using // [Cmix.GetID]. -// - username - the username the user wants. -// - goEvent - the channels event model -func NewChannelsManagerGoEventModelDummyNameService(cmixID int, username string, - goEvent channels.EventModel) (*ChannelsManager, error) { +// - storageTag - retrieved with ChannelsManager.GetStorageTag +// - event - the event model, compatible over the bindings +func LoadChannelsManager(cmixID int, storageTag string, + event EventModel) (*ChannelsManager, error) { + // Get user from singleton user, err := cmixTrackerSingleton.get(cmixID) if err != nil { return nil, err } - rng := user.api.GetRng().GetStream() - defer rng.Close() + // wrap the event model to make it compatible + goEvent := NewEventModel(event) - nameService, err := channels.NewDummyNameService(username, rng) + // Construct new channels manager + m, err := channels.LoadManager(storageTag, user.api.GetStorage().GetKV(), + user.api.GetCmix(), + user.api.GetRng(), goEvent) if err != nil { return nil, err } - // Construct new channels manager - m := channels.NewManager(user.api.GetStorage().GetKV(), user.api.GetCmix(), - user.api.GetRng(), nameService, goEvent) // Add channel to singleton and return return channelManagerTrackerSingleton.make(m), nil @@ -626,6 +709,60 @@ func (cm *ChannelsManager) SendReaction(marshalledChanId []byte, return constructChannelSendReport(chanMsgId, rnd.ID, ephId) } +// GetIdentity returns the marshaled public identity that the channel is +// using +func (cm *ChannelsManager) GetIdentity() ([]byte, error) { + i := cm.api.GetIdentity() + return json.Marshal(&i) +} + +// GetStorageTag returns the storage tag needed to reload the manager +func (cm *ChannelsManager) GetStorageTag() string { + return cm.api.GetStorageTag() +} + +// SetNickname sets the nickname for a given channel +func (cm *ChannelsManager) SetNickname(newNick string, ch []byte) error { + chid, err := id.Unmarshal(ch) + if err != nil { + return err + } + return cm.api.SetNickname(newNick, chid) +} + +// DeleteNickname deletes the nickname for a given channel +func (cm *ChannelsManager) DeleteNickname(ch []byte) error { + chid, err := id.Unmarshal(ch) + if err != nil { + return err + } + return cm.api.DeleteNickname(chid) +} + +// GetNickname returns the nickname for a given channel. Returns an error if +// there is no nickname +func (cm *ChannelsManager) GetNickname(ch []byte) (string, error) { + chid, err := id.Unmarshal(ch) + if err != nil { + return "", err + } + nick, exists := cm.api.GetNickname(chid) + if !exists { + return "", errors.New("no nickname found for the given channel") + } + + return nick, nil +} + +// IsNicknameValid checks if a nickname is valid +// +// rules +// - a nickname must not be longer than 24 characters +// - a nickname must not be shorter than 1 character +func IsNicknameValid(nick string) error { + return channels.IsNicknameValid(nick) +} + // parseChannelsParameters is a helper function for the Send functions. It // parses the channel ID and the passed in parameters into their respective // objects. These objects are passed into the API via the internal send @@ -684,20 +821,23 @@ func constructChannelSendReport(channelMessageId cryptoChannel.MessageID, // "Rounds": [ 1, 4, 9], // } type ReceivedChannelMessageReport struct { - ChannelId []byte - MessageId []byte - MessageType int - SenderUsername string - Content []byte - Timestamp int64 - Lease int64 + ChannelId []byte + MessageId []byte + MessageType int + Nickname string + Identity []byte + Content []byte + Timestamp int64 + Lease int64 RoundsList } // ChannelMessageReceptionCallback is the callback that returns the context for // a channel message via the Callback. +// It must return a unique UUID for the message by which it can be referenced +// later type ChannelMessageReceptionCallback interface { - Callback(receivedChannelMessageReport []byte, err error) + Callback(receivedChannelMessageReport []byte, err error) int } // RegisterReceiveHandler is used to register handlers for non-default message @@ -719,21 +859,29 @@ func (cm *ChannelsManager) RegisterReceiveHandler(messageType int, cb := channels.MessageTypeReceiveMessage( func(channelID *id.ID, messageID cryptoChannel.MessageID, messageType channels.MessageType, - senderUsername string, content []byte, timestamp time.Time, - lease time.Duration, round rounds.Round, status channels.SentStatus) { + nickname string, content []byte, identity cryptoChannel.Identity, + timestamp time.Time, lease time.Duration, round rounds.Round, + status channels.SentStatus) uint64 { + + idBytes, err := json.Marshal(&identity) + if err != nil { + jww.WARN.Printf("failed to marshal identity object: %+v", err) + return 0 + } rcm := ReceivedChannelMessageReport{ - ChannelId: channelID.Marshal(), - MessageId: messageID.Bytes(), - MessageType: int(messageType), - SenderUsername: senderUsername, - Content: content, - Timestamp: timestamp.UnixNano(), - Lease: int64(lease), - RoundsList: makeRoundsList(round.ID), + ChannelId: channelID.Marshal(), + MessageId: messageID.Bytes(), + MessageType: int(messageType), + Nickname: nickname, + Identity: idBytes, + Content: content, + Timestamp: timestamp.UnixNano(), + Lease: int64(lease), + RoundsList: makeRoundsList(round.ID), } - listenerCb.Callback(json.Marshal(rcm)) + return uint64(listenerCb.Callback(json.Marshal(rcm))) }) // Register handler @@ -768,10 +916,11 @@ type EventModel interface { // - channelID - The marshalled channel [id.ID]. // - messageID - The bytes of the [channel.MessageID] of the received // message. - // - senderUsername - The username of the sender of the message. + // - nickname - The nickname of the sender of the message. // - text - The content of the message. // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch. + // - identity - the json of the identity of the sender // - lease - The number of nanoseconds that the message is valid for. // - roundId - The ID of the round that the message was received on. // - status - the [channels.SentStatus] of the message. @@ -780,8 +929,11 @@ type EventModel interface { // Sent = 0 // Delivered = 1 // Failed = 2 - ReceiveMessage(channelID, messageID []byte, senderUsername, text string, - timestamp, lease, roundId, status int64) + // + // Returns a non-negative unique uuid for the message by which it can be + // referenced later with UpdateSentStatus + ReceiveMessage(channelID, messageID []byte, nickname, text string, + identity []byte, timestamp, lease, roundId, status int64) int64 // ReceiveReply is called whenever a message is received that is a reply on // a given channel. It may be called multiple times on the same message. It @@ -796,8 +948,9 @@ type EventModel interface { // message. // - reactionTo - The [channel.MessageID] for the message that received a // reply. - // - senderUsername - The username of the sender of the message. + // - nickname - The nickname of the sender of the message. // - text - The content of the message. + // - identity - the json marshaled identity of the sender // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch. // - lease - The number of nanoseconds that the message is valid for. @@ -808,9 +961,12 @@ type EventModel interface { // Sent = 0 // Delivered = 1 // Failed = 2 + // + // Returns a non-negative unique uuid for the message by which it can be + // referenced later with UpdateSentStatus ReceiveReply(channelID, messageID, reactionTo []byte, - senderUsername, text string, - timestamp, lease, roundId, status int64) + nickname, text string, identity []byte, + timestamp, lease, roundId, status int64) int64 // ReceiveReaction is called whenever a reaction to a message is received // on a given channel. It may be called multiple times on the same reaction. @@ -827,8 +983,9 @@ type EventModel interface { // message. // - reactionTo - The [channel.MessageID] for the message that received a // reply. - // - senderUsername - The username of the sender of the message. + // - nickname - The nickname of the sender of the message. // - reaction - The contents of the reaction message. + // - identity - The json marshal of the identity of the sender // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch. // - lease - The number of nanoseconds that the message is valid for. @@ -839,9 +996,12 @@ type EventModel interface { // Sent = 0 // Delivered = 1 // Failed = 2 + // + // Returns a non-negative unique uuid for the message by which it can be + // referenced later with UpdateSentStatus ReceiveReaction(channelID, messageID, reactionTo []byte, - senderUsername, reaction string, - timestamp, lease, roundId, status int64) + nickname, reaction string, identity []byte, + timestamp, lease, roundId, status int64) int64 // UpdateSentStatus is called whenever the sent status of a message has // changed. @@ -855,7 +1015,8 @@ type EventModel interface { // Sent = 0 // Delivered = 1 // Failed = 2 - UpdateSentStatus(messageID []byte, status int64) + UpdateSentStatus(uuint int64, messageID []byte, timestamp, roundid, + status int64) // unimplemented // IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) @@ -871,7 +1032,7 @@ type toEventModel struct { // NewEventModel is a constructor for a toEventModel. This will take in an // EventModel and wraps it around the toEventModel. -func NewEventModel(em EventModel) *toEventModel { +func NewEventModel(em EventModel) channels.EventModel { return &toEventModel{em: em} } @@ -888,13 +1049,20 @@ func (tem *toEventModel) LeaveChannel(channelID *id.ID) { // ReceiveMessage is called whenever a message is received on a given channel. // It may be called multiple times on the same message. It is incumbent on the // user of the API to filter such called by message ID. -func (tem *toEventModel) ReceiveMessage(channelID *id.ID, - messageID cryptoChannel.MessageID, senderUsername string, text string, +func (tem *toEventModel) ReceiveMessage(channelID *id.ID, messageID cryptoChannel.MessageID, + nickname, text string, identity cryptoChannel.Identity, timestamp time.Time, lease time.Duration, round rounds.Round, - status channels.SentStatus) { + status channels.SentStatus) uint64 { - tem.em.ReceiveMessage(channelID[:], messageID[:], senderUsername, text, - timestamp.UnixNano(), int64(lease), int64(round.ID), int64(status)) + idBytes, err := json.Marshal(&identity) + if err != nil { + jww.WARN.Printf("failed to marshal identity object: %+v", err) + return 0 + } + + return uint64(tem.em.ReceiveMessage(channelID[:], messageID[:], nickname, + text, idBytes, timestamp.UnixNano(), int64(lease), int64(round.ID), + int64(status))) } // ReceiveReply is called whenever a message is received that is a reply on a @@ -903,15 +1071,20 @@ func (tem *toEventModel) ReceiveMessage(channelID *id.ID, // // Messages may arrive our of order, so a reply in theory can arrive before the // initial message. As a result, it may be important to buffer replies. -func (tem *toEventModel) ReceiveReply(channelID *id.ID, - messageID cryptoChannel.MessageID, - reactionTo cryptoChannel.MessageID, senderUsername string, - text string, timestamp time.Time, lease time.Duration, - round rounds.Round, status channels.SentStatus) { - - tem.em.ReceiveReply(channelID[:], messageID[:], reactionTo[:], - senderUsername, text, timestamp.UnixNano(), int64(lease), - int64(round.ID), int64(status)) +func (tem *toEventModel) ReceiveReply(channelID *id.ID, messageID cryptoChannel.MessageID, + reactionTo cryptoChannel.MessageID, nickname, text string, + identity cryptoChannel.Identity, timestamp time.Time, + lease time.Duration, round rounds.Round, status channels.SentStatus) uint64 { + + idBytes, err := json.Marshal(&identity) + if err != nil { + jww.WARN.Printf("failed to marshal identity object: %+v", err) + return 0 + } + + return uint64(tem.em.ReceiveReply(channelID[:], messageID[:], reactionTo[:], + nickname, text, idBytes, timestamp.UnixNano(), int64(lease), + int64(round.ID), int64(status))) } @@ -921,18 +1094,26 @@ func (tem *toEventModel) ReceiveReply(channelID *id.ID, // // Messages may arrive our of order, so a reply in theory can arrive before the // initial message. As a result, it may be important to buffer reactions. -func (tem *toEventModel) ReceiveReaction(channelID *id.ID, - messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, - senderUsername string, reaction string, timestamp time.Time, - lease time.Duration, round rounds.Round, status channels.SentStatus) { +func (tem *toEventModel) ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID, + reactionTo cryptoChannel.MessageID, nickname, reaction string, + identity cryptoChannel.Identity, timestamp time.Time, + lease time.Duration, round rounds.Round, status channels.SentStatus) uint64 { - tem.em.ReceiveReaction(channelID[:], messageID[:], reactionTo[:], - senderUsername, reaction, timestamp.UnixNano(), int64(lease), - int64(round.ID), int64(status)) + idBytes, err := json.Marshal(&identity) + if err != nil { + jww.WARN.Printf("failed to marshal identity object: %+v", err) + return 0 + } + + return uint64(tem.em.ReceiveReaction(channelID[:], messageID[:], + reactionTo[:], nickname, reaction, idBytes, timestamp.UnixNano(), + int64(lease), int64(round.ID), int64(status))) } // UpdateSentStatus is called whenever the sent status of a message has changed. -func (tem *toEventModel) UpdateSentStatus(messageID cryptoChannel.MessageID, +func (tem *toEventModel) UpdateSentStatus(uuid uint64, + messageID cryptoChannel.MessageID, timestamp time.Time, round rounds.Round, status channels.SentStatus) { - tem.em.UpdateSentStatus(messageID[:], int64(status)) + tem.em.UpdateSentStatus(int64(uuid), messageID[:], timestamp.UnixNano(), + int64(round.ID), int64(status)) } diff --git a/channels/adminListener.go b/channels/adminListener.go index 1c53be04bd0b33f0feb79675dcd3dbb62a32d272..0f5b6630e6c2bbb0c68a72b029c46882789a9d09 100644 --- a/channels/adminListener.go +++ b/channels/adminListener.go @@ -13,6 +13,7 @@ import ( "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" ) @@ -53,8 +54,15 @@ func (al *adminListener) Listen(payload []byte, return } + // Modify the timestamp to reduce the chance message order will be ambiguous + ts := mutateTimestamp(round.Timestamps[states.QUEUED], msgID) + // Submit the message to the event model for listening - al.trigger(al.chID, cm, msgID, receptionID, round, Delivered) + if uuid, err := al.trigger(al.chID, cm, ts, msgID, receptionID, + round, Delivered); err != nil { + jww.WARN.Printf("Error in passing off trigger for admin "+ + "message (UUID: %d): %+v", uuid, err) + } return } diff --git a/channels/eventModel.go b/channels/eventModel.go index a87016e1dc648e47d8c637e3b6194a8d38ec3fc7..9dbd9b4cfef3385814cb49289ddea16b032a0043 100644 --- a/channels/eventModel.go +++ b/channels/eventModel.go @@ -13,7 +13,6 @@ import ( "github.com/golang/protobuf/proto" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix/identity/receptionID" - "gitlab.com/elixxir/primitives/states" "sync" "time" @@ -128,6 +127,8 @@ type EventModel interface { // types. Default ones for Text, Reaction, and AdminText. // A unique uuid must be returned by which the message can be referenced later // via UpdateSentStatus +// It must return a unique UUID for the message by which it can be referenced +// later type MessageTypeReceiveMessage func(channelID *id.ID, messageID cryptoChannel.MessageID, messageType MessageType, nickname string, content []byte, identity cryptoChannel.Identity, @@ -221,18 +222,18 @@ func (e *events) triggerEvent(chID *id.ID, umi *userMessageInternal, ts time.Tim return uuid, nil } -type triggerAdminEventFunc func(chID *id.ID, cm *ChannelMessage, +type triggerAdminEventFunc func(chID *id.ID, cm *ChannelMessage, ts time.Time, messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, - round rounds.Round, status SentStatus) + round rounds.Round, status SentStatus) (uint64, error) // triggerAdminEvent is an internal function that is used to trigger message // reception on a message received from the admin (asymmetric encryption). // // It will call the appropriate MessageTypeHandler assuming one exists. func (e *events) triggerAdminEvent(chID *id.ID, cm *ChannelMessage, - messageID cryptoChannel.MessageID, + ts time.Time, messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, round rounds.Round, - status SentStatus) { + status SentStatus) (uint64, error) { messageType := MessageType(cm.PayloadType) // check if the type is already registered @@ -240,21 +241,19 @@ func (e *events) triggerAdminEvent(chID *id.ID, cm *ChannelMessage, listener, exists := e.registered[messageType] e.mux.RUnlock() if !exists { - jww.WARN.Printf("Received Admin message from %s on channel %s in "+ + errStr := fmt.Sprintf("Received Admin message from %s on channel %s in "+ "round %d which could not be handled due to unregistered message "+ "type %s; Contents: %v", AdminUsername, chID, round.ID, messageType, cm.Payload) - return + jww.WARN.Printf(errStr) + return 0, errors.New(errStr) } - // Modify the timestamp to reduce the chance message order will be ambiguous - ts := mutateTimestamp(round.Timestamps[states.QUEUED], messageID) - // Call the listener. This is already in an instanced event, no new thread needed. - listener(chID, messageID, messageType, AdminUsername, cm.Payload, + uuid := listener(chID, messageID, messageType, AdminUsername, cm.Payload, cryptoChannel.Identity{Codename: AdminUsername}, ts, time.Duration(cm.Lease), round, status) - return + return uuid, nil } // receiveTextMessage is the internal function that handles the reception of diff --git a/channels/interface.go b/channels/interface.go index fd029b16ec6404f93c3b45df65a29d0ea522fc46..fd22d6426a4bd77d26e22dc51042cfba070ecbf5 100644 --- a/channels/interface.go +++ b/channels/interface.go @@ -9,7 +9,6 @@ package channels import ( "gitlab.com/elixxir/client/cmix" - "gitlab.com/elixxir/client/cmix/pickup/store" "gitlab.com/elixxir/client/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" cryptoChannel "gitlab.com/elixxir/crypto/channel" @@ -29,7 +28,7 @@ var ValidForever = time.Duration(math.MaxInt64) type Manager interface { // GetIdentity returns the public identity associated with this channel manager - GetIdentity() store.Identity + GetIdentity() cryptoChannel.Identity // GetStorageTag returns the tag at which this manager is store for loading // it is derived from the public key @@ -121,7 +120,7 @@ type Manager interface { // DeleteNickname removes the nickname for a given channel, using the codename // for that channel instead - DeleteNickname(ch *id.ID) + DeleteNickname(ch *id.ID) error // GetNickname returns the nickname for the given channel if it exists GetNickname(ch *id.ID) (nickname string, exists bool) diff --git a/channels/joinedChannel.go b/channels/joinedChannel.go index f3ff927bd0a9249df2ffd5fec246b00d63aef3aa..1b6aa53e7f9b2539b983c418eab52368d7c129c0 100644 --- a/channels/joinedChannel.go +++ b/channels/joinedChannel.go @@ -72,7 +72,7 @@ func (m *manager) loadChannels() { for i := range chList { jc, err := loadJoinedChannel( - chList[i], m.kv, m.net, m.rng, m.name, m.events, m.broadcastMaker, + chList[i], m.kv, m.net, m.rng, m.events, m.broadcastMaker, m.st.MessageReceive) if err != nil { jww.FATAL.Panicf("Failed to load channel %s: %+v", chList[i], err) @@ -111,7 +111,6 @@ func (m *manager) addChannel(channel *cryptoBroadcast.Channel) error { // Connect to listeners err = b.RegisterListener((&userListener{ - name: m.name, chID: channel.ReceptionID, trigger: m.events.triggerEvent, checkSent: m.st.MessageReceive, @@ -211,7 +210,7 @@ func (jc *joinedChannel) Store(kv *versioned.KV) error { // loadJoinedChannel loads a given channel from ekv storage. func loadJoinedChannel(chId *id.ID, kv *versioned.KV, net broadcast.Client, - rngGen *fastRNG.StreamGenerator, name NameService, e *events, + rngGen *fastRNG.StreamGenerator, e *events, broadcastMaker broadcast.NewBroadcastChannelFunc, mr messageReceiveFunc) (*joinedChannel, error) { obj, err := kv.Get(makeJoinedChannelKey(chId), joinedChannelVersion) if err != nil { @@ -225,7 +224,7 @@ func loadJoinedChannel(chId *id.ID, kv *versioned.KV, net broadcast.Client, return nil, err } - b, err := initBroadcast(jcd.Broadcast, name, e, net, broadcastMaker, rngGen, mr) + b, err := initBroadcast(jcd.Broadcast, e, net, broadcastMaker, rngGen, mr) jc := &joinedChannel{broadcast: b} return jc, nil @@ -242,7 +241,7 @@ func makeJoinedChannelKey(chId *id.ID) string { } func initBroadcast(c *cryptoBroadcast.Channel, - name NameService, e *events, net broadcast.Client, + e *events, net broadcast.Client, broadcastMaker broadcast.NewBroadcastChannelFunc, rngGen *fastRNG.StreamGenerator, mr messageReceiveFunc) (broadcast.Channel, error) { b, err := broadcastMaker(c, net, rngGen) @@ -251,7 +250,6 @@ func initBroadcast(c *cryptoBroadcast.Channel, } err = b.RegisterListener((&userListener{ - name: name, chID: c.ReceptionID, trigger: e.triggerEvent, checkSent: mr, diff --git a/channels/manager.go b/channels/manager.go index 71181eec14d26208b41c5f65a5e13fa89ec6ff91..ddbdb04f377457d0b10670ab1754dc2083a8a692 100644 --- a/channels/manager.go +++ b/channels/manager.go @@ -90,14 +90,13 @@ func NewManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV, m := setupManager(identity, kv, net, rng, model) - return &m, nil + return m, nil } // LoadManager restores a channel.Manager from disk stored at the given //storage tag. func LoadManager(storageTag string, kv *versioned.KV, net Client, - rng *fastRNG.StreamGenerator, name NameService, model EventModel) (Manager, - error) { + rng *fastRNG.StreamGenerator, model EventModel) (Manager, error) { // Prefix the kv with the username so multiple can be run kv = kv.Prefix(storageTag) @@ -110,7 +109,7 @@ func LoadManager(storageTag string, kv *versioned.KV, net Client, m := setupManager(identity, kv, net, rng, model) - return &m + return m, nil } func setupManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV, @@ -199,7 +198,7 @@ func (m *manager) ReplayChannel(chID *id.ID) error { jc.broadcast.Stop() //re-instantiate the broadcast, re-registering it from scratch - b, err := initBroadcast(c, m.name, m.events, m.net, m.broadcastMaker, m.rng, + b, err := initBroadcast(c, m.events, m.net, m.broadcastMaker, m.rng, m.st.MessageReceive) if err != nil { return err diff --git a/channels/nickname.go b/channels/nickname.go index 71523d87e0df4933c41289ab3d306ed109fb8725..13dc43d074263018f12fbf6c2d2aab6dd62ab53d 100644 --- a/channels/nickname.go +++ b/channels/nickname.go @@ -129,12 +129,18 @@ func (nm *nicknameManager) load() error { // IsNicknameValid checks if a nickname is valid // // rules -// - a Nickname must not be longer than 24 characters +// - a nickname must not be longer than 24 characters +// - a nickname must not be shorter than 1 character // todo: add character filtering -func IsNicknameValid(nm string) error { - if len([]rune(nm)) > 24 { +func IsNicknameValid(nick string) error { + runeNick := []rune(nick) + if len(runeNick) > 24 { return errors.New("nicknames must be 24 characters in length or less") } + if len(runeNick) < 1 { + return errors.New("nicknames must be at least 1 character in length") + } + return nil } diff --git a/channels/send.go b/channels/send.go index 978c66e925d79691c9a6986c57b084f1ac99f70e..6d17fdab0b4e5634c2f97159e5235dee6a2bc52c 100644 --- a/channels/send.go +++ b/channels/send.go @@ -8,6 +8,7 @@ package channels import ( + "crypto/ed25519" "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/rounds" cryptoChannel "gitlab.com/elixxir/crypto/channel" @@ -39,9 +40,21 @@ func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err } + nickname, _ := m.nicknameManager.GetNickname(channelID) + var msgId cryptoChannel.MessageID - var usrMsg *UserMessage - var chMsg *ChannelMessage + + chMsg := &ChannelMessage{ + Lease: validUntil.Nanoseconds(), + PayloadType: uint32(messageType), + Payload: msg, + Nickname: nickname, + } + + usrMsg := &UserMessage{ + ECCPublicKey: m.me.PubKey, + } + //Note: we are not checking check if message is too long before trying to //find a round @@ -49,12 +62,7 @@ func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, assemble := func(rid id.Round) ([]byte, error) { //Build the message - chMsg = &ChannelMessage{ - Lease: validUntil.Nanoseconds(), - RoundID: uint64(rid), - PayloadType: uint32(messageType), - Payload: msg, - } + chMsg.RoundID = uint64(rid) //Serialize the message chMsgSerial, err := proto.Marshal(chMsg) @@ -66,22 +74,10 @@ func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, msgId = cryptoChannel.MakeMessageID(chMsgSerial) //Sign the message - messageSig, err := m.name.SignChannelMessage(chMsgSerial) - if err != nil { - return nil, err - } + messageSig := ed25519.Sign(*m.me.Privkey, chMsgSerial) - //Build the user message - validationSig, unameLease := m.name.GetChannelValidationSignature() - - usrMsg = &UserMessage{ - Message: chMsgSerial, - ValidationSignature: validationSig, - Signature: messageSig, - Username: m.name.GetUsername(), - ECCPublicKey: m.name.GetChannelPubkey(), - UsernameLease: unameLease.UnixNano(), - } + usrMsg.Message = chMsgSerial + usrMsg.Signature = messageSig //Serialize the user message usrMsgSerial, err := proto.Marshal(usrMsg) @@ -92,15 +88,17 @@ func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, return usrMsgSerial, nil } + uuid, err := m.st.denotePendingSend(channelID, &userMessageInternal{ + userMessage: usrMsg, + channelMessage: chMsg, + messageID: msgId, + }) + r, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params) if err != nil { return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err } - m.st.send(channelID, &userMessageInternal{ - userMessage: usrMsg, - channelMessage: chMsg, - messageID: msgId, - }, r) + err = m.st.send(uuid, msgId, r) return msgId, r, ephid, err } @@ -121,7 +119,12 @@ func (m *manager) SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID, } var msgId cryptoChannel.MessageID - var chMsg *ChannelMessage + chMsg := &ChannelMessage{ + Lease: validUntil.Nanoseconds(), + PayloadType: uint32(messageType), + Payload: msg, + Nickname: AdminUsername, + } //Note: we are not checking check if message is too long before trying to //find a round @@ -129,12 +132,7 @@ func (m *manager) SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID, assemble := func(rid id.Round) ([]byte, error) { //Build the message - chMsg = &ChannelMessage{ - Lease: validUntil.Nanoseconds(), - RoundID: uint64(rid), - PayloadType: uint32(messageType), - Payload: msg, - } + chMsg.RoundID = uint64(rid) //Serialize the message chMsgSerial, err := proto.Marshal(chMsg) @@ -152,10 +150,18 @@ func (m *manager) SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID, return chMsgSerial, nil } + uuid, err := m.st.denotePendingAdminSend(channelID, chMsg) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + r, ephid, err := ch.broadcast.BroadcastRSAToPublicWithAssembler(privKey, assemble, params) - m.st.sendAdmin(channelID, chMsg, msgId, r) + err = m.st.sendAdmin(uuid, msgId, r) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } return msgId, r, ephid, err } diff --git a/channels/sendTracker.go b/channels/sendTracker.go index bc2205916823aa04ad70ef27642da18971eedee1..69fe04a1968d3c9157e47df2a257fe4e02f9dfb7 100644 --- a/channels/sendTracker.go +++ b/channels/sendTracker.go @@ -9,12 +9,14 @@ package channels import ( "encoding/json" + "errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/storage/versioned" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" "sync" "time" @@ -33,7 +35,7 @@ const ( // runs maxChecks = 3 - onePointFiveSeconds = 15000 * time.Millisecond + oneSecond = 1000 * time.Millisecond ) type tracked struct { @@ -190,11 +192,47 @@ func (st *sendTracker) load() error { return nil } +// denotePendingSend is called before the pending send. It tracks the send +// internally and notifies the UI of the send func (st *sendTracker) denotePendingSend(channelID *id.ID, - umi *userMessageInternal, nickname string) { - ts := time.Now().Add(onePointFiveSeconds) - uuid := st.trigger(channelID, umi, ts, receptionID.EphemeralIdentity{}, + umi *userMessageInternal) (uint64, error) { + // for a timestamp for the message, use 1 second from now to + // approximate the lag due to round submission + ts := time.Now().Add(oneSecond) + + // submit the message to the UI + uuid, err := st.trigger(channelID, umi, ts, receptionID.EphemeralIdentity{}, + rounds.Round{}, Unsent) + if err != nil { + return 0, err + } + + // track the message on disk + st.handleDenoteSend(uuid, channelID, cryptoChannel.MessageID{}, + rounds.Round{}) + return uuid, nil +} + +// denotePendingAdminSend is called before the pending admin send. It tracks the +// send internally and notifies the UI of the send +func (st *sendTracker) denotePendingAdminSend(channelID *id.ID, + cm *ChannelMessage) (uint64, error) { + // for a timestamp for the message, use 1 second from now to + // approximate the lag due to round submission + ts := time.Now().Add(oneSecond) + + // submit the message to the UI + uuid, err := st.adminTrigger(channelID, cm, ts, cryptoChannel.MessageID{}, + receptionID.EphemeralIdentity{}, rounds.Round{}, Unsent) + + // track the message on disk + if err != nil { + return 0, err + } + st.handleDenoteSend(uuid, channelID, cryptoChannel.MessageID{}, + rounds.Round{}) + return uuid, nil } // handleDenoteSend does the nity gritty of editing internal structures @@ -218,34 +256,62 @@ func (st *sendTracker) handleDenoteSend(uuid uint64, channelID *id.ID, } // send tracks a generic send message -func (st *sendTracker) send(channelID *id.ID, - umi *userMessageInternal, round rounds.Round) { - st.handleSend(channelID, umi.GetMessageID(), round) - go st.trigger(channelID, umi, - receptionID.EphemeralIdentity{}, round, Sent) +func (st *sendTracker) send(uuid uint64, msgID cryptoChannel.MessageID, + round rounds.Round) error { + + // update the on disk message status + t, err := st.handleSend(uuid, msgID, round) + if err != nil { + return err + } + + // Modify the timestamp to reduce the chance message order will be ambiguous + ts := mutateTimestamp(round.Timestamps[states.QUEUED], msgID) + + //update the message on the UI + go st.updateStatus(t.UUID, msgID, ts, round, Sent) + return nil } // sendAdmin tracks a generic sendAdmin message -func (st *sendTracker) sendAdmin(channelID *id.ID, - cm *ChannelMessage, msgID cryptoChannel.MessageID, round rounds.Round) { - st.handleSend(channelID, msgID, round) - go st.adminTrigger(channelID, cm, msgID, - receptionID.EphemeralIdentity{}, round, Sent) +func (st *sendTracker) sendAdmin(uuid uint64, msgID cryptoChannel.MessageID, + round rounds.Round) error { + + // update the on disk message status + t, err := st.handleSend(uuid, msgID, round) + if err != nil { + return err + } + + // Modify the timestamp to reduce the chance message order will be ambiguous + ts := mutateTimestamp(round.Timestamps[states.QUEUED], msgID) + + //update the message on the UI + go st.updateStatus(t.UUID, msgID, ts, round, Sent) + + return nil } // handleSend does the nity gritty of editing internal structures -func (st *sendTracker) handleSend(channelID *id.ID, - messageID cryptoChannel.MessageID, round rounds.Round) { +func (st *sendTracker) handleSend(uuid uint64, + messageID cryptoChannel.MessageID, round rounds.Round) (*tracked, error) { st.mux.Lock() defer st.mux.Unlock() - //skip if already added + //check if in unsent + t, exists := st.unsent[uuid] + if !exists { + return nil, errors.New("cannot handle send on an unprepared message") + } + _, existsMessage := st.byMessageID[messageID] if existsMessage { - return + return nil, errors.New("cannot handle send on a message which was " + + "already sent") } - t := &tracked{messageID, channelID, round.ID} + t.MsgID = messageID + t.RoundID = round.ID //add the roundID roundsList, existsRound := st.byRound[round.ID] @@ -263,10 +329,12 @@ func (st *sendTracker) handleSend(channelID *id.ID, } //store the changed list to disk - err := st.store() + err := st.storeSent() if err != nil { jww.FATAL.Panicf(err.Error()) } + + return t, nil } // MessageReceive is used when a message is received to check if the message @@ -317,7 +385,7 @@ type roundResults struct { // callback is called when results are known about a round. it will re-trigger // the wait if it fails up to 'maxChecks' times. -func (rr *roundResults) callback(allRoundsSucceeded, timedOut bool, rounds map[id.Round]cmix.RoundResult) { +func (rr *roundResults) callback(allRoundsSucceeded, timedOut bool, _ map[id.Round]cmix.RoundResult) { rr.st.mux.Lock() @@ -360,6 +428,7 @@ func (rr *roundResults) callback(allRoundsSucceeded, timedOut bool, rounds map[i rr.st.mux.Unlock() for i := range registered { - go rr.st.updateStatus(registered[i].MsgID, status) + go rr.st.updateStatus(registered[i].UUID, registered[i].MsgID, time.Time{}, + rounds.Round{}, status) } } diff --git a/channels/userListener.go b/channels/userListener.go index 575c05e54d5810a717610fff64b7b81ad754bc99..5e56121f9bb6c07007a61e4535e4ddc1c0bde84f 100644 --- a/channels/userListener.go +++ b/channels/userListener.go @@ -14,7 +14,6 @@ import ( "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" - "time" ) // the userListener adheres to the [broadcast.ListenerFunc] interface and is @@ -57,37 +56,25 @@ func (ul *userListener) Listen(payload []byte, return } - // check that the username lease is valid - usernameLeaseEnd := time.Unix(0, um.UsernameLease) - if !usernameLeaseEnd.After(round.Timestamps[states.QUEUED]) { - jww.WARN.Printf("Message %s on channel %s purportedly from %s "+ - "has an expired lease, ended %s, round %d was sent at %s", msgID, - ul.chID, um.Username, usernameLeaseEnd, round.ID, - round.Timestamps[states.QUEUED]) - return - } - - // check that the signature from the nameserver is valid - if !ul.name.ValidateChannelMessage(um.Username, - time.Unix(0, um.UsernameLease), um.ECCPublicKey, um.ValidationSignature) { - jww.WARN.Printf("Message %s on channel %s purportedly from %s "+ - "failed the check of its Name Server with signature %v", msgID, - ul.chID, um.Username, um.ValidationSignature) - return - } - // check that the user properly signed the message if !ed25519.Verify(um.ECCPublicKey, um.Message, um.Signature) { jww.WARN.Printf("Message %s on channel %s purportedly from %s "+ "failed its user signature with signature %v", msgID, - ul.chID, um.Username, um.Signature) + ul.chID, cm.Nickname, um.Signature) return } + // Modify the timestamp to reduce the chance message order will be ambiguous + ts := mutateTimestamp(round.Timestamps[states.QUEUED], msgID) + //TODO: Processing of the message relative to admin commands will be here //Submit the message to the event model for listening - ul.trigger(ul.chID, umi, receptionID, round, Delivered) + if uuid, err := ul.trigger(ul.chID, umi, ts, receptionID, round, + Delivered); err != nil { + jww.WARN.Printf("Error in passing off trigger for "+ + "message (UUID: %d): %+v", uuid, err) + } return } diff --git a/cmix/identity/receptionID/store.go b/cmix/identity/receptionID/store.go index 78400d30f9d1bd15e1ef03c22a01f096c091abbe..967c22e034270d726b2c4e216d460dc767e0dbd2 100644 --- a/cmix/identity/receptionID/store.go +++ b/cmix/identity/receptionID/store.go @@ -29,7 +29,8 @@ const ( receptionStoreStorageVersion = 0 ) -var InvalidRequestedNumIdentities = errors.New("cannot get less than one identity(s)")code +var InvalidRequestedNumIdentities = errors.New("cannot get less than one identity(s)") + type Store struct { // Identities which are being actively checked active []*registration