package model import ( "crypto/rand" "crypto/rsa" "cwtch.im/cwtch/protocol" "encoding/asn1" "errors" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "github.com/golang/protobuf/proto" "golang.org/x/crypto/ed25519" "io" "sync" "time" ) // PublicProfile is a local copy of a CwtchIdentity type PublicProfile struct { Name string Ed25519PublicKey ed25519.PublicKey Trusted bool Blocked bool Onion string } // Profile encapsulates all the attributes necessary to be a Cwtch Peer. type Profile struct { PublicProfile Contacts map[string]*PublicProfile Ed25519PrivateKey ed25519.PrivateKey OnionPrivateKey *rsa.PrivateKey Groups map[string]*Group lock sync.Mutex } // GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name. func GenerateNewProfile(name string) *Profile { p := new(Profile) p.Name = name pub, priv, _ := ed25519.GenerateKey(rand.Reader) p.Ed25519PublicKey = pub p.Ed25519PrivateKey = priv p.OnionPrivateKey, _ = utils.GeneratePrivateKey() // DER Encode the Public Key publicKeyBytes, _ := asn1.Marshal(rsa.PublicKey{ N: p.OnionPrivateKey.PublicKey.N, E: p.OnionPrivateKey.PublicKey.E, }) p.Onion = utils.GetTorHostname(publicKeyBytes) 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 } // AddCwtchIdentity takes a wire message and if it is a CwtchIdentity message adds the identity as a contact // otherwise returns an error func (p *Profile) AddCwtchIdentity(onion string, ci *protocol.CwtchIdentity) { p.AddContact(onion, &PublicProfile{Name: ci.GetName(), Ed25519PublicKey: ci.GetEd25519PublicKey(), Onion: onion}) } // AddContact allows direct manipulation of cwtch contacts func (p *Profile) AddContact(onion string, profile *PublicProfile) { p.lock.Lock() p.Contacts[onion] = profile p.lock.Unlock() } // RejectInvite rejects and removes a group invite func (p *Profile) RejectInvite(groupID string) { p.lock.Lock() delete(p.Groups, groupID) p.lock.Unlock() } // 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 } // 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) } contact, found := p.GetContact(onion) if found { m := groupID + group.GroupServer + string(ciphertext) return ed25519.Verify(contact.Ed25519PublicKey, []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) { group := NewGroup(server) groupID = group.GroupID signedGroupID := p.SignMessage(groupID + server) group.SignGroup(signedGroupID) invite, err = group.Invite() 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.SignedGroupID = gci.GetSignedGroupId() copy(group.GroupKey[:], gci.GetGroupSharedKey()[:]) group.GroupServer = gci.GetServerHost() group.Accepted = false group.Owner = peerHostname p.AddGroup(group) } // AddGroup is a convenience method for adding a group to a profile. func (p *Profile) AddGroup(group *Group) { existingGroup, exists := p.Groups[group.GroupID] if !exists { owner, ok := p.GetContact(group.Owner) if ok { valid := ed25519.Verify(owner.Ed25519PublicKey, []byte(group.GroupID+group.GroupServer), group.SignedGroupID) if valid { p.lock.Lock() defer p.lock.Unlock() p.Groups[group.GroupID] = group } } } else if exists && existingGroup.Owner == group.Owner { p.lock.Lock() defer p.lock.Unlock() p.Groups[group.GroupID] = group } // If we are sent an invite or group update by someone who is not an owner // then we reject the group. } // AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups. func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *Message) { for _, group := range p.Groups { success, dgm := group.DecryptMessage(ciphertext) if success { // Assert that we know the owner of the group owner, ok := p.Contacts[group.Owner] if ok { valid := ed25519.Verify(owner.Ed25519PublicKey, []byte(group.GroupID+group.GroupServer), dgm.SignedGroupId) // If we can decrypt the message, but the group id is wrong that means that // this message is from someone who was not invited to the group. // As such this group has been compromised, probably by one of the other members. // We set the flag to be handled by the UX and reject the message. if !valid { group.Compromised() return false, nil } } verified := p.VerifyGroupMessage(dgm.GetOnion(), group.GroupID, dgm.GetText(), dgm.GetTimestamp(), ciphertext, signature) return true, group.AddMessage(dgm, signature, verified) } } return false, nil } 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) { 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 := 1024 - 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 := group.EncryptMessage(dm) signature := p.SignMessage(groupID + group.GroupServer + string(ciphertext)) return ciphertext, signature, nil } return nil, nil, errors.New("group does not exist") }