package model import ( "crypto/rand" "cwtch.im/cwtch/protocol/groups" "encoding/base64" "encoding/hex" "encoding/json" "errors" "git.openprivacy.ca/cwtch.im/tapir/primitives" "golang.org/x/crypto/ed25519" "io" "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 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 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) 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 }