package model import ( "crypto/rand" "cwtch.im/cwtch/protocol/groups" "encoding/base32" "encoding/hex" "encoding/json" "errors" "git.openprivacy.ca/openprivacy/connectivity/tor" "golang.org/x/crypto/ed25519" "io" "path/filepath" "strings" "sync" "time" ) // Authorization is a type determining client assigned authorization to a peer type Authorization string const ( // AuthUnknown is an initial state for a new unseen peer AuthUnknown Authorization = "unknown" // AuthApproved means the client has approved the peer, it can send messages to us, perform GetVals, etc AuthApproved Authorization = "approved" // AuthBlocked means the client has blocked the peer, it's messages and connections should be rejected AuthBlocked Authorization = "blocked" ) // PublicProfile is a local copy of a CwtchIdentity type PublicProfile struct { Name string Ed25519PublicKey ed25519.PublicKey Authorization Authorization DeprecatedBlocked bool `json:"Blocked"` Onion string Attributes map[string]string Timeline Timeline `json:"-"` LocalID string // used by storage engine State string `json:"-"` lock sync.Mutex UnacknowledgedMessages map[string]int } // 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]int) 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 } // IsServer returns true if the profile is associated with a server. func (p *PublicProfile) IsServer() (isServer bool) { _, isServer = p.GetAttribute(string(KeyTypeServerOnion)) return } // 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]int) } contact.Timeline.Insert(message) contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1 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 { mIdx, ok := contact.UnacknowledgedMessages[eventID] if ok { p.Timeline.Messages[mIdx].Error = error 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 { mIdx, ok := contact.UnacknowledgedMessages[eventID] if ok { contact.Timeline.Messages[mIdx].Acknowledged = true 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 } // SetContactAuthorization sets the authoirization level of a peer func (p *Profile) SetContactAuthorization(onion string, auth Authorization) (err error) { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { contact.Authorization = auth } else { err = errors.New("peer does not exist") } return } // GetContactAuthorization returns the contact's authorization level func (p *Profile) GetContactAuthorization(onion string) Authorization { p.lock.Lock() defer p.lock.Unlock() contact, ok := p.Contacts[onion] if ok { return contact.Authorization } return AuthUnknown } // ContactsAuthorizations calculates a list of Peers who are at the supplied auth levels func (p *Profile) ContactsAuthorizations(authorizationFilter ...Authorization) map[string]Authorization { authorizations := map[string]Authorization{} for _, contact := range p.GetContacts() { c, _ := p.GetContact(contact) authorizations[c.Onion] = c.Authorization } return authorizations } // 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 groups.GroupInvite err := json.Unmarshal([]byte(invite), &gci) if err == nil { group := new(Group) group.Version = CurrentGroupVersion group.GroupID = gci.GroupName group.LocalID = GenerateRandomID() group.SignedGroupID = gci.SignedGroupID copy(group.GroupKey[:], gci.SharedKey[:]) group.GroupServer = gci.ServerHost group.InitialMessage = []byte(gci.InitialMessage) 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.Onion, group.GroupID, dgm.Text, int32(dgm.Timestamp), 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 { if err != nil { // If we can't do randomness, just crash something is very very wrong and we are not going // to resolve it here.... panic(err.Error()) } } } // 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 := &groups.DecryptedGroupMessage{ Onion: p.Onion, Text: message, SignedGroupID: group.SignedGroupID[:], Timestamp: uint64(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() } for peerID := range newp.Contacts { newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy() } } return newp }