cwtch/model/profile.go

189 lines
5.6 KiB
Go

package model
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"git.mascherari.press/cwtch/protocol"
"github.com/golang/protobuf/proto"
"github.com/s-rah/go-ricochet/utils"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"time"
"encoding/asn1"
"strconv"
)
// PublicProfile is a local copy of a CwtchIdentity
type PublicProfile struct {
Name string
Ed25519PublicKey ed25519.PublicKey
Trusted bool
Blocked bool
}
// 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
Onion string
Groups map[string]*Group
}
// 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.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
}
// GetCwtchIdentity returns the wire message for conveying this profiles identity.
func (p *Profile) GetCwtchIdentity() (message []byte) {
ci := &protocol.CwtchIdentity{
Name: p.Name,
Ed25519PublicKey: p.Ed25519PublicKey,
}
message, err := proto.Marshal(ci)
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()})
}
// AddContact allows direct manipulation of cwtch contacts
func (p *Profile) AddContact(onion string, profile PublicProfile) {
p.Contacts[onion] = profile
}
// VerifyMessage confirms the authenticity of a message given an onion, message and signature.
func (p *Profile) VerifyMessage(onion string, message string, signature []byte) bool {
return ed25519.Verify(p.Contacts[onion].Ed25519PublicKey, []byte(message), signature)
}
// 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) {
group := NewGroup(server)
groupID = group.GroupID
invite = group.Invite()
return
}
func (p *Profile) GetGroupByGroupId(groupID string) (*Group) {
return p.Groups[groupID]
}
// 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()
copy(group.GroupKey[:], gci.GetGroupSharedKey()[:])
group.GroupServer = gci.GetServerHost()
group.Accepted = false
group.Owner = peerHostname
p.AddGroup(group)
}
// AddGroup is a conveniance method for adding a group to a profle.
func (p *Profile) AddGroup(group *Group) {
existingGroup, exists := p.Groups[group.GroupID]
if !exists {
p.Groups[group.GroupID] = group
}
if exists && existingGroup.Owner == group.Owner {
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.
// FIXME: This opens up an attack vector!!
}
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) {
for _, group := range p.Groups {
success, dgm := group.DecryptMessage(ciphertext)
if success {
// FIXME
verified := p.VerifyMessage(dgm.GetOnion(), dgm.GetText(), dgm.GetSignature())
group.AddMessage(dgm, verified)
}
}
}
// 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) (ciphertext []byte, signature []byte) {
group := p.Groups[groupID]
timestamp := time.Now().Unix()
signature = p.SignMessage(message + groupID + strconv.Itoa(int(timestamp)))
dm := &protocol.DecryptedGroupMessage {
Onion: proto.String(p.Onion),
Text: proto.String(message),
SignedGroupId: group.SignedGroupID,
Timestamp: proto.Int32(int32(timestamp)),
Signature: signature,
}
ciphertext = group.EncryptMessage(dm)
return
}
// Save makes a opy of the profile in the given file
func (p *Profile) Save(profilefile string) error {
bytes, _ := json.Marshal(p)
return ioutil.WriteFile(profilefile, bytes, 0600)
}
// LoadProfile loads a saved profile from a file.
func LoadProfile(profilefile string) (*Profile, error) {
bytes, _ := ioutil.ReadFile(profilefile)
profile := new(Profile)
err := json.Unmarshal(bytes, &profile)
return profile, err
}