|
- package model
-
- import (
- "crypto/rand"
- "cwtch.im/cwtch/protocol"
- "encoding/base32"
- "encoding/hex"
- "encoding/json"
- "errors"
- "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:"-"` // TODO: cache recent messages for client
- LocalID string // used by storage engine
- State string `json:"-"`
- lock sync.Mutex
- }
-
- // 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 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.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 = utils.GetTorV3Hostname(pub)
-
- p.Contacts = make(map[string]*PublicProfile)
- p.Contacts[p.Onion] = &p.PublicProfile
- p.Groups = make(map[string]*Group)
- return p
- }
-
- // GetCwtchIdentityPacket returns the wire message for conveying this profiles identity.
- func (p *Profile) GetCwtchIdentityPacket() (message []byte) {
- ci := &protocol.CwtchIdentity{
- Name: p.Name,
- Ed25519PublicKey: p.Ed25519PublicKey,
- }
- cpp := &protocol.CwtchPeerPacket{
- CwtchIdentify: ci,
- }
- message, err := proto.Marshal(cpp)
- utils.CheckError(err)
- return
- }
-
- // 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()
- }
-
- /*
- // AddMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
- func (p *Profile) AddMessageToContactTimeline(onion string, fromMe bool, message string, sent time.Time) {
- 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 + message + sent.String() + now.String())
- if ok {
- if fromMe {
- contact.Timeline.Insert(&Message{PeerID: p.Onion, Message: message, Timestamp: sent, Received: now, Signature: sig})
- } else {
- contact.Timeline.Insert(&Message{PeerID: onion, Message: message, Timestamp: sent, Received: now, Signature: sig})
- }
- }
- }
- */
-
- // 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
- }
-
- // 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.GetGroupByGroupID(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 {
- 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
- }
-
- // GetGroupByGroupID a pointer to a Group by the group Id, returns nil if no group found.
- func (p *Profile) GetGroupByGroupID(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.
- func (p *Profile) ProcessInvite(gci *protocol.GroupChatInvite, peerHostname string) {
- 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)
- }
-
- // 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.
- 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.GetGroupByGroupID(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
- }
|