cwtch/model/profile.go

420 lines
12 KiB
Go
Raw Normal View History

2018-03-09 20:44:13 +00:00
package model
import (
"crypto/rand"
2018-05-28 18:05:06 +00:00
"cwtch.im/cwtch/protocol"
2018-10-05 03:18:34 +00:00
"encoding/base32"
"encoding/hex"
2019-01-21 20:11:40 +00:00
"encoding/json"
"errors"
2018-06-23 16:15:36 +00:00
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
2018-06-29 19:20:07 +00:00
"github.com/golang/protobuf/proto"
2018-03-09 20:44:13 +00:00
"golang.org/x/crypto/ed25519"
2018-05-28 17:44:47 +00:00
"io"
"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
)
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
2018-03-30 21:16:51 +00:00
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
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
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.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
}
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
2018-10-05 03:18:34 +00:00
p.Onion = utils.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
}
2018-03-15 16:33:26 +00:00
// GetCwtchIdentityPacket returns the wire message for conveying this profiles identity.
2018-03-09 20:44:13 +00:00
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])
2018-03-09 20:44:13 +00:00
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
2018-03-09 20:44:13 +00:00
}
2018-05-16 20:53:09 +00:00
// 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
}
2018-05-03 04:12:45 +00:00
if onion == p.Onion {
m := groupID + group.GroupServer + string(ciphertext)
2018-05-03 04:12:45 +00:00
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
}
2018-10-05 03:18:34 +00:00
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)
2018-03-31 19:33:32 +00:00
}
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
}
2018-10-05 03:18:34 +00:00
// StartGroup when given a server, creates a new Group under this profile and returns the group id an a precomputed
2018-03-15 16:33:26 +00:00
// invite which can be sent on the wire.
2018-05-16 20:18:47 +00:00
func (p *Profile) StartGroup(server string) (groupID string, invite []byte, err error) {
return p.StartGroupWithMessage(server, []byte{})
}
2018-10-05 03:18:34 +00:00
// 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
}
2018-03-09 20:44:13 +00:00
groupID = group.GroupID
group.Owner = p.Onion
2018-05-16 20:53:09 +00:00
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
2018-03-09 20:44:13 +00:00
return
}
2018-05-16 20:53:09 +00:00
// 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
}
2018-03-15 16:33:26 +00:00
// ProcessInvite adds a new group invite to the profile.
func (p *Profile) ProcessInvite(gci *protocol.GroupChatInvite, peerHostname string) {
2018-03-09 20:44:13 +00:00
group := new(Group)
group.GroupID = gci.GetGroupName()
2019-02-04 19:53:42 +00:00
group.LocalID = generateRandomID()
2018-03-30 21:16:51 +00:00
group.SignedGroupID = gci.GetSignedGroupId()
2018-03-09 20:44:13 +00:00
copy(group.GroupKey[:], gci.GetGroupSharedKey()[:])
group.GroupServer = gci.GetServerHost()
group.InitialMessage = gci.GetInitialMessage()[:]
group.Accepted = false
group.Owner = peerHostname
2019-02-05 18:58:14 +00:00
group.Attributes = make(map[string]string)
2018-03-09 20:44:13 +00:00
p.AddGroup(group)
}
2018-03-31 19:33:32 +00:00
// AddGroup is a convenience method for adding a group to a profile.
2018-03-09 20:44:13 +00:00
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
}
2018-03-09 20:44:13 +00:00
}
2018-03-15 16:33:26 +00:00
// 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)
2018-03-09 20:44:13 +00:00
if success {
verified := p.VerifyGroupMessage(dgm.GetOnion(), group.GroupID, dgm.GetText(), dgm.GetTimestamp(), ciphertext, signature)
2018-10-05 03:18:34 +00:00
// 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
2018-10-05 03:18:34 +00:00
}
message, seen := group.AddMessage(dgm, signature)
return true, group.GroupID, message, seen
2018-03-09 20:44:13 +00:00
}
}
// 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
2018-03-09 20:44:13 +00:00
}
2018-05-28 17:44:47 +00:00
func getRandomness(arr *[]byte) {
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
utils.CheckError(err)
}
}
2018-03-15 16:33:26 +00:00
// 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) {
2018-11-28 19:50:32 +00:00
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
2019-02-03 01:18:33 +00:00
if len(group.Timeline.Messages) > 0 {
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
} else {
2018-05-28 17:44:47 +00:00
prevSig = group.SignedGroupID
}
2018-05-28 17:44:47 +00:00
2018-11-28 19:50:32 +00:00
lenPadding := MaxGroupMessageLength - len(message)
2018-05-28 17:44:47 +00:00
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,
2018-05-28 17:44:47 +00:00
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")
2018-03-09 20:44:13 +00:00
}
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()
}
}
2019-01-21 20:11:40 +00:00
return newp
}