package model import ( "crypto/rand" "encoding/base32" "encoding/hex" "encoding/json" "git.openprivacy.ca/openprivacy/connectivity/tor" "golang.org/x/crypto/ed25519" "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() // We expect callers to verify addresses before we get to this point, so if this isn't a // valid address this is a noop. if tor.IsValidHostname(onion) { decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56])) if err == nil { profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32]) p.Contacts[onion] = profile } } 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) int { 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].Error = error delete(contact.UnacknowledgedMessages, eventID) return mIdx } } return -1 } // AckSentMessageToPeer sets mesage to a peer as acknowledged func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int { 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) return mIdx } } return -1 } // VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature. // The goal of this function is 2-fold: // 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least // knows the senders private key) // 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't // matter if we actually received this message from the server or from a hybrid protocol, all that matters is // that the sender and receivers agree that this message was intended for the group // The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at // https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups // on two different servers with the same key and then forwards messages between them to convince the parties in // each group that they are actually in one big group (with the intent to later censor and/or selectively send messages // to each group). //func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool { // // group := p.GetGroup(groupID) // if group == nil { // return false // } // // // We use our group id, a known reference server and the ciphertext of the message. // m := groupID + group.GroupServer + message // // // If the message is ostensibly from us then we check it against our public key... // if onion == p.Onion { // return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature) // } // // // Otherwise we derive the public key from the sender and check it against that. // 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 } //// ProcessInvite validates a group invite and adds a new group invite to the profile if it is valid. //// returns the new group ID on success, error on fail. //func (p *Profile) ProcessInvite(invite string) (string, error) { // gci, err := ValidateInvite(invite) // if err == nil { // if server, exists := p.GetContact(gci.ServerHost); !exists || !server.IsServer() { // return "", fmt.Errorf("unknown server. a server key bundle needs to be imported before this group can be verified") // } // group := new(Group) // group.Version = CurrentGroupVersion // group.GroupID = gci.GroupID // group.LocalID = GenerateRandomID() // copy(group.GroupKey[:], gci.SharedKey[:]) // group.GroupServer = gci.ServerHost // group.Accepted = false // group.Attributes = make(map[string]string) // group.Attributes[attr.GetLocalScope(constants.Name)] = gci.GroupName // p.AddGroup(group) // return gci.GroupID, nil // } // return "", err //} // // //// 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, int) { // for _, group := range p.Groups { // success, dgm := group.DecryptMessage(ciphertext) // if success { // // // Attempt to serialize this message // serialized, err := json.Marshal(dgm) // // // Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer // // to verify the message, we simply ignore it. // if err != nil { // return false, group.GroupID, nil, -1 // } // // // This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only // // be derivable from the cryptographic key) which contains many unique elements such as the time and random padding // verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, base64.StdEncoding.EncodeToString(serialized), signature) // // if !verified { // // An earlier version of this protocol mistakenly signed the ciphertext of the message // // instead of the serialized decrypted group message. // // This has 2 issues: // // 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages // // 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret. // // While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature // // structure in a backwards compatible way for the duration of the Groups Experiment. // // TODO: Delete this check when Groups are no long Experimental // if group.Version == 3 { // verified = p.VerifyGroupMessage(dgm.Onion, group.GroupID, string(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 { // return false, group.GroupID, nil, -1 // } // message, index := group.AddMessage(dgm, signature) // return true, group.GroupID, message, index // } // } // // // If we couldn't find a group to decrypt the message with we just return false. This is an expected case // return false, "", nil, -1 //} // //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() // // // 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, err // } // // dm := &groups.DecryptedGroupMessage{ // Onion: p.Onion, // Text: message, // SignedGroupID: hexGroupID, // Timestamp: uint64(timestamp), // PreviousMessageSig: prevSig, // Padding: padding[:], // } // // ciphertext, err := group.EncryptMessage(dm) // if err != nil { // return nil, nil, err // } // serialized, _ := json.Marshal(dm) // signature := p.SignMessage(groupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized)) // 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 }