304 lines
9.5 KiB
Go
304 lines
9.5 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"forge.redroom.link/yves/meowlib"
|
|
"github.com/ProtonMail/gopenpgp/v2/helper"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const maxHiddenCount = 30
|
|
|
|
type Identity struct {
|
|
Nickname string `json:"nickname,omitempty"`
|
|
DefaultAvatar string `json:"default_avatar,omitempty"`
|
|
RootKp meowlib.KeyPair `json:"id_kp,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Peers PeerList `json:"peers,omitempty"`
|
|
HiddenPeers [][]byte `json:"hidden_peers,omitempty"`
|
|
Device meowlib.KeyPair `json:"device,omitempty"`
|
|
KnownServers ServerList `json:"known_servers,omitempty"`
|
|
MessageServers ServerStorage `json:"message_servers,omitempty"`
|
|
DefaultDbPassword string `json:"default_db_password,omitempty"`
|
|
DbPasswordStore bool `json:"db_password_store,omitempty"`
|
|
OwnedDevices PeerList `json:"owned_devices,omitempty"`
|
|
StaticMtkServerPaths []ServerList `json:"static_mtk_server_paths,omitempty"`
|
|
DynamicMtkServeRules []string `json:"dynamic_mtk_serve_rules,omitempty"`
|
|
InvitationTimeout int `json:"invitation_timeout,omitempty"`
|
|
unlockedHiddenPeers PeerList
|
|
}
|
|
|
|
func CreateIdentity(nickname string) *Identity {
|
|
var id Identity
|
|
id.Nickname = nickname
|
|
id.RootKp = meowlib.NewKeyPair()
|
|
GetConfig().me = &id
|
|
id.MessageServers = ServerStorage{DbFile: uuid.NewString()}
|
|
id.generateRandomHiddenStuff()
|
|
return &id
|
|
}
|
|
|
|
// Creates an invitation for a peer, returns the newly created peer including infos to provide a ContactCard
|
|
func (id *Identity) InvitePeer(MyName string, ContactName string, MessageServerUids []string, InvitationMessage string) (*Peer, error) {
|
|
var peer Peer
|
|
peer.MyIdentity = meowlib.NewKeyPair()
|
|
peer.MyEncryptionKp = meowlib.NewKeyPair()
|
|
peer.MyLookupKp = meowlib.NewKeyPair()
|
|
peer.Name = ContactName
|
|
peer.InvitationId = uuid.New().String() // todo as param to identify then update url
|
|
/* if id.MessageServers.Servers == nil {
|
|
return nil, errors.New("no message servers defined in your identity")
|
|
}
|
|
for _, i := range MessageServerIdxs {
|
|
if i > len(id.MessageServers.Servers)-1 {
|
|
return nil, errors.New("requested server out of range of defined message servers")
|
|
}
|
|
}
|
|
for _, i := range MessageServerIdxs {
|
|
srv := id.MessageServers.Servers[i].GetServerCard()
|
|
peer.MyContact.PullServers = append(peer.MyContact.PullServers, srv)
|
|
}*/
|
|
pullServers, err := id.MessageServers.LoadServerCardsFromUids(MessageServerUids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
peer.MyContact.PullServers = pullServers
|
|
peer.MyContact.Name = MyName
|
|
peer.MyContact.ContactPublicKey = peer.MyIdentity.Public
|
|
peer.MyContact.EncryptionPublicKey = peer.MyEncryptionKp.Public
|
|
peer.MyContact.LookupPublicKey = peer.MyLookupKp.Public
|
|
peer.MyContact.InvitationId = peer.InvitationId
|
|
peer.MyContact.InvitationMessage = InvitationMessage
|
|
id.Peers = append(id.Peers, peer)
|
|
|
|
return &peer, nil
|
|
}
|
|
|
|
// Checks if the received contact card is an answer to an invitation, returns true if it is, and the proposed and received nicknames
|
|
func (id *Identity) CheckInvitation(ReceivedContact *meowlib.ContactCard) (isAnswer bool, proposedNick string, receivedNick string) {
|
|
for _, p := range id.Peers {
|
|
if p.InvitationId == ReceivedContact.InvitationId {
|
|
return true, p.Name, ReceivedContact.Name
|
|
}
|
|
}
|
|
return false, "", ReceivedContact.Name
|
|
}
|
|
|
|
// Answers an invitation, returns the newly created peer including infos to provide a ContactCard
|
|
func (id *Identity) AnswerInvitation(MyName string, ContactName string, MessageServerIdxs []string, ReceivedContact *meowlib.ContactCard) *Peer {
|
|
var peer Peer
|
|
//var myContactCard meowlib.ContactCard
|
|
peer.MyIdentity = meowlib.NewKeyPair()
|
|
peer.MyEncryptionKp = meowlib.NewKeyPair()
|
|
peer.MyLookupKp = meowlib.NewKeyPair()
|
|
if ContactName != "" {
|
|
peer.Name = ContactName
|
|
} else {
|
|
peer.Name = ReceivedContact.Name
|
|
}
|
|
peer.Contact = *ReceivedContact
|
|
/* for _, i := range MessageServerIdxs {
|
|
srv := id.MessageServers.Servers[i].GetServerCard()
|
|
peer.MyContact.PullServers = append(peer.MyContact.PullServers, srv)
|
|
}*/
|
|
srvCards, err := GetConfig().GetIdentity().MessageServers.LoadServerCardsFromUids(MessageServerIdxs)
|
|
if err != nil {
|
|
peer.MyContact.PullServers = srvCards
|
|
}
|
|
peer.MyContact.Name = MyName
|
|
peer.MyContact.ContactPublicKey = peer.MyIdentity.Public
|
|
peer.MyContact.EncryptionPublicKey = peer.MyEncryptionKp.Public
|
|
peer.MyContact.LookupPublicKey = peer.MyLookupKp.Public
|
|
peer.MyContact.InvitationId = ReceivedContact.InvitationId
|
|
peer.InvitationId = ReceivedContact.InvitationId
|
|
id.Peers = append(id.Peers, peer)
|
|
|
|
return &peer
|
|
}
|
|
|
|
// Finalizes an invitation, returns nil if successful
|
|
func (id *Identity) FinalizeInvitation(ReceivedContact *meowlib.ContactCard) error {
|
|
for i, p := range id.Peers {
|
|
if p.InvitationId == ReceivedContact.InvitationId {
|
|
id.Peers[i].Contact = *ReceivedContact
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("no matching contact found for invitationId " + ReceivedContact.InvitationId)
|
|
}
|
|
|
|
// LoadIdentity loads an identity from an encrypted file
|
|
func LoadIdentity(filename string, password string) (*Identity, error) {
|
|
var id Identity
|
|
GetConfig().memoryPassword = password
|
|
GetConfig().IdentityFile = filename
|
|
indata, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pass, err := helper.DecryptMessageWithPassword([]byte(password), string(indata))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal([]byte(pass), &id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
GetConfig().me = &id
|
|
return &id, err
|
|
}
|
|
|
|
func (id *Identity) Save() error {
|
|
if GetConfig().IdentityFile == "" {
|
|
return errors.New("identity filename empty")
|
|
}
|
|
b, _ := json.Marshal(id)
|
|
armor, err := helper.EncryptMessageWithPassword([]byte(GetConfig().memoryPassword), string(b))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(GetConfig().IdentityFile, []byte(armor), 0600)
|
|
return err
|
|
}
|
|
|
|
func (id *Identity) TryUnlockHidden(password string) error {
|
|
found := false
|
|
for _, encPeer := range id.HiddenPeers {
|
|
p := Peer{}
|
|
jsonPeer, err := meowlib.SymDecrypt(password, encPeer)
|
|
if err == nil {
|
|
err = json.Unmarshal(jsonPeer, &p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.dbPassword = password
|
|
id.unlockedHiddenPeers = append(id.unlockedHiddenPeers, p)
|
|
found = true
|
|
}
|
|
}
|
|
if found {
|
|
return nil
|
|
}
|
|
return errors.New("no peer found")
|
|
}
|
|
|
|
func (id *Identity) HidePeer(peerIdx int, password string) error {
|
|
serializedPeer, err := json.Marshal(id.Peers[peerIdx])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encrypted, err := meowlib.SymEncrypt(password, serializedPeer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// add encrypted peer data
|
|
id.HiddenPeers = append(id.HiddenPeers, encrypted)
|
|
// remove clear text peer
|
|
id.Peers = append(id.Peers[:peerIdx], id.Peers[peerIdx+1:]...)
|
|
return nil
|
|
}
|
|
|
|
func (id *Identity) generateRandomHiddenStuff() {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
count := r.Intn(maxHiddenCount) + 1
|
|
for i := 1; i < count; i++ {
|
|
var p Peer
|
|
p.Name = randomLenString(4, 20)
|
|
p.MyEncryptionKp = meowlib.NewKeyPair()
|
|
p.MyIdentity = meowlib.NewKeyPair()
|
|
p.MyLookupKp = meowlib.NewKeyPair()
|
|
p.Contact.Name = randomLenString(4, 20)
|
|
p.Contact.ContactPublicKey = p.MyLookupKp.Public
|
|
p.Contact.EncryptionPublicKey = p.MyIdentity.Public
|
|
p.Contact.LookupPublicKey = p.MyEncryptionKp.Public
|
|
p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)})
|
|
id.Peers = append(id.Peers, p)
|
|
id.HidePeer(0, randomLenString(8, 14))
|
|
// TODO Add random conversations
|
|
}
|
|
}
|
|
|
|
type BackgroundJob struct {
|
|
RootPublic string `json:"root_public,omitempty"`
|
|
Device meowlib.KeyPair `json:"device,omitempty"`
|
|
Jobs []*RequestsJob `json:"jobs,omitempty"`
|
|
}
|
|
|
|
type RequestsJob struct {
|
|
Server *Server `json:"server,omitempty"`
|
|
LookupKeys []meowlib.KeyPair `json:"lookup_keys,omitempty"`
|
|
}
|
|
|
|
func (id *Identity) GetRequestJobs() []*RequestsJob {
|
|
var list []*RequestsJob
|
|
srvs := map[string]*RequestsJob{}
|
|
// build a server map
|
|
servers, err := id.MessageServers.LoadAllServers()
|
|
if err != nil {
|
|
for _, server := range servers {
|
|
var rj RequestsJob
|
|
rj.Server = server
|
|
srvs[server.GetServerCard().GetUid()] = &rj
|
|
}
|
|
// add ids to the map
|
|
for _, peer := range id.Peers {
|
|
for _, server := range peer.MyPullServers {
|
|
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
|
}
|
|
}
|
|
// add hidden peers
|
|
for _, peer := range id.unlockedHiddenPeers {
|
|
for _, server := range peer.MyPullServers {
|
|
srvs[server].LookupKeys = append(srvs[server].LookupKeys, peer.MyLookupKp)
|
|
}
|
|
}
|
|
// todo add garbage
|
|
|
|
// todo random reorder
|
|
|
|
// build list
|
|
for _, value := range srvs {
|
|
list = append(list, value)
|
|
}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (id *Identity) SaveBackgroundJob() error {
|
|
var bj BackgroundJob
|
|
bj.Jobs = id.GetRequestJobs()
|
|
bj.RootPublic = id.RootKp.Public
|
|
bj.Device = id.Device
|
|
jsonjobs, err := json.Marshal(bj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(filepath.Join(GetConfig().StoragePath, ".jobs"), jsonjobs, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func randomLenString(min int, max int) string {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
n := r.Intn(max-min) + min
|
|
return randomString(n)
|
|
}
|
|
|
|
func randomString(n int) string {
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
|
s := make([]rune, n)
|
|
for i := range s {
|
|
s[i] = letters[r.Intn(len(letters))]
|
|
}
|
|
return string(s)
|
|
}
|