493 lines
15 KiB
Go
493 lines
15 KiB
Go
package model
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"cwtch.im/cwtch/protocol/groups"
|
|
"encoding/base32"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
|
"golang.org/x/crypto/ed25519"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Authorization is a type determining client assigned authorization to a peer
|
|
type Authorization string
|
|
|
|
const (
|
|
// AuthUnknown is an initial state for a new unseen peer
|
|
AuthUnknown Authorization = "unknown"
|
|
// AuthApproved means the client has approved the peer, it can send messages to us, perform GetVals, etc
|
|
AuthApproved Authorization = "approved"
|
|
// AuthBlocked means the client has blocked the peer, it's messages and connections should be rejected
|
|
AuthBlocked Authorization = "blocked"
|
|
)
|
|
|
|
// PublicProfile is a local copy of a CwtchIdentity
|
|
type PublicProfile struct {
|
|
Name string
|
|
Ed25519PublicKey ed25519.PublicKey
|
|
Authorization Authorization
|
|
DeprecatedBlocked bool `json:"Blocked"`
|
|
Onion string
|
|
Attributes map[string]string
|
|
Timeline Timeline `json:"-"`
|
|
LocalID string // used by storage engine
|
|
State string `json:"-"`
|
|
lock sync.Mutex
|
|
UnacknowledgedMessages map[string]int
|
|
}
|
|
|
|
// 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
|
|
|
|
// GenerateRandomID generates a random 16 byte hex id code
|
|
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.UnacknowledgedMessages = make(map[string]int)
|
|
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
|
|
}
|
|
|
|
// IsServer returns true if the profile is associated with a server.
|
|
func (p *PublicProfile) IsServer() (isServer bool) {
|
|
_, isServer = p.GetAttribute(string(KeyTypeServerOnion))
|
|
return
|
|
}
|
|
|
|
// 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 = tor.GetTorV3Hostname(pub)
|
|
|
|
p.Contacts = make(map[string]*PublicProfile)
|
|
p.Contacts[p.Onion] = &p.PublicProfile
|
|
p.Groups = make(map[string]*Group)
|
|
return p
|
|
}
|
|
|
|
// AddContact allows direct manipulation of cwtch contacts
|
|
func (p *Profile) AddContact(onion string, profile *PublicProfile) {
|
|
p.lock.Lock()
|
|
profile.init()
|
|
// We expect callers to verify addresses before we get to this point, so if this isn't a
|
|
// valid address this is a noop.
|
|
if tor.IsValidHostname(onion) {
|
|
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
|
|
if err == nil {
|
|
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()
|
|
}
|
|
|
|
// AddSentMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
|
|
func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt string, sent time.Time, eventID string) *Message {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
contact, ok := p.Contacts[onion]
|
|
if ok {
|
|
now := time.Now()
|
|
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
|
|
|
|
message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false}
|
|
if contact.UnacknowledgedMessages == nil {
|
|
contact.UnacknowledgedMessages = make(map[string]int)
|
|
}
|
|
contact.Timeline.Insert(message)
|
|
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
|
|
return message
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
|
|
func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, sent time.Time) (message *Message) {
|
|
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 + messageTxt + sent.String() + now.String())
|
|
if ok {
|
|
message = &Message{PeerID: onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: true}
|
|
contact.Timeline.Insert(message)
|
|
}
|
|
return
|
|
}
|
|
|
|
// ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list
|
|
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
contact, ok := p.Contacts[onion]
|
|
if ok {
|
|
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
|
if ok {
|
|
p.Timeline.Messages[mIdx].Error = error
|
|
delete(contact.UnacknowledgedMessages, eventID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AckSentMessageToPeer sets mesage to a peer as acknowledged
|
|
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
contact, ok := p.Contacts[onion]
|
|
if ok {
|
|
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
|
if ok {
|
|
contact.Timeline.Messages[mIdx].Acknowledged = true
|
|
delete(contact.UnacknowledgedMessages, eventID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddGroupSentMessageError searches matching groups for the message by sig and marks it as an error
|
|
func (p *Profile) AddGroupSentMessageError(groupServer string, signature string, error string) {
|
|
for _, group := range p.Groups {
|
|
if group.GroupServer == groupServer {
|
|
if group.ErrorSentMessage([]byte(signature), error) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SetContactAuthorization sets the authoirization level of a peer
|
|
func (p *Profile) SetContactAuthorization(onion string, auth Authorization) (err error) {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
contact, ok := p.Contacts[onion]
|
|
if ok {
|
|
contact.Authorization = auth
|
|
} else {
|
|
err = errors.New("peer does not exist")
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetContactAuthorization returns the contact's authorization level
|
|
func (p *Profile) GetContactAuthorization(onion string) Authorization {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
contact, ok := p.Contacts[onion]
|
|
if ok {
|
|
return contact.Authorization
|
|
}
|
|
return AuthUnknown
|
|
}
|
|
|
|
// ContactsAuthorizations calculates a list of Peers who are at the supplied auth levels
|
|
func (p *Profile) ContactsAuthorizations(authorizationFilter ...Authorization) map[string]Authorization {
|
|
authorizations := map[string]Authorization{}
|
|
for _, contact := range p.GetContacts() {
|
|
c, _ := p.GetContact(contact)
|
|
authorizations[c.Onion] = c.Authorization
|
|
}
|
|
return authorizations
|
|
}
|
|
|
|
// 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.GetGroup(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 && len(decodedPub) >= 32 {
|
|
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
|
|
}
|
|
|
|
// GetGroup a pointer to a Group by the group Id, returns nil if no group found.
|
|
func (p *Profile) GetGroup(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. returns the new group ID
|
|
func (p *Profile) ProcessInvite(invite string, peerHostname string) (string, string, error) {
|
|
var gci groups.GroupInvite
|
|
err := json.Unmarshal([]byte(invite), &gci)
|
|
if err == nil {
|
|
group := new(Group)
|
|
group.Version = CurrentGroupVersion
|
|
group.GroupID = gci.GroupID
|
|
group.LocalID = GenerateRandomID()
|
|
group.SignedGroupID = gci.SignedGroupID
|
|
copy(group.GroupKey[:], gci.SharedKey[:])
|
|
group.GroupServer = gci.ServerHost
|
|
group.InitialMessage = []byte(gci.InitialMessage)
|
|
group.Accepted = false
|
|
group.Owner = peerHostname
|
|
group.Attributes = make(map[string]string)
|
|
p.AddGroup(group)
|
|
return group.GroupID, gci.GroupName, nil
|
|
}
|
|
return "", "", err
|
|
}
|
|
|
|
// 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.
|
|
// If successful, adds the message to the group's timeline
|
|
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.Onion, group.GroupID, dgm.Text, int32(dgm.Timestamp), 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 {
|
|
if err != nil {
|
|
// If we can't do randomness, just crash something is very very wrong and we are not going
|
|
// to resolve it here....
|
|
panic(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.GetGroup(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 := &groups.DecryptedGroupMessage{
|
|
Onion: p.Onion,
|
|
Text: message,
|
|
SignedGroupID: group.SignedGroupID[:],
|
|
Timestamp: uint64(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()
|
|
}
|
|
|
|
for peerID := range newp.Contacts {
|
|
newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy()
|
|
}
|
|
}
|
|
|
|
return newp
|
|
}
|