More comments + UpdateMessageAttribute public API
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details

This commit is contained in:
Sarah Jamie Lewis 2021-11-19 12:27:52 -08:00
parent f1caca3adf
commit 847b04e4fc
4 changed files with 62 additions and 116 deletions

View File

@ -11,12 +11,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
"io" "io"
"strings" "strings"
"time"
) )
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date // CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
@ -229,3 +231,45 @@ func (g *Group) VerifyGroupMessage(onion string, groupID string, message string,
} }
return false return false
} }
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
// profile
func EncryptMessageToGroup(message string, author primitives.Identity, group *Group) ([]byte, []byte, *groups.DecryptedGroupMessage, error) {
if len(message) > MaxGroupMessageLength {
return nil, nil, nil, errors.New("group message is too long")
}
timestamp := time.Now().Unix()
// Select the latest message from the timeline as a reference point.
var prevSig []byte
if len(group.Timeline.Messages) > 0 {
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
} else {
prevSig = []byte(group.GroupID)
}
lenPadding := MaxGroupMessageLength - len(message)
padding := make([]byte, lenPadding)
getRandomness(&padding)
hexGroupID, err := hex.DecodeString(group.GroupID)
if err != nil {
return nil, nil, nil, err
}
dm := &groups.DecryptedGroupMessage{
Onion: author.Hostname(),
Text: message,
SignedGroupID: hexGroupID,
Timestamp: uint64(timestamp),
PreviousMessageSig: prevSig,
Padding: padding[:],
}
ciphertext, err := group.EncryptMessage(dm)
if err != nil {
return nil, nil, nil, err
}
serialized, _ := json.Marshal(dm)
signature := author.Sign([]byte(group.GroupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized)))
return ciphertext, signature, dm, nil
}

View File

