package model import ( "crypto/rand" "cwtch.im/cwtch/protocol" "encoding/base32" "encoding/hex" "encoding/json" "errors" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "github.com/golang/protobuf/proto" "golang.org/x/crypto/ed25519" "io" "path/filepath" "strings" "sync" "time" ) // PublicProfile is a local copy of a CwtchIdentity type PublicProfile struct { Name string Ed25519PublicKey ed25519.PublicKey Trusted bool Blocked bool Onion string Attributes map[string]string Timeline Timeline `json:"-"` LocalID string // used by storage engine State string `json:"-"` lock sync.Mutex unacknowledgedMessages map[string]Message } // Profile encapsulates all the attributes necessary to be a Cwtch Peer. type Profile struct { PublicProfile Contacts map[string]*PublicProfile Ed25519PrivateKey ed25519.PrivateKey Groups map[string]*Group } // MaxGroupMessageLength is the maximum length of a message posted to a server group. // TODO: Should this be per server? const MaxGroupMessageLength = 1800 // GenerateRandomID generates a random 16 byte hex id code func GenerateRandomID() string { randBytes := make([]byte, 16) rand.Read(randBytes) return filepath.Join(hex.EncodeToString(randBytes)) } func (p *PublicProfile) init() { if p.Attributes == nil { p.Attributes = make(map[string]string) } p.unacknowledgedMessages = make(map[string]Message) p.LocalID = GenerateRandomID() } // SetAttribute allows applications to store arbitrary configuration info at the profile level. func (p *PublicProfile) SetAttribute(name string, value string) { p.lock.Lock() defer p.lock.Unlock() p.Attributes[name] = value } // GetAttribute returns the value of a value set with SetCustomAttribute. If no such value has been set exists is set to false. func (p *PublicProfile) GetAttribute(name string) (value string, exists bool) { p.lock.Lock() defer p.lock.Unlock() value, exists = p.Attributes[name] return } // GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name. func GenerateNewProfile(name string) *Profile { p := new(Profile) p.init() p.Name = name pub, priv, _ := ed25519.GenerateKey(rand.Reader) p.Ed25519PublicKey = pub p.Ed25519PrivateKey = priv p.Onion = tor.GetTorV3Hostname(pub) p.Contacts = make(map[string]*PublicProfile) p.Contacts[p.Onion] = &p.PublicProfile p.Groups = make(map[string]*Group) return p } // AddContact allows direct manipulation of cwtch contacts func (p *Profile) AddContact(onion string, profile *PublicProfile) { p.lock.Lock() profile.init() // TODO: More Robust V3 Onion Handling decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56])) profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32]) p.Contacts[onion] = profile p.lock.Unlock() } // DeleteContact deletes a peer contact func (p *Profile) DeleteContact(onion string) { p.lock.Lock() defer p.lock.Unlock() delete(p.Contacts, onion) } // DeleteGroup deletes a group func (p *Profile) DeleteGroup(groupID string) { p.lock.Lock() defer p.lock.Unlock() delete(p.Groups, groupID) } // RejectInvite rejects and removes a group invite func (p *Profile) RejectInvite(groupID string) { p.lock.Lock() delete(p.Groups, groupID) p.lock.Unlock() } // AddSentMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile. func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt string, sent time.Time, eventID string) *Message { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { now := time.Now() sig := p.SignMessage(onion + messageTxt + sent.String() + now.String()) message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false} if contact.unacknowledgedMessages == nil { contact.unacknowledgedMessages = make(map[string]Message) } contact.unacknowledgedMessages[eventID] = *message return message } return nil } // AddMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile. func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, sent time.Time) (message *Message) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] // We don't really need a Signature here, but we use it to maintain order now := time.Now() sig := p.SignMessage(onion + messageTxt + sent.String() + now.String()) if ok { message = &Message{PeerID: onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: true} contact.Timeline.Insert(message) } return } // ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { message, ok := contact.unacknowledgedMessages[eventID] if ok { message.Error = error contact.Timeline.Insert(&message) // TODO: do we want a non timeline.Insert way to handle errors delete(contact.unacknowledgedMessages, eventID) } } } // AckSentMessageToPeer sets mesage to a peer as acknowledged func (p *Profile) AckSentMessageToPeer(onion string, eventID string) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { message, ok := contact.unacknowledgedMessages[eventID] if ok { message.Acknowledged = true contact.Timeline.Insert(&message) delete(contact.unacknowledgedMessages, eventID) } } } // AddGroupSentMessageError searches matching groups for the message by sig and marks it as an error func (p *Profile) AddGroupSentMessageError(groupServer string, signature string, error string) { for _, group := range p.Groups { if group.GroupServer == groupServer { if group.ErrorSentMessage([]byte(signature), error) { break } } } } // AcceptInvite accepts a group invite func (p *Profile) AcceptInvite(groupID string) (err error) { p.lock.Lock() defer p.lock.Unlock() group, ok := p.Groups[groupID] if ok { group.Accepted = true } else { err = errors.New("group does not exist") } return } // GetGroups returns an unordered list of group IDs associated with this profile. func (p *Profile) GetGroups() []string { p.lock.Lock() defer p.lock.Unlock() var keys []string for onion := range p.Groups { keys = append(keys, onion) } return keys } // GetContacts returns an unordered list of contact onions associated with this profile. func (p *Profile) GetContacts() []string { p.lock.Lock() defer p.lock.Unlock() var keys []string for onion := range p.Contacts { if onion != p.Onion { keys = append(keys, onion) } } return keys } // BlockPeer blocks a contact func (p *Profile) BlockPeer(onion string) (err error) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { contact.Blocked = true } else { err = errors.New("peer does not exist") } return } // UnblockPeer unblocks a contact func (p *Profile) UnblockPeer(onion string) (err error) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { contact.Blocked = false } else { err = errors.New("peer does not exist") } return } // BlockedPeers calculates a list of Peers who have been Blocked. func (p *Profile) BlockedPeers() []string { blockedPeers := []string{} for _, contact := range p.GetContacts() { c, _ := p.GetContact(contact) if c.Blocked { blockedPeers = append(blockedPeers, c.Onion) } } return blockedPeers } // TrustPeer sets a contact to trusted func (p *Profile) TrustPeer(onion string) (err error) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { contact.Trusted = true } else { err = errors.New("peer does not exist") } return } // IsBlocked returns true if the contact has been blocked, false otherwise func (p *Profile) IsBlocked(onion string) bool { contact, ok := p.GetContact(onion) if ok { return contact.Blocked } return false } // GetContact returns a contact if the profile has it func (p *Profile) GetContact(onion string) (*PublicProfile, bool) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] return contact, ok } // VerifyGroupMessage confirms the authenticity of a message given an onion, message and signature. func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, timestamp int32, ciphertext []byte, signature []byte) bool { group := p.GetGroup(groupID) if group == nil { return false } if onion == p.Onion { m := groupID + group.GroupServer + string(ciphertext) return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature) } m := groupID + group.GroupServer + string(ciphertext) decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion)) if err == nil && len(decodedPub) >= 32 { return ed25519.Verify(decodedPub[:32], []byte(m), signature) } return false } // SignMessage takes a given message and returns an Ed21159 signature func (p *Profile) SignMessage(message string) []byte { sig := ed25519.Sign(p.Ed25519PrivateKey, []byte(message)) return sig } // StartGroup when given a server, creates a new Group under this profile and returns the group id an a precomputed // invite which can be sent on the wire. func (p *Profile) StartGroup(server string) (groupID string, invite []byte, err error) { return p.StartGroupWithMessage(server, []byte{}) } // StartGroupWithMessage when given a server, and an initial message creates a new Group under this profile and returns the group id an a precomputed // invite which can be sent on the wire. func (p *Profile) StartGroupWithMessage(server string, initialMessage []byte) (groupID string, invite []byte, err error) { group, err := NewGroup(server) if err != nil { return "", nil, err } groupID = group.GroupID group.Owner = p.Onion signedGroupID := p.SignMessage(groupID + server) group.SignGroup(signedGroupID) invite, err = group.Invite(initialMessage) p.lock.Lock() defer p.lock.Unlock() p.Groups[group.GroupID] = group return } // GetGroup a pointer to a Group by the group Id, returns nil if no group found. func (p *Profile) GetGroup(groupID string) (g *Group) { p.lock.Lock() defer p.lock.Unlock() g = p.Groups[groupID] return } // ProcessInvite adds a new group invite to the profile. returns the new group ID func (p *Profile) ProcessInvite(invite string, peerHostname string) (string, error) { var gci protocol.GroupChatInvite err := proto.Unmarshal([]byte(invite), &gci) if err == nil { group := new(Group) group.GroupID = gci.GetGroupName() group.LocalID = GenerateRandomID() group.SignedGroupID = gci.GetSignedGroupId() copy(group.GroupKey[:], gci.GetGroupSharedKey()[:]) group.GroupServer = gci.GetServerHost() group.InitialMessage = gci.GetInitialMessage()[:] group.Accepted = false group.Owner = peerHostname group.Attributes = make(map[string]string) p.AddGroup(group) return group.GroupID, nil } return "", err } // AddGroup is a convenience method for adding a group to a profile. func (p *Profile) AddGroup(group *Group) { _, exists := p.Groups[group.GroupID] if !exists { p.lock.Lock() defer p.lock.Unlock() p.Groups[group.GroupID] = group } } // AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups. // If successful, adds the message to the group's timeline func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, string, *Message, bool) { for _, group := range p.Groups { success, dgm := group.DecryptMessage(ciphertext) if success { verified := p.VerifyGroupMessage(dgm.GetOnion(), group.GroupID, dgm.GetText(), dgm.GetTimestamp(), ciphertext, signature) // So we have a message that has a valid group key, but the signature can't be verified. // The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious) // Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised. if !verified { group.Compromised() return false, group.GroupID, nil, false } message, seen := group.AddMessage(dgm, signature) return true, group.GroupID, message, seen } } // If we couldn't find a group to decrypt the message with we just return false. This is an expected case return false, "", nil, false } func getRandomness(arr *[]byte) { if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil { utils.CheckError(err) } } // EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and // profile func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte, []byte, error) { if len(message) > MaxGroupMessageLength { return nil, nil, errors.New("group message is too long") } group := p.GetGroup(groupID) if group != nil { timestamp := time.Now().Unix() var prevSig []byte if len(group.Timeline.Messages) > 0 { prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature } else { prevSig = group.SignedGroupID } lenPadding := MaxGroupMessageLength - len(message) padding := make([]byte, lenPadding) getRandomness(&padding) dm := &protocol.DecryptedGroupMessage{ Onion: proto.String(p.Onion), Text: proto.String(message), SignedGroupId: group.SignedGroupID[:], Timestamp: proto.Int32(int32(timestamp)), PreviousMessageSig: prevSig, Padding: padding[:], } ciphertext, err := group.EncryptMessage(dm) if err != nil { return nil, nil, err } signature := p.SignMessage(groupID + group.GroupServer + string(ciphertext)) group.AddSentMessage(dm, signature) return ciphertext, signature, nil } return nil, nil, errors.New("group does not exist") } // 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 { p.lock.Lock() defer p.lock.Unlock() newp := new(Profile) bytes, _ := json.Marshal(p) json.Unmarshal(bytes, &newp) if timeline { for groupID := range newp.Groups { newp.Groups[groupID].Timeline = *p.Groups[groupID].Timeline.GetCopy() } } return newp }