package model import ( "bytes" "crypto/rand" "crypto/rsa" "cwtch.im/cwtch/protocol" "encoding/asn1" "encoding/json" "errors" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "github.com/golang/protobuf/proto" "golang.org/x/crypto/ed25519" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/sha3" "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 key [32]byte salt [128]byte save func() error } // AttemptLoadProfile attempts to load a profile from some encryptedBytes and a password func AttemptLoadProfile(encryptedbytes []byte, password string, saveFn func() error) *Profile { var dkr [32]byte var salty [128]byte //Separate the salt from the encrypted bytes, then generate the derived key salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:] dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512) //cast to arrays copy(dkr[:], dk) copy(salty[:], salt) var profile *Profile profile = decryptProfile(encryptedbytes, dkr) if profile != nil { profile.key = dkr profile.salt = salty profile.save = saveFn return profile } return nil } //decryptProfile decrypts the passed ciphertext into a Profile via the specified key if possible func decryptProfile(ciphertext []byte, key [32]byte) *Profile { var decryptNonce [24]byte copy(decryptNonce[:], ciphertext[:24]) decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key) if ok { profile := &Profile{} dec := json.NewDecoder(bytes.NewReader(decrypted)) err := dec.Decode(&profile) if err == nil { return profile } return nil } return nil } //EncryptProfile encrypts the cwtch Profile (padded to blocksize) via the specified key. func (p *Profile) EncryptProfile(blocksize int) []byte { var nonce [24]byte if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { panic(err) } bytes, _ := json.Marshal(p) padding := make([]byte, blocksize-len(bytes)) rand.Read(padding) bytes = append(bytes, padding...) encrypted := secretbox.Seal(nonce[:], []byte(bytes), &nonce, &p.key) encrypted = append(p.salt[:], encrypted...) return encrypted } // Save if initialized by adding the peer to a ProfileStore, will save this profile to the storage file func (p *Profile) Save() error { p.lock.Lock() err := p.save() p.lock.Unlock() return err } // OnProfileStoreAdd is an event function that should be called when the Profile is added to a ProfileStore // It takes in the necesary data to allow saving to work through the ProfileStore func (p *Profile) OnProfileStoreAdd(key [32]byte, salt [128]byte, saveFn func() error) { p.key = key p.salt = salt p.save = saveFn } // 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 p.save = func() error { return errors.New("Profile needs to be added to a ProfileStore before Save() is initialized and can work") } 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") }