@ -2,20 +2,17 @@ package model
import ( import (
"crypto/rand" "crypto/rand"
"cwtch.im/cwtch/protocol/groups"
"encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"io" "io"
"path/filepath" "path/filepath"
"sync" "sync"
"time"
) )
// Authorization is a type determining client assigned authorization to a peer // Authorization is a type determining client assigned authorization to a peer
// Deprecated - Only used for Importing legacy profile formats
// Still used in some APIs in UI but will be replaced prior to full deprecation
type Authorization string type Authorization string
const ( const (
@ -28,6 +25,7 @@ const (
) )
// PublicProfile is a local copy of a CwtchIdentity // PublicProfile is a local copy of a CwtchIdentity
// Deprecated - Only used for Importing legacy profile formats
type PublicProfile struct { type PublicProfile struct {
Name string Name string
Ed25519PublicKey ed25519.PublicKey Ed25519PublicKey ed25519.PublicKey
@ -43,6 +41,7 @@ type PublicProfile struct {
} }
// Profile encapsulates all the attributes necessary to be a Cwtch Peer. // Profile encapsulates all the attributes necessary to be a Cwtch Peer.
// Deprecated - Only used for Importing legacy profile formats
type Profile struct { type Profile struct {
PublicProfile PublicProfile
Contacts map[string]*PublicProfile Contacts map[string]*PublicProfile
@ -71,48 +70,6 @@ func GenerateRandomID() string {
return filepath.Join(hex.EncodeToString(randBytes)) return filepath.Join(hex.EncodeToString(randBytes))
} }
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
// profile
func EncryptMessageToGroup(message string, author primitives.Identity, group *Group) ([]byte, []byte, *groups.DecryptedGroupMessage, error) {
if len(message) > MaxGroupMessageLength {
return nil, nil, nil, errors.New("group message is too long")
}
timestamp := time.Now().Unix()
// Select the latest message from the timeline as a reference point.
var prevSig []byte
if len(group.Timeline.Messages) > 0 {
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
} else {
prevSig = []byte(group.GroupID)
}
lenPadding := MaxGroupMessageLength - len(message)
padding := make([]byte, lenPadding)
getRandomness(&padding)
hexGroupID, err := hex.DecodeString(group.GroupID)
if err != nil {
return nil, nil, nil, err
}
dm := &groups.DecryptedGroupMessage{
Onion: author.Hostname(),
Text: message,
SignedGroupID: hexGroupID,
Timestamp: uint64(timestamp),
PreviousMessageSig: prevSig,
Padding: padding[:],
}
ciphertext, err := group.EncryptMessage(dm)
if err != nil {
return nil, nil, nil, err
}
serialized, _ := json.Marshal(dm)
signature := author.Sign([]byte(group.GroupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized)))
return ciphertext, signature, dm, nil
}
// GetCopy returns a full deep copy of the Profile struct and its members (timeline inclusion control by arg) // GetCopy returns a full deep copy of the Profile struct and its members (timeline inclusion control by arg)
func (p *Profile) GetCopy(timeline bool) *Profile { func (p *Profile) GetCopy(timeline bool) *Profile {
p.lock.Lock() p.lock.Lock()

View File

@ -223,16 +223,6 @@ func (cp *cwtchPeer) SendMessage(conversation int, message string) error {
return fmt.Errorf("error sending message to conversation %v", err) return fmt.Errorf("error sending message to conversation %v", err)
} }
// UpdateMessageFlags
// Status: TODO
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
cp.mutex.Lock()
defer cp.mutex.Unlock()
log.Debugf("Updating Flags for %v %v %v", handle, mIdx, flags)
//cp.Profile.UpdateMessageFlags(handle, mIdx, flags)
cp.eventBus.Publish(event.NewEvent(event.UpdateMessageFlags, map[event.Field]string{event.Handle: handle, event.Index: strconv.Itoa(mIdx), event.Flags: strconv.FormatUint(flags, 2)}))
}
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname // BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
// known to peer. // known to peer.
// Status: Ready for 1.5 // Status: Ready for 1.5
@ -385,36 +375,6 @@ func ImportLegacyProfile(profile *model.Profile, cps *CwtchProfileStorage) Cwtch
func (cp *cwtchPeer) Init(eventBus event.Manager) { func (cp *cwtchPeer) Init(eventBus event.Manager) {
cp.InitForEvents(eventBus, DefaultEventsToHandle) cp.InitForEvents(eventBus, DefaultEventsToHandle)
// Upgrade the Cwtch Peer if necessary
// It would be nice to do these checks in the storage engine itself, but it is easier to do them here
// rather than duplicating the logic to construct/reconstruct attributes in storage engine...
// TODO: Remove these checks after Cwtch ~1.5 storage engine is implemented
// TODO: Move this to import script
//if _, exists := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name); !exists {
// // If public.profile.name does not exist, and we have an existing public.name then:
// // set public.profile.name from public.name
// // set local.profile.name from public.name
// if name, exists := cp.GetAttribute(attr.GetPublicScope(constants.Name)); exists {
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name)
// } else {
// // Otherwise check if local.name exists and set it from that
// // If not, then check the very old unzoned, unscoped name.
// // If not, then set directly from Profile.Name...
// if name, exists := cp.Profile.GetAttribute(attr.GetLocalScope(constants.Name)); exists {
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name)
// } else if name, exists := cp.Profile.GetAttribute(constants.Name); exists {
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name)
// } else {
// // Profile.Name is very deprecated at this point...
// cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, cp.Profile.Name)
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, cp.Profile.Name)
// }
// }
//}
// At this point we can safely assume that public.profile.name exists // At this point we can safely assume that public.profile.name exists
localName, _ := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name) localName, _ := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
publicName, _ := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) publicName, _ := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
@ -422,21 +382,6 @@ func (cp *cwtchPeer) Init(eventBus event.Manager) {
if localName != publicName { if localName != publicName {
cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, publicName) cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, publicName)
} }
// At this point we can safely assume that public.profile.name exists AND is consistent with
// local.profile.name - regardless of whatever Cwtch version we have upgraded from. This will
// be important after Cwtch 1.5 when we purge all previous references to local.profile.name and
// profile-> name - and remove all name processing code from libcwtch-go.
// If local.profile.tag does not exist then set it from deprecated GetAttribute
//if _, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag); !exists {
// if tag, exists := cp.Profile.GetAttribute(constants.Tag); exists {
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
// } else {
// // Assume a default password, which will allow the older profile to have it's password reset by the UI
// cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, constants.ProfileTypeV1DefaultPassword)
// }
//}
} }
// InitForEvents // InitForEvents
@ -584,6 +529,17 @@ func (cp *cwtchPeer) GetMostRecentMessages(conversation int, channel int, offset
return cp.storage.GetMostRecentMessages(conversation, channel, offset, limit) return cp.storage.GetMostRecentMessages(conversation, channel, offset, limit)
} }
func (cp *cwtchPeer) UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error {
_, attr, err := cp.GetChannelMessage(conversation, channel, id)
if err == nil {
cp.mutex.Lock()
defer cp.mutex.Unlock()
attr[key] = value
return cp.storage.UpdateMessageAttributes(conversation, channel, id, attr)
}
return err
}
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error. // StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
// Status: TODO change server handle to conversation id...? // Status: TODO change server handle to conversation id...?
func (cp *cwtchPeer) StartGroup(name string, server string) (int, error) { func (cp *cwtchPeer) StartGroup(name string, server string) (int, error) {
@ -714,14 +670,8 @@ func (cp *cwtchPeer) SendInviteToConversation(conversationID int, inviteConversa
return err return err
} }
// groupServer, isGroup := conversationInfo.Attributes[event.GroupServer]; isGroup {
//bundle, _ := cp.Get(profile.GetGroup(target).GroupServer).GetAttribute(string(model.BundleType))
//inviteStr, err := profile.GetGroup(target).Invite()
//if err == nil {
// invite = model.MessageWrapper{Overlay: 101, Data: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), inviteStr)}
//}
if tor.IsValidHostname(inviteConversationInfo.Handle) { if tor.IsValidHostname(inviteConversationInfo.Handle) {
invite = model.MessageWrapper{Overlay: 100, Data: inviteConversationInfo.Handle} invite = model.MessageWrapper{Overlay: model.OverlayInviteContact, Data: inviteConversationInfo.Handle}
} else { } else {
// Reconstruct Group // Reconstruct Group
groupID, ok := inviteConversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()] groupID, ok := inviteConversationInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]
@ -771,7 +721,7 @@ func (cp *cwtchPeer) SendInviteToConversation(conversationID int, inviteConversa
return errors.New("server bundle not found") return errors.New("server bundle not found")
} }
invite = model.MessageWrapper{Overlay: 101, Data: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), groupInvite)} invite = model.MessageWrapper{Overlay: model.OverlayInviteContact, Data: fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString([]byte(bundle)), groupInvite)}
} }
inviteBytes, err := json.Marshal(invite) inviteBytes, err := json.Marshal(invite)

View File

@ -51,11 +51,6 @@ type SendMessages interface {
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string) SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
} }
// ModifyMessages enables a caller to modify the messages in a timeline
type ModifyMessages interface {
UpdateMessageFlags(string, int, uint64)
}
// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to // CwtchPeer provides us with a way of testing systems built on top of cwtch without having to
// directly implement a cwtchPeer. // directly implement a cwtchPeer.
type CwtchPeer interface { type CwtchPeer interface {
@ -95,7 +90,6 @@ type CwtchPeer interface {
ModifyServers ModifyServers
SendMessages SendMessages
ModifyMessages
// Import Bundle // Import Bundle
ImportBundle(string) error ImportBundle(string) error
@ -116,6 +110,7 @@ type CwtchPeer interface {
GetChannelMessageCount(conversation int, channel int) (int, error) GetChannelMessageCount(conversation int, channel int) (int, error)
GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error) GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error)
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error) GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
ShareFile(fileKey string, serializedManifest string) ShareFile(fileKey string, serializedManifest string)
CheckPassword(password string) bool CheckPassword(password string) bool