cwtch/model/profile.go

370 lines
11 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"
"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"
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
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
Custom map[string]string
lock sync.Mutex
2018-03-09 20:44:13 +00:00
}
func (p *PublicProfile) init() {
p.Attributes = make(map[string]string)
}
// 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)
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)
p.Custom = make(map[string]string)
2018-03-09 20:44:13 +00:00
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()
}
// 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
}
// SetCustomAttribute allows applications to store arbitrary configuration info at the profile level.
func (p *Profile) SetCustomAttribute(name string, value string) {
p.lock.Lock()
defer p.lock.Unlock()
p.Custom[name] = value
}
// GetCustomAttribute returns the value of a value set with SetCustomAttribute. If no such value has been set exists is set to false.
func (p *Profile) GetCustomAttribute(name string) (value string, exists bool) {
p.lock.Lock()
defer p.lock.Unlock()
value, exists = p.Custom[name]
return
}
// 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
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
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()
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
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) {
existingGroup, exists := p.Groups[group.GroupID]
if !exists {
// TODO More robust error handling (confirm this onion checksum is correct)
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(group.Owner[:56]))
valid := ed25519.Verify(ed25519.PublicKey(decodedPub[:32]), []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.
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, *Message) {
for _, group := range p.Groups {
success, dgm := group.DecryptMessage(ciphertext)
2018-03-09 20:44:13 +00:00
if success {
2018-05-20 18:38:56 +00:00
2018-05-20 19:58:16 +00:00
// Assert that we know the owner of the group
2018-05-20 18:38:56 +00:00
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)
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, nil
}
return true, group.AddMessage(dgm, signature)
2018-03-09 20:44:13 +00:00
}
}
2018-03-31 19:33:32 +00:00
return false, nil
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) {
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 {
2018-05-28 17:44:47 +00:00
prevSig = group.SignedGroupID
}
2018-05-28 17:44:47 +00:00
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,
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))
return ciphertext, signature, nil
}
return nil, nil, errors.New("group does not exist")
2018-03-09 20:44:13 +00:00
}