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"` Uuid string `json:"uuid,omitempty"` unlockedHiddenPeers PeerList } func CreateIdentity(nickname string) (*Identity, error) { var id Identity id.Nickname = nickname id.Uuid = uuid.New().String() id.RootKp = meowlib.NewKeyPair() GetConfig().me = &id id.MessageServers = ServerStorage{DbFile: uuid.NewString()} id.generateRandomHiddenStuff() err := id.CreateFolder() if err != nil { return nil, err } return &id, nil } func (id *Identity) CreateFolder() error { err := os.MkdirAll(filepath.Join(GetConfig().StoragePath, id.Uuid), 0700) if err != nil { return err } return nil } func (id *Identity) WipeFolder() error { err := os.RemoveAll(filepath.Join(GetConfig().StoragePath, id.Uuid)) if err != nil { return err } return nil } // 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.Uid = uuid.New().String() 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.MyPullServers = MessageServerUids peer.MyName = MyName peer.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, invitationMessage string) { // invitation Id found, this is an answer to an invitation for _, p := range id.Peers { if p.InvitationId == ReceivedContact.InvitationId { return true, p.Name, ReceivedContact.Name, ReceivedContact.InvitationMessage } } // it's an invitation return false, "", ReceivedContact.Name, ReceivedContact.InvitationMessage } // 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.Uid = uuid.New().String() peer.MyIdentity = meowlib.NewKeyPair() peer.MyEncryptionKp = meowlib.NewKeyPair() peer.MyLookupKp = meowlib.NewKeyPair() if ContactName != "" { peer.Name = ContactName } else { peer.Name = ReceivedContact.Name } peer.ContactEncryption = ReceivedContact.EncryptionPublicKey peer.ContactLookupKey = ReceivedContact.LookupPublicKey peer.ContactPublicKey = ReceivedContact.ContactPublicKey peer.InvitationId = ReceivedContact.InvitationId peer.InvitationMessage = ReceivedContact.InvitationMessage for srv := range ReceivedContact.PullServers { peer.ContactPullServers = append(peer.ContactPullServers, ReceivedContact.PullServers[srv].GetUid()) } /* 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.MyPullServers = MessageServerIdxs peer.MyName = MyName 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].Name = ReceivedContact.Name id.Peers[i].ContactEncryption = ReceivedContact.EncryptionPublicKey id.Peers[i].ContactLookupKey = ReceivedContact.LookupPublicKey id.Peers[i].ContactPublicKey = ReceivedContact.ContactPublicKey srvs := []string{} for srv := range ReceivedContact.PullServers { srvs = append(srvs, ReceivedContact.PullServers[srv].GetUid()) } id.Peers[i].ContactPullServers = srvs 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.Name = randomLenString(4, 20) p.ContactPublicKey = p.MyLookupKp.Public p.ContactEncryption = p.MyIdentity.Public p.ContactLookupKey = p.MyEncryptionKp.Public // p.Contact.AddUrls([]string{randomLenString(14, 60), randomLenString(14, 60)}) // todo add servers 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{} // get all servers servers, err := id.MessageServers.LoadAllServers() if err == nil { // build a server map 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 { // check if peer inviation is accepted 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 _, srv := range srvs { if len(srv.LookupKeys) > 0 { list = append(list, srv) } } } 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 } id.CreateFolder() err = os.WriteFile(filepath.Join(GetConfig().StoragePath, id.Uuid, ".jobs"), jsonjobs, 0600) 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) }