cwtch/model/profile.go

391 lines
14 KiB
Go
Raw Normal View History

2018-03-09 20:44:13 +00:00
package model
import (
"crypto/rand"
2018-10-05 03:18:34 +00:00
"encoding/base32"
"encoding/hex"
2019-01-21 20:11:40 +00:00
"encoding/json"
"git.openprivacy.ca/openprivacy/connectivity/tor"
2018-03-09 20:44:13 +00:00
"golang.org/x/crypto/ed25519"
"path/filepath"
2018-10-05 03:18:34 +00:00
"strings"
"sync"
2018-03-30 21:16:51 +00:00
"time"
2018-03-09 20:44:13 +00:00
)
// Authorization is a type determining client assigned authorization to a peer
type Authorization string
const (
2020-07-08 18:29:33 +00:00
// 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"
)
2018-03-15 16:33:26 +00:00
// PublicProfile is a local copy of a CwtchIdentity
2018-03-09 20:44:13 +00:00
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
2018-03-09 20:44:13 +00:00
}
2018-03-15 16:33:26 +00:00
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
2018-03-09 20:44:13 +00:00
type Profile struct {
PublicProfile
Contacts map[string]*PublicProfile
2018-03-09 20:44:13 +00:00
Ed25519PrivateKey ed25519.PrivateKey
Groups map[string]*Group
}
2018-11-28 19:50:32 +00:00
// MaxGroupMessageLength is the maximum length of a message posted to a server group.
// TODO: Should this be per server?
const MaxGroupMessageLength = 1800
2018-11-28 19:50:32 +00:00
// 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() {
2019-02-11 21:40:20 +00:00
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
}
2020-07-23 21:40:44 +00:00
// 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
}
2018-03-15 16:33:26 +00:00
// GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name.
2018-03-09 20:44:13 +00:00
func GenerateNewProfile(name string) *Profile {
p := new(Profile)
2019-01-21 20:11:40 +00:00
p.init()
2018-03-09 20:44:13 +00:00
p.Name = name
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
p.Ed25519PublicKey = pub
p.Ed25519PrivateKey = priv
p.Onion = tor.GetTorV3Hostname(pub)
2018-03-09 20:44:13 +00:00
p.Contacts = make(map[string]*PublicProfile)
p.Contacts[p.Onion] = &p.PublicProfile
2018-03-09 20:44:13 +00:00
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()
2020-11-05 21:26:03 +00:00
// 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}
2020-10-07 20:53:22 +00:00
if contact.UnacknowledgedMessages == nil {
contact.UnacknowledgedMessages = make(map[string]int)
}
2020-10-08 19:51:17 +00:00
contact.Timeline.Insert(message)
2020-10-08 20:08:20 +00:00
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
2021-05-26 17:07:08 +00:00
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 {
2021-05-26 17:07:08 +00:00
contact.Timeline.Messages[mIdx].Error = error
2020-10-07 20:53:22 +00:00
delete(contact.UnacknowledgedMessages, eventID)
2021-05-26 17:07:08 +00:00
return mIdx
}
}
2021-05-26 17:07:08 +00:00
return -1
}
// AckSentMessageToPeer sets mesage to a peer as acknowledged
2021-05-03 18:35:35 +00:00
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 {
2020-10-08 20:08:20 +00:00
contact.Timeline.Messages[mIdx].Acknowledged = true
2020-10-07 20:53:22 +00:00
delete(contact.UnacknowledgedMessages, eventID)
2021-05-03 18:35:35 +00:00
return mIdx
}
}
2021-05-03 18:35:35 +00:00
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
//}
2018-03-30 21:16:51 +00:00
2018-03-15 16:33:26 +00:00
// SignMessage takes a given message and returns an Ed21159 signature
2018-03-09 20:44:13 +00:00
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")
//}
//
2019-01-21 20:11:40 +00:00
// 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 {
2019-01-21 20:11:40 +00:00
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()
}
2020-07-08 18:29:33 +00:00
for peerID := range newp.Contacts {
newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy()
}
}
2019-01-21 20:11:40 +00:00
return newp
